initial
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
250
README.md
Normal file
250
README.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# gormcol
|
||||||
|
|
||||||
|
Type-safe GORM column descriptors and model generation utilities.
|
||||||
|
|
||||||
|
## Library
|
||||||
|
|
||||||
|
The library provides [Field](#field) for type-safe column references and helper functions to extract column/table names.
|
||||||
|
|
||||||
|
### Field
|
||||||
|
|
||||||
|
`Field` represents a GORM column descriptor with table context. Define package-level variables to get type-safe column references:
|
||||||
|
|
||||||
|
```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"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
|
||||||
|
#### Column
|
||||||
|
|
||||||
|
Returns just the column name from a Field descriptor.
|
||||||
|
|
||||||
|
```go
|
||||||
|
gormcol.Column(dbmodel.PsAccess.IDAuthorizationRole) // "id_authorization_role"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ColumnOnTable
|
||||||
|
|
||||||
|
Returns "table.column" format from a Field descriptor.
|
||||||
|
|
||||||
|
```go
|
||||||
|
gormcol.ColumnOnTable(dbmodel.PsAccess.IDAuthorizationRole) // "ps_access.id_authorization_role"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TableField
|
||||||
|
|
||||||
|
Returns the table name from a Field descriptor.
|
||||||
|
|
||||||
|
```go
|
||||||
|
gormcol.TableField(dbmodel.PsAccess.IDAuthorizationRole) // "ps_access"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
The `cmd/` directory contains a standalone tool that generates GORM model files with column descriptors.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.ma-al.com/goc_marek/gormcol/cmd/gormcol@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install ./cmd/gormcol
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [fzf](https://github.com/junegunn/fzf) - required for interactive mode (optional for `--all`)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
gormcol-gen [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
DSN can be provided via `--dsn` flag or `DSN` env var (from `.env` file).
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|----------|------------------------|--------------------------------------------------|
|
||||||
|
| `--dsn` | *(from DSN env)* | MySQL/MariaDB DSN, e.g. `user:pass@tcp(localhost:3306)/dbname` |
|
||||||
|
| `--filter` | `(ps_|b2b_).*` | Regex matching table names to generate |
|
||||||
|
| `--all` | *(interactive)* | Generate all tables matching filter (shows confirmation) |
|
||||||
|
| `--out` | `./app/model/dbmodel` | Output directory for generated files |
|
||||||
|
| `--pkg` | `dbmodel` | Go package name for generated files |
|
||||||
|
|
||||||
|
### Interactive Mode (Default)
|
||||||
|
|
||||||
|
Without flags, the tool launches an interactive table selector:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gormcol-gen --dsn "user:pass@tcp(localhost:3306)/mydb"
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- **Fuzzy search** as you type
|
||||||
|
- **Tab** - toggle table selection (multi-select)
|
||||||
|
- **Enter** - confirm selection
|
||||||
|
- **Esc** - cancel
|
||||||
|
|
||||||
|
### Generate All Tables
|
||||||
|
|
||||||
|
Use `--all` to generate all tables matching the filter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gormcol-gen --dsn "user:pass@tcp(localhost:3306)/mydb" --all
|
||||||
|
```
|
||||||
|
|
||||||
|
A confirmation prompt appears:
|
||||||
|
```
|
||||||
|
WARNING: Generate all 325 tables? [Enter] confirm / [Esc] cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Enter** - confirm and generate
|
||||||
|
- **Esc** - cancel
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Configuration File (.env)
|
||||||
|
|
||||||
|
Create a `.env` file in your project root for default values:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database connection
|
||||||
|
DSN=user:pass@tcp(localhost:3306)/mydb
|
||||||
|
|
||||||
|
# Table filter (regex)
|
||||||
|
FILTER=(ps_|b2b_).*
|
||||||
|
|
||||||
|
# Output settings
|
||||||
|
OUT=./app/model/dbmodel
|
||||||
|
PKG=dbmodel
|
||||||
|
```
|
||||||
|
|
||||||
|
Command-line flags override `.env` values.
|
||||||
|
|
||||||
|
## Library Functions Reference
|
||||||
|
|
||||||
|
### ConnectDSN
|
||||||
|
|
||||||
|
Opens a MySQL/MariaDB connection from a DSN string.
|
||||||
|
|
||||||
|
```go
|
||||||
|
db, err := gormcol.ConnectDSN("user:pass@tcp(localhost:3306)/dbname")
|
||||||
|
```
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
Creates a new GormGen with default configuration.
|
||||||
|
|
||||||
|
```go
|
||||||
|
gg := gormcol.New(db)
|
||||||
|
```
|
||||||
|
|
||||||
|
### NewWithConfig
|
||||||
|
|
||||||
|
Creates a new GormGen with custom configuration.
|
||||||
|
|
||||||
|
```go
|
||||||
|
gg := gormcol.NewWithConfig(db, gormcol.GenConfig{
|
||||||
|
OutputDir: "./models",
|
||||||
|
PkgName: "models",
|
||||||
|
TableFilter: "ps_.*",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### GenModels
|
||||||
|
|
||||||
|
Generates GORM model files and column descriptors for matched tables.
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := context.Background()
|
||||||
|
err := gg.GenModels(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated Models
|
||||||
|
|
||||||
|
After generation, each model file contains a struct and a `Cols` variable:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// model/product.go
|
||||||
|
type Product struct {
|
||||||
|
ID uint `gorm:"column:id_product;primaryKey"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Price float32 `gorm:"column:price;type:decimal(20,6)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProductCols = struct {
|
||||||
|
ID Field
|
||||||
|
Name Field
|
||||||
|
Price Field
|
||||||
|
}{
|
||||||
|
ID: Field{Table: "ps_product", Column: "id_product"},
|
||||||
|
Name: Field{Table: "ps_product", Column: "name"},
|
||||||
|
Price: Field{Table: "ps_product", Column: "price"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Generated Models
|
||||||
|
|
||||||
|
### GORM queries with type-safe columns
|
||||||
|
|
||||||
|
Use `ColumnOnTable` for table-qualified column references in GORM clauses:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.ma-al.com/goc_marek/gormcol"
|
||||||
|
|
||||||
|
// Where clauses
|
||||||
|
db.Where(
|
||||||
|
gormcol.ColumnOnTable(model.ProductCols.Price) + " > ?",
|
||||||
|
100.0,
|
||||||
|
).Find(&products)
|
||||||
|
|
||||||
|
// Order
|
||||||
|
db.Order(gormcol.ColumnOnTable(model.ProductCols.Name) + " ASC").Find(&products)
|
||||||
|
|
||||||
|
// Joins
|
||||||
|
db.Joins("JOIN ps_category ON " +
|
||||||
|
gormcol.ColumnOnTable(model.ProductCols.ID) + " = ps_category.id_product",
|
||||||
|
).Find(&products)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unqualified column names
|
||||||
|
|
||||||
|
Use `Column` when the table is already scoped:
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Select(gormcol.Column(model.ProductCols.Name)).Find(&products)
|
||||||
|
|
||||||
|
// Raw queries
|
||||||
|
db.Raw("SELECT " + gormcol.Column(model.ProductCols.Name) + " FROM ps_product").Scan(&names)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table name
|
||||||
|
|
||||||
|
Use `TableField` to get the table name from a column descriptor:
|
||||||
|
|
||||||
|
```go
|
||||||
|
table := gormcol.TableField(model.ProductCols.ID) // "ps_product"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `gorm.io/gorm`
|
||||||
|
- `gorm.io/gen`
|
||||||
|
- `gorm.io/driver/mysql`
|
||||||
182
cmd/gormcol/main.go
Normal file
182
cmd/gormcol/main.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_marek/gormcol"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// main is the entry point for the gormcol-gen CLI tool.
|
||||||
|
// It parses flags, loads configuration from .env, connects to the database,
|
||||||
|
// and generates GORM models with column descriptors.
|
||||||
|
func main() {
|
||||||
|
dsn := flag.String("dsn", "", "database DSN (e.g. user:pass@tcp(host:3306)/dbname)")
|
||||||
|
filter := flag.String("filter", "(ps_|b2b_).*", "regex to match table names")
|
||||||
|
all := flag.Bool("all", false, "generate all tables matching filter (shows confirmation)")
|
||||||
|
outDir := flag.String("out", "./app/model/dbmodel", "output directory for generated files")
|
||||||
|
pkgName := flag.String("pkg", "dbmodel", "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 [options]\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "DSN can be provided via --dsn flag or DSN env var (from .env)\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load .env file if present in current directory.
|
||||||
|
godotenv.Load()
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Get DSN from flag or environment variable.
|
||||||
|
dsnValue := *dsn
|
||||||
|
if dsnValue == "" {
|
||||||
|
dsnValue = os.Getenv("DSN")
|
||||||
|
}
|
||||||
|
if dsnValue == "" {
|
||||||
|
flag.Usage()
|
||||||
|
fmt.Fprintln(os.Stderr, "\nerror: --dsn or DSN env is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the database.
|
||||||
|
db, err := gormcol.ConnectDSN(dsnValue)
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive mode (default): select tables using fzf.
|
||||||
|
// All mode: generate all tables matching filter with confirmation.
|
||||||
|
if *all {
|
||||||
|
if !confirmGenerateAll(db) {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if fzf is available for interactive mode.
|
||||||
|
if _, err := exec.LookPath("fzf"); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error: fzf is required for interactive mode. Install from https://github.com/junegunn/fzf")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
selected, err := selectTablesInteractive(db)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to select tables interactively", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(selected) == 0 {
|
||||||
|
fmt.Println("No tables selected, exiting.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
cfg.SelectedTables = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate models with column descriptors.
|
||||||
|
if err := gormcol.NewWithConfig(db, cfg).GenModels(ctx); err != nil {
|
||||||
|
slog.Error("failed to generate models", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectTablesInteractive displays all database tables in fzf for interactive selection.
|
||||||
|
// Returns a list of selected table names.
|
||||||
|
func selectTablesInteractive(db *gorm.DB) ([]string, error) {
|
||||||
|
type migratorWithGetTables interface {
|
||||||
|
GetTables() ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get tables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d tables, launching fzf for selection...\n", len(tableNames))
|
||||||
|
|
||||||
|
selected, err := runFzf(tableNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fzf selection failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirmGenerateAll displays a confirmation prompt before generating all tables.
|
||||||
|
// Returns true if user confirms, false if cancelled or timeout.
|
||||||
|
func confirmGenerateAll(db *gorm.DB) bool {
|
||||||
|
type migratorWithGetTables interface {
|
||||||
|
GetTables() ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tableNames) == 0 {
|
||||||
|
fmt.Println("No tables in database.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display yellow warning message and wait for user input.
|
||||||
|
yellow := "\033[33m"
|
||||||
|
reset := "\033[0m"
|
||||||
|
msg := fmt.Sprintf("%sWARNING:%s Generate all %d tables? [Enter] confirm / [Esc] cancel", yellow, reset, len(tableNames))
|
||||||
|
|
||||||
|
script := fmt.Sprintf(`echo; echo -e '%s'; read -t 10 -n 1 -s key; if [[ -z "$key" ]]; then exit 0; elif [[ "$key" == $'\e' ]]; then exit 1; else exit 0; fi`, msg)
|
||||||
|
cmd := exec.Command("bash", "-c", script)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFzf launches fzf for interactive table selection.
|
||||||
|
// Supports multi-select with Tab, fuzzy search, and keyboard navigation.
|
||||||
|
func runFzf(tableNames []string) ([]string, error) {
|
||||||
|
header := "Tab: toggle select | Enter: confirm | Esc: cancel"
|
||||||
|
cmd := exec.Command("fzf", "-m", "--height=50%", "--border", "--prompt", "Select tables> ", "--header", header)
|
||||||
|
cmd.Stdin = strings.NewReader(strings.Join(tableNames, "\n"))
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if exitErr.ExitCode() == 130 {
|
||||||
|
// User pressed Esc to cancel.
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("fzf failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse selected lines from fzf output.
|
||||||
|
selected := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
var result []string
|
||||||
|
for _, s := range selected {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
306
gen.go
Normal file
306
gen.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// Package gormcol provides GORM model generation with type-safe column descriptors.
|
||||||
|
//
|
||||||
|
// This file contains the core generation logic including:
|
||||||
|
// - Model generation from database tables
|
||||||
|
// - Configuration management
|
||||||
|
// - Output directory handling and file cleanup
|
||||||
|
package gormcol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gen"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenConfig holds configuration for model generation.
|
||||||
|
type GenConfig struct {
|
||||||
|
// OutputDir is the directory where generated files are written.
|
||||||
|
OutputDir string
|
||||||
|
// PkgName is the Go package name for generated files.
|
||||||
|
PkgName string
|
||||||
|
// TableFilter is a regex pattern to match table names.
|
||||||
|
// Example: "(ps_|b2b_).*" matches tables starting with ps_ or b2b_.
|
||||||
|
TableFilter string
|
||||||
|
// SelectedTables is a list of specific table names to generate.
|
||||||
|
// When set, TableFilter is ignored.
|
||||||
|
SelectedTables []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConfig returns the default configuration values.
|
||||||
|
func defaultConfig() GenConfig {
|
||||||
|
return GenConfig{
|
||||||
|
OutputDir: "./app/model/dbmodel",
|
||||||
|
PkgName: "dbmodel",
|
||||||
|
TableFilter: "ps_.*",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GormGen handles GORM model generation with column descriptors.
|
||||||
|
type GormGen struct {
|
||||||
|
db *gorm.DB
|
||||||
|
cfg GenConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new GormGen with default configuration.
|
||||||
|
func New(db *gorm.DB) *GormGen {
|
||||||
|
return &GormGen{db: db, cfg: defaultConfig()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithConfig creates a new GormGen with custom configuration.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if len(cfg.SelectedTables) > 0 {
|
||||||
|
d.SelectedTables = cfg.SelectedTables
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenModels generates GORM model files and column descriptors for matched tables.
|
||||||
|
// It cleans the output directory, generates models using gorm.io/gen,
|
||||||
|
// and appends <Model>Cols variables with type-safe Field descriptors.
|
||||||
|
func (m *GormGen) GenModels(ctx context.Context) error {
|
||||||
|
if err := m.cleanOutputDir(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clean output dir: %w", 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched int
|
||||||
|
var re *regexp.Regexp
|
||||||
|
|
||||||
|
if len(m.cfg.SelectedTables) > 0 {
|
||||||
|
tableSet := make(map[string]bool)
|
||||||
|
for _, t := range m.cfg.SelectedTables {
|
||||||
|
tableSet[t] = true
|
||||||
|
}
|
||||||
|
for _, tableName := range tableNames {
|
||||||
|
if tableSet[tableName] {
|
||||||
|
g.GenerateModel(tableName)
|
||||||
|
matched++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("Selected %d tables\n", matched)
|
||||||
|
} else {
|
||||||
|
re, err = regexp.Compile("^" + m.cfg.TableFilter + "$")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid table filter regex %q: %w", m.cfg.TableFilter, err)
|
||||||
|
}
|
||||||
|
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(); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate column descriptors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanOutputDir removes existing .go files from the output directory
|
||||||
|
// or creates it if it doesn't exist.
|
||||||
|
func (m *GormGen) cleanOutputDir() error {
|
||||||
|
dir := m.cfg.OutputDir
|
||||||
|
if !strings.HasPrefix(dir, "./") {
|
||||||
|
dir = "./" + dir
|
||||||
|
}
|
||||||
|
|
||||||
|
absDir, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(absDir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(absDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output dir: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Created: %s\n", absDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(absDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(entry.Name(), ".go") {
|
||||||
|
path := filepath.Join(absDir, entry.Name())
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove %s: %w", path, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Removed: %s\n", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupGeneratedFiles removes gorm.io/gen helper files and renames
|
||||||
|
// .gen.go files to .go with cleaned content.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var re *regexp.Regexp
|
||||||
|
if len(m.cfg.SelectedTables) > 0 {
|
||||||
|
pattern := "^(" + strings.Join(m.cfg.SelectedTables, "|") + ")\\.gen\\.go$"
|
||||||
|
re, err = regexp.Compile(pattern)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanModelContent removes gorm.io/gen-specific imports and type declarations
|
||||||
|
// from the generated model content.
|
||||||
|
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)
|
||||||
|
}
|
||||||
286
gencols.go
Normal file
286
gencols.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// Package gormcol provides GORM model generation with type-safe column descriptors.
|
||||||
|
//
|
||||||
|
// This file handles the generation of <Model>Cols variables that provide
|
||||||
|
// type-safe column references for use in GORM queries.
|
||||||
|
package gormcol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fieldInfo holds information about a struct field.
|
||||||
|
type fieldInfo struct {
|
||||||
|
GoName string // Go struct field name
|
||||||
|
ColName string // database column name from gorm tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// structInfo holds information about a parsed Go struct.
|
||||||
|
type structInfo struct {
|
||||||
|
Name string // struct name
|
||||||
|
Table string // table name from TableName const
|
||||||
|
Fields []fieldInfo // list of fields
|
||||||
|
FilePath string // source file path
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGormColumn extracts the column name value from a gorm tag string.
|
||||||
|
// The tag is expected to be in the format "column:name;otherTag" or similar.
|
||||||
|
// Returns the column name if found, empty string otherwise.
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseModelFile parses a Go model source file and extracts struct information.
|
||||||
|
// It uses go/ast to parse the file and extract:
|
||||||
|
// - Struct name from the type declaration
|
||||||
|
// - Table name from the TableName constant
|
||||||
|
// - Field names and their database column names from gorm tags
|
||||||
|
//
|
||||||
|
// Returns nil if no valid struct is found in the file.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateColsVarBlock generates the Go source code for a <Model>Cols variable.
|
||||||
|
// The generated code defines a struct with Field typed members for each
|
||||||
|
// database column, providing type-safe column references.
|
||||||
|
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 gormcol.Field\n", f.GoName))
|
||||||
|
}
|
||||||
|
b.WriteString("}{\n")
|
||||||
|
for _, f := range si.Fields {
|
||||||
|
b.WriteString(fmt.Sprintf("\t%s: gormcol.Field{Table: %q, Column: %q},\n", f.GoName, si.Table, f.ColName))
|
||||||
|
}
|
||||||
|
b.WriteString("}\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// findGoMod searches upward from startDir for a go.mod file.
|
||||||
|
func findGoMod(startDir string) (string, error) {
|
||||||
|
dir := startDir
|
||||||
|
for {
|
||||||
|
path := filepath.Join(dir, "go.mod")
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
return "", fmt.Errorf("go.mod not found from %s", startDir)
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readModulePath extracts the module path from a go.mod file.
|
||||||
|
func readModulePath(goModPath string) (string, error) {
|
||||||
|
content, err := os.ReadFile(goModPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(content), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "module ") {
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("module directive not found in %s", goModPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCols appends <Model>Cols variables to generated model files.
|
||||||
|
// It parses each .go file in the output directory, extracts struct fields
|
||||||
|
// with gorm column tags, and generates type-safe Field descriptors.
|
||||||
|
func (m *GormGen) generateCols() 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
|
||||||
|
}
|
||||||
|
|
||||||
|
goModPath, err := findGoMod(absDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
modulePath, err := readModulePath(goModPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gormcolImport := fmt.Sprintf("%q", modulePath+"")
|
||||||
|
|
||||||
|
var fileFilter *regexp.Regexp
|
||||||
|
if len(m.cfg.SelectedTables) > 0 {
|
||||||
|
pattern := "^(" + strings.Join(m.cfg.SelectedTables, "|") + ")$"
|
||||||
|
fileFilter, err = regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid selected tables pattern: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileFilter, err = regexp.Compile("^" + m.cfg.TableFilter + "$")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid table filter regex %q: %w", m.cfg.TableFilter, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "gormcol.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 dbmodel") {
|
||||||
|
fileContent = strings.Replace(fileContent, "package dbmodel",
|
||||||
|
"package dbmodel\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
|
||||||
|
}
|
||||||
25
go.mod
Normal file
25
go.mod
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.20.0 // indirect
|
||||||
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
|
gorm.io/datatypes v1.2.4 // indirect
|
||||||
|
gorm.io/hints v1.1.0 // indirect
|
||||||
|
gorm.io/plugin/dbresolver v1.6.2 // indirect
|
||||||
|
)
|
||||||
70
go.sum
Normal file
70
go.sum
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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.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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
|
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.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.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.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
|
||||||
|
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
|
||||||
58
gormcol.go
Normal file
58
gormcol.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Package gormcol provides type-safe GORM column descriptors.
|
||||||
|
//
|
||||||
|
// This package enables defining struct-like variables that map Go field names
|
||||||
|
// to database table.column pairs, allowing type-safe queries in GORM.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// 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"},
|
||||||
|
// }
|
||||||
|
package gormcol
|
||||||
|
|
||||||
|
// Field represents a GORM column descriptor with table context.
|
||||||
|
// It holds the table name and column name for type-safe column references.
|
||||||
|
type Field struct {
|
||||||
|
Table string // Database table name
|
||||||
|
Column string // Database column name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column returns the column name from a Field descriptor.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// gormcol.Column(dbmodel.PsAccess.IDAuthorizationRole) // "id_authorization_role"
|
||||||
|
func Column(f Field) string {
|
||||||
|
return f.Column
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnOnTable returns a qualified "table.column" string from a Field descriptor.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// gormcol.ColumnOnTable(dbmodel.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.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// gormcol.TableField(dbmodel.PsAccess.IDAuthorizationRole) // "ps_access"
|
||||||
|
func TableField(f Field) string {
|
||||||
|
return f.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table is an alias for TableField, returning the table name from a Field descriptor.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// gormcol.Table(dbmodel.PsAccess.IDAuthorizationRole) // "ps_access"
|
||||||
|
func Table(f Field) string {
|
||||||
|
return f.Table
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user