This commit is contained in:
2026-03-28 17:45:22 +01:00
commit 8726cf9e13
8 changed files with 851 additions and 0 deletions
View File
+90
View File
@@ -0,0 +1,90 @@
# gormcol
Type-safe GORM column descriptors and model generation utilities.
## Library
The `gormcol` package provides two approaches for referencing database columns:
### Field descriptors
Define struct-like variables mapping Go field names to `table.column` pairs:
```go
var PsAccess = struct {
IDProfile gormcol.Field
IDAuthorizationRole gormcol.Field
}{
IDProfile: gormcol.Field{Table: "ps_access", Column: "id_profile"},
IDAuthorizationRole: gormcol.Field{Table: "ps_access", Column: "id_authorization_role"},
}
```
```go
gormcol.Column(PsAccess.IDAuthorizationRole) // "id_authorization_role"
gormcol.ColumnOnTable(PsAccess.IDAuthorizationRole) // "ps_access.id_authorization_role"
gormcol.TableField(PsAccess.IDAuthorizationRole) // "ps_access"
```
### Reflection-based utilities
Extract column/table names from GORM struct tags at compile time:
```go
type PsAccess struct {
IDProfile uint `gorm:"column:id_profile;primaryKey"`
IDAuthorizationRole uint `gorm:"column:id_authorization_role"`
}
gormcol.Col[PsAccess]("IDAuthorizationRole") // "id_authorization_role"
gormcol.Tbl[PsAccess]() // "ps_access" (via NamingStrategy)
gormcol.ColOnTbl[PsAccess]("IDAuthorizationRole") // "ps_access.id_authorization_role"
gormcol.PK[PsAccess]() // "id_profile"
gormcol.Columns[PsAccess]() // ["id_profile", "id_authorization_role"]
gormcol.ColByJSON[PsAccess]("id_profile") // "id_profile"
```
### Naming strategy
The reflection-based functions fall back to a configurable `NamingStrategy` when no explicit `gorm:"column:..."` tag is set. Default is `schema.NamingStrategy{}`. Override it with your project's GORM naming strategy:
```go
gormcol.NamingStrategy = db.DB.NamingStrategy
```
## CLI
The `cmd/` directory contains a standalone tool that generates GORM model files with column descriptors.
### Build
```bash
go build -o gormcol-gen ./cmd/
```
### Usage
```
gormcol-gen --dsn <connection-string> [options]
```
| Flag | Default | Description |
|------|---------|-------------|
| `--dsn` | *(required)* | MySQL/MariaDB DSN, e.g. `user:pass@tcp(localhost:3306)/dbname` |
| `--filter` | `(ps_\|b2b_).*` | Regex matching table names to generate |
| `--out` | `./app/model/prestadb` | Output directory for generated files |
| `--pkg` | `prestadb` | Go package name for generated files |
### Example
```bash
./gormcol-gen --dsn "user:pass@tcp(localhost:3306)/mydb" --filter "ps_.*" --out ./internal/model --pkg model
```
This connects to the database, generates a `.go` model file for each matching table, and appends `<Model>Cols` variables with typed `gormcol.Field` descriptors to each file.
## Dependencies
- `gorm.io/gorm`
- `gorm.io/gen`
- `gorm.io/driver/mysql`
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"time"
"git.ma-al.com/goc_marek/gormcol"
)
func main() {
dsn := flag.String("dsn", "", "database DSN (required, e.g. user:pass@tcp(host:3306)/dbname)")
filter := flag.String("filter", "(ps_|b2b_).*", "regex to match table names")
outDir := flag.String("out", "./app/model/prestadb", "output directory for generated files")
pkgName := flag.String("pkg", "prestadb", "Go package name for generated files")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "gormcol-gen - generate GORM models with column descriptors\n\n")
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, " gormcol-gen --dsn <connection-string> [options]\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}
if len(os.Args) < 2 {
flag.Usage()
os.Exit(0)
}
flag.Parse()
if *dsn == "" {
fmt.Fprintln(os.Stderr, "error: --dsn is required")
flag.Usage()
os.Exit(1)
}
db, err := gormcol.ConnectDSN(*dsn)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
cfg := gormcol.GenConfig{
OutputDir: *outDir,
PkgName: *pkgName,
TableFilter: *filter,
}
if err := gormcol.NewWithConfig(db, cfg).GenModels(ctx); err != nil {
slog.Error("failed to generate models", "error", err)
os.Exit(1)
}
}
+214
View File
@@ -0,0 +1,214 @@
package gormcol
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"gorm.io/driver/mysql"
"gorm.io/gen"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type GenConfig struct {
OutputDir string
PkgName string
TableFilter string // regex pattern, e.g. "(ps_|b2b_).*"
}
func defaultConfig() GenConfig {
return GenConfig{
OutputDir: "./app/model/prestadb",
PkgName: "prestadb",
TableFilter: "ps_.*",
}
}
type GormGen struct {
db *gorm.DB
cfg GenConfig
}
func New(db *gorm.DB) *GormGen {
return &GormGen{db: db, cfg: defaultConfig()}
}
func NewWithConfig(db *gorm.DB, cfg GenConfig) *GormGen {
d := defaultConfig()
if cfg.OutputDir != "" {
d.OutputDir = cfg.OutputDir
}
if cfg.PkgName != "" {
d.PkgName = cfg.PkgName
}
if cfg.TableFilter != "" {
d.TableFilter = cfg.TableFilter
}
return &GormGen{db: db, cfg: d}
}
// ConnectDSN opens a MySQL/MariaDB connection from a DSN string.
func ConnectDSN(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Error),
})
if err != nil {
return nil, fmt.Errorf("failed to connect with dsn: %w", err)
}
return db, nil
}
func (m *GormGen) GenModels(ctx context.Context) error {
re, err := regexp.Compile("^" + m.cfg.TableFilter + "$")
if err != nil {
return fmt.Errorf("invalid table filter regex %q: %w", m.cfg.TableFilter, err)
}
g := gen.NewGenerator(gen.Config{
OutPath: m.cfg.OutputDir,
ModelPkgPath: m.cfg.PkgName,
FieldNullable: true,
FieldWithIndexTag: true,
})
g.UseDB(m.db)
tableNames, err := m.db.Migrator().GetTables()
if err != nil {
return fmt.Errorf("failed to get table list: %w", err)
}
matched := 0
for _, tableName := range tableNames {
if re.MatchString(tableName) {
g.GenerateModel(tableName)
matched++
}
}
fmt.Printf("Matched %d tables with filter %q\n", matched, m.cfg.TableFilter)
g.Execute()
if err := m.cleanupGeneratedFiles(); err != nil {
return fmt.Errorf("failed to cleanup generated files: %w", err)
}
if err := m.generateCols(re); err != nil {
return fmt.Errorf("failed to generate column descriptors: %w", err)
}
return nil
}
func (m *GormGen) cleanupGeneratedFiles() error {
filesToRemove := []string{"gen.go", "do.go", "_gen.go"}
dir := m.cfg.OutputDir
if !strings.HasPrefix(dir, "./") {
dir = "./" + dir
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
for _, fileName := range filesToRemove {
filePath := filepath.Join(absDir, fileName)
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to remove %s: %w", filePath, err)
}
fmt.Printf("Removed: %s\n", filePath)
}
}
files, err := os.ReadDir(absDir)
if err != nil {
return err
}
re, err := regexp.Compile("^(" + m.cfg.TableFilter + ")\\.gen\\.go$")
if err != nil {
return err
}
for _, file := range files {
name := file.Name()
if re.MatchString(name) {
oldPath := filepath.Join(absDir, name)
baseName := strings.TrimSuffix(name, ".gen.go")
newPath := filepath.Join(absDir, baseName+".go")
content, err := os.ReadFile(oldPath)
if err != nil {
return err
}
content = m.cleanModelContent(content)
if err := os.WriteFile(newPath, content, 0644); err != nil {
return err
}
if err := os.Remove(oldPath); err != nil {
return err
}
fmt.Printf("Renamed: %s -> %s\n", oldPath, newPath)
}
}
return nil
}
func (m *GormGen) cleanModelContent(content []byte) []byte {
result := string(content)
lines := strings.Split(result, "\n")
var newLines []string
importStarted := false
importEnded := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "import (" {
importStarted = true
newLines = append(newLines, line)
continue
}
if importStarted && trimmed == ")" {
importEnded = true
importStarted = false
newLines = append(newLines, line)
continue
}
if importStarted && !importEnded {
if strings.Contains(trimmed, "\"gorm.io/gen\"") ||
strings.Contains(trimmed, "\"gorm.io/gen/field\"") ||
strings.Contains(trimmed, "\"gorm.io/plugin/dbresolver\"") ||
strings.Contains(trimmed, "gen.DO") {
continue
}
}
if strings.Contains(trimmed, "type psCategoryDo struct") ||
strings.HasPrefix(trimmed, "func (p psCategoryDo)") {
continue
}
newLines = append(newLines, line)
}
result = strings.Join(newLines, "\n")
result = strings.ReplaceAll(result, "psCategoryDo", "")
return []byte(result)
}
+211
View File
@@ -0,0 +1,211 @@
package gormcol
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)
type fieldInfo struct {
GoName string
ColName string
}
type structInfo struct {
Name string
Table string
Fields []fieldInfo
FilePath string
}
func parseGormColumn(tag string) string {
for _, part := range strings.Split(tag, ";") {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "column:") {
return strings.TrimPrefix(part, "column:")
}
}
return ""
}
func parseModelFile(path string) (*structInfo, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
var si structInfo
si.FilePath = path
for _, decl := range f.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
if gd.Tok == token.CONST {
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, name := range vs.Names {
if strings.HasPrefix(name.Name, "TableName") {
if i < len(vs.Values) {
if bl, ok := vs.Values[i].(*ast.BasicLit); ok {
si.Table = strings.Trim(bl.Value, "\"")
}
}
}
}
}
}
if gd.Tok != token.TYPE {
continue
}
for _, spec := range gd.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
st, ok := ts.Type.(*ast.StructType)
if !ok {
continue
}
si.Name = ts.Name.Name
for _, field := range st.Fields.List {
if len(field.Names) == 0 || field.Tag == nil {
continue
}
goName := field.Names[0].Name
tag := strings.Trim(field.Tag.Value, "`")
var gormTag string
for _, t := range strings.Split(tag, " ") {
t = strings.TrimSpace(t)
if strings.HasPrefix(t, "gorm:") {
gormTag = strings.TrimPrefix(t, "gorm:")
gormTag = strings.Trim(gormTag, "\"")
break
}
}
colName := parseGormColumn(gormTag)
if colName == "" {
continue
}
si.Fields = append(si.Fields, fieldInfo{
GoName: goName,
ColName: colName,
})
}
}
}
if si.Name == "" {
return nil, nil
}
return &si, nil
}
func generateColsVarBlock(si *structInfo) string {
if len(si.Fields) == 0 {
return ""
}
var b strings.Builder
b.WriteString(fmt.Sprintf("\nvar %sCols = struct {\n", si.Name))
for _, f := range si.Fields {
b.WriteString(fmt.Sprintf("\t%s Field\n", f.GoName))
}
b.WriteString("}{\n")
for _, f := range si.Fields {
b.WriteString(fmt.Sprintf("\t%s: Field{Table: %q, Column: %q},\n", f.GoName, si.Table, f.ColName))
}
b.WriteString("}\n")
return b.String()
}
func (m *GormGen) generateCols(fileFilter *regexp.Regexp) error {
dir := m.cfg.OutputDir
if !strings.HasPrefix(dir, "./") {
dir = "./" + dir
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
entries, err := os.ReadDir(absDir)
if err != nil {
return err
}
const gormcolImport = "\"git.ma-al.com/goc_daniel/b2b/app/utils/gormcol\""
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
continue
}
// Match files by the table filter regex (strip .go suffix for matching)
fileBase := strings.TrimSuffix(entry.Name(), ".go")
if !fileFilter.MatchString(fileBase) {
continue
}
path := filepath.Join(absDir, entry.Name())
si, err := parseModelFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: skipping cols for %s: %v\n", entry.Name(), err)
continue
}
if si == nil || len(si.Fields) == 0 {
continue
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
fileContent := string(content)
if strings.Contains(fileContent, "Field{") {
continue
}
if !strings.Contains(fileContent, gormcolImport) {
if strings.Contains(fileContent, "import (") {
fileContent = strings.Replace(fileContent, "import (", "import (\n\t"+gormcolImport, 1)
} else if strings.Contains(fileContent, "package prestadb") {
fileContent = strings.Replace(fileContent, "package prestadb",
"package prestadb\n\nimport "+gormcolImport, 1)
}
}
colsBlock := generateColsVarBlock(si)
fileContent += colsBlock
if err := os.WriteFile(path, []byte(fileContent), 0644); err != nil {
return err
}
fmt.Printf("Cols: %s\n", entry.Name())
}
return nil
}
+24
View File
@@ -0,0 +1,24 @@
module git.ma-al.com/goc_marek/gormcol
go 1.26.0
require (
gorm.io/driver/mysql v1.6.0
gorm.io/gen v0.3.27
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gorm.io/datatypes v1.2.4 // indirect
gorm.io/hints v1.1.0 // indirect
gorm.io/plugin/dbresolver v1.5.0 // indirect
)
+71
View File
@@ -0,0 +1,71 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.1.6/go.mod h1:W8LmC/6UvVbHKah0+QOC7Ja66EaZXHwUTjgXY8YNWX8=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gen v0.3.27 h1:ziocAFLpE7e0g4Rum69pGfB9S6DweTxK8gAun7cU8as=
gorm.io/gen v0.3.27/go.mod h1:9zquz2xD1f3Eb/eHq4oLn2z6vDVvQlCY5S3uMBLv4EA=
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw=
gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y=
gorm.io/plugin/dbresolver v1.5.0 h1:XVHLxh775eP0CqVh3vcfJtYqja3uFl5Wr3cKlY8jgDY=
gorm.io/plugin/dbresolver v1.5.0/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0=
+182
View File
@@ -0,0 +1,182 @@
package gormcol
import (
"fmt"
"reflect"
"strings"
"gorm.io/gorm/schema"
)
// NamingStrategy is the GORM naming strategy used for column/table name resolution.
// Defaults to gorm.io/gorm/schema.NamingStrategy.
// Set this to NamingStrategy when using within a project that has a database connection.
var NamingStrategy schema.Namer = schema.NamingStrategy{}
type Tabler interface {
TableName() string
}
// Field represents a GORM column descriptor with table context.
// Define package-level variables to get type-safe column references:
//
// var PsAccess = struct {
// IDProfile gormcol.Field
// IDAuthorizationRole gormcol.Field
// }{
// IDProfile: gormcol.Field{Table: "ps_access", Column: "id_profile"},
// IDAuthorizationRole: gormcol.Field{Table: "ps_access", Column: "id_authorization_role"},
// }
type Field struct {
Table string
Column string
}
// Column returns the column name from a Field descriptor.
//
// gormcol.Column(prestadb.PsAccess.IDAuthorizationRole) // "id_authorization_role"
func Column(f Field) string {
return f.Column
}
// ColumnOnTable returns "table.column" from a Field descriptor.
//
// gormcol.ColumnOnTable(prestadb.PsAccess.IDAuthorizationRole) // "ps_access.id_authorization_role"
func ColumnOnTable(f Field) string {
return f.Table + "." + f.Column
}
// TableField returns the table name from a Field descriptor.
func TableField(f Field) string {
return f.Table
}
// -- Reflection-based utilities (for structs without Field descriptors) --
// Col extracts the GORM column name from the `gorm:"column:..."` struct tag.
//
// gormcol.Col[prestadb.PsAccess]("IDAuthorizationRole") // "id_authorization_role"
func Col[T any](fieldName string) string {
var zero T
typ := reflect.TypeOf(zero)
return columnFromType(typ, fieldName)
}
// ColOf extracts a GORM column name from a field on a nested struct referenced
// by structFieldName.
func ColOf[T, S any](structFieldName, fieldName string) string {
var zero T
typ := reflect.TypeOf(zero)
field, ok := typ.FieldByName(structFieldName)
if !ok {
panic(fmt.Sprintf("gormcol: struct %q has no field %q", typ.Name(), structFieldName))
}
return columnFromType(field.Type, fieldName)
}
// Tbl returns the table name for a GORM model.
// Uses TableName() method if the struct implements Tabler,
// otherwise falls back to the GORM naming strategy.
//
// gormcol.Tbl[prestadb.PsAccess]() // "ps_access"
func Tbl[T any]() string {
var zero T
if t, ok := any(zero).(Tabler); ok {
return t.TableName()
}
typ := reflect.TypeOf(zero)
return NamingStrategy.TableName(typ.Name())
}
// ColOnTbl returns the column prefixed with the table name (reflection-based).
//
// gormcol.ColOnTbl[prestadb.PsAccess]("IDAuthorizationRole") // "ps_access.id_authorization_role"
func ColOnTbl[T any](fieldName string) string {
return Tbl[T]() + "." + Col[T](fieldName)
}
// PK returns the primary key column name for a GORM model.
//
// gormcol.PK[prestadb.PsAccess]() // "id_authorization_role"
func PK[T any]() string {
var zero T
typ := reflect.TypeOf(zero)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
gormTag := field.Tag.Get("gorm")
if gormTag == "" || gormTag == "-" {
continue
}
for _, part := range strings.Split(gormTag, ";") {
if strings.TrimSpace(part) == "primaryKey" {
return parseGormColumn(gormTag)
}
}
}
return ""
}
// Columns returns all GORM column names for a struct type.
//
// gormcol.Columns[prestadb.PsAccess]() // []string{"id_profile", "id_authorization_role"}
func Columns[T any]() []string {
var zero T
typ := reflect.TypeOf(zero)
var cols []string
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.PkgPath != "" {
continue
}
col := parseGormColumn(field.Tag.Get("gorm"))
if col == "" {
col = NamingStrategy.ColumnName(typ.Name(), field.Name)
}
cols = append(cols, col)
}
return cols
}
// ColByJSON finds the GORM column name by matching the struct field's json tag.
//
// gormcol.ColByJSON[prestadb.PsAccess]("id_authorization_role") // "id_authorization_role"
func ColByJSON[T any](jsonName string) string {
var zero T
typ := reflect.TypeOf(zero)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
continue
}
tagName := strings.Split(jsonTag, ",")[0]
if tagName == jsonName {
return columnFromType(typ, field.Name)
}
}
panic(fmt.Sprintf("gormcol: struct %q has no field with json tag %q", typ.Name(), jsonName))
}
func columnFromType(typ reflect.Type, fieldName string) string {
field, ok := typ.FieldByName(fieldName)
if !ok {
panic(fmt.Sprintf("gormcol: struct %q has no field %q", typ.Name(), fieldName))
}
gormTag := field.Tag.Get("gorm")
if gormTag == "" || gormTag == "-" {
return NamingStrategy.ColumnName(typ.Name(), fieldName)
}
col := parseGormColumn(gormTag)
if col != "" {
return col
}
return NamingStrategy.ColumnName(typ.Name(), fieldName)
}