Skip to content

go-gorm/cmd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GORM CMD

GORM CMD is a code generation tool for Go that produces type-safe query interfaces and field helper methods for GORM. It eliminates runtime query errors by verifying database operations at compile time.

🚀 Features

  • Type-safe Queries – Compile-time validation of database operations
  • SQL Templates – Generate query methods directly from SQL template comments
  • Field Helpers – Auto-generated, strongly typed field accessor methods
  • Seamless GORM Integration – Works with existing GORM APIs out of the box

📦 Installation

Requires Go 1.18+ (with generics).

go install gorm.io/cmd/gorm@latest

⚡ Quick Start

1. Generate code from interfaces

gorm gen -i ./query.go -o ./generated

2. Run type-safe queries

import "your_project/generated"

func Example(db *gorm.DB, ctx context.Context) {
    // Template-based query (from interface)
    user, err := generated.Query[User](db).GetByID(ctx, 123)

    // Field-based query (using generated helpers)
    users, err := gorm.G[User](db).
        Where(generated.User.Age.Gt(18)).
        Find(ctx)

    fmt.Println(user, users)
}

🔎 API Overview

Template-based Query Generation

Write SQL/templating in interface method comments. Placeholders bind to method parameters automatically, and concrete, type‑safe implementations are generated.

type Query[T any] interface {
    // SELECT * FROM @@table WHERE id=@id
    GetByID(id int) (T, error)

    // SELECT * FROM @@table WHERE @@column=@value
    FilterWithColumn(column string, value string) (T, error)

    // where("name=@name AND age=@age")
    FilterByNameAndAge(name string, age int)

    // SELECT * FROM @@table
    // {{where}}
    //   {{if @user.Name }} name=@user.Name {{end}}
    //   {{if @user.Age > 0}} AND age=@user.Age {{end}}
    // {{end}}
    SearchUsers(user User) ([]T, error)

    // UPDATE @@table
    // {{set}}
    //   {{if user.Name != ""}} name=@user.Name, {{end}}
    //   {{if user.Age > 0}} age=@user.Age {{end}}
    // {{end}}
    // WHERE id=@id
    UpdateUser(user User, id int) error
}

Usage

Usage notes (ctx auto‑injection): if a method signature doesn’t include ctx context.Context, the generator adds it as the first parameter of the implementation.

import "your_project/generated"

func ExampleQuery(db *gorm.DB, ctx context.Context) {
    // Get a single user by ID
    user, err := generated.Query[User](db).GetByID(ctx, 123)

    // Filter users by dynamic column and value
    user, err := generated.Query[User](db).FilterWithColumn(ctx, "role", "admin")

    // Filter users by name and age
    users, err := generated.Query[User](db).FilterByNameAndAge("jinzhu", 25).Find(ctx)

    // Conditional search using template logic
    users, err := generated.Query[User](db).
        SearchUsers(ctx, User{Name: "jinzhu", Age: 25})

    // Update user with dynamic SET clause
    err := generated.Query[User](db).
        UpdateUser(ctx, updatedUser, 123)
}

Field Helper Generation

Helpers are generated for “column‑like” fields on your models. These enable expressive, compile-time validated queries.

Example Model

type User struct {
    ID        uint
    Name      string
    Email     string
    Age       int
    Status    string
    CreatedAt time.Time
}

Generated Helpers

// Equality
generated.User.ID.Eq(1)          // id = 1
generated.User.ID.Neq(1)         // id != 1
generated.User.ID.In(1, 2, 3)    // id IN (1, 2, 3)

// String
generated.User.Name.Like("%jinzhu%") // name LIKE '%jinzhu%'
generated.User.Name.IsNotNull()      // name IS NOT NULL

// Numeric
generated.User.Age.Gt(18)            // age > 18
generated.User.Age.Between(18, 65)   // age BETWEEN 18 AND 65

// Nullable (Scanner/Valuer) types
generated.User.Score.IsNull()        // score IS NULL (sql.NullInt64)
generated.User.LastLogin.IsNotNull() // last_login IS NOT NULL (sql.NullTime)

// ... more, see https://pkg.go.dev/gorm.io/cmd/gorm/field

Usage

// Simple filter
gorm.G[User](db).
    Where(generated.User.Status.Eq("active")).
    Find(ctx)

// Multiple conditions
gorm.G[User](db).
    Where(generated.User.Age.Gt(18), generated.User.Status.Eq("active")).
    Find(&users)

// Update using query helpers
gorm.G[User](db).
    Where(generated.User.Status.Eq("pending")).
    Update("status", "active")

// Update with Set: zero values + expressions (Assigner and SetExpr)
gorm.G[User](db).
    Where(generated.User.Name.Eq("alice")).
    Set(
        generated.User.Name.Set("jinzhu"),         // name = "jinzhu"
        generated.User.IsAdult.Set(false),         // is_adult = false (zero value)
        generated.User.Score.Set(sql.NullInt64{}), // score = NULL (zero value)
        generated.User.Age.Incr(1),                // age = age + 1
        generated.User.Age.SetExpr(                // age = GREATEST(age, 18)
            clause.Expr{SQL: "GREATEST(?, ?)", Vars: []any{clause.Column{Name: "age"}, 18}},
        ),
    ).
    Update(ctx)

// Create with Set
gorm.G[User](db).
    Set(
        generated.User.Name.Set("alice"),  // name = "alice"
        generated.User.Age.Set(0),         // age = 0
        generated.User.IsAdult.Set(false), // is_adult = false
        generated.User.Role.Set("active"), // role = "active"
    ).
    Create(ctx)

đź§  How Fields Are Chosen

Helpers are generated only for “column‑like” fields; associations are skipped.

  • Included: all integers, floats, string, bool, time.Time, []byte, and any named type implementing one of:
    • database/sql.Scanner, database/sql/driver.Valuer
    • gorm.io/gorm.Valuer, gorm.io/gorm/schema.SerializerInterface
  • Excluded: has one, has many, belongs to, many2many, and embedded slices/structs that represent relations

📝 Template DSL

GORM CMD provides a SQL template DSL:

Directive Purpose Example
@@table Resolves to the model’s table name SELECT * FROM @@table WHERE id=@id
@@column Dynamic column binding @@column=@value
@param Maps Go params to SQL params WHERE name=@user.Name
{{where}} Conditional WHERE clause {{where}} age > 18 {{end}}
{{set}} Conditional SET clause (UPDATE) {{set}} name=@name {{end}}
{{if}} Conditional SQL fragment {{if age > 0}} AND age=@age {{end}}
{{for}} Iteration over a collection {{for _, t := range tags}} ... {{end}}

Examples

-- Safe parameter binding
SELECT * FROM @@table WHERE id=@id AND status=@status

-- Dynamic column binding
SELECT * FROM @@table WHERE @@column=@value

-- Conditional WHERE
SELECT * FROM @@table
{{where}}
  {{if name != ""}} name=@name {{end}}
  {{if age > 0}} AND age=@age {{end}}
{{end}}

-- Dynamic UPDATE
UPDATE @@table
{{set}}
  {{if user.Name != ""}} name=@user.Name, {{end}}
  {{if user.Email != ""}} email=@user.Email {{end}}
{{end}}
WHERE id=@id

-- Iteration
SELECT * FROM @@table
{{where}}
  {{for _, tag := range tags}}
    {{if tag != ""}} tags LIKE concat('%',@tag,'%') OR {{end}}
  {{end}}
{{end}}

Generation Config (optional)

You don’t need any configuration to use the generator. For overrides, declare a package‑level genconfig.Config in the package being generated — the generator will pick it up automatically.

package examples

import (
    "database/sql"
    "gorm.io/cmd/gorm/field"
    "gorm.io/cmd/gorm/genconfig"
)

var _ = genconfig.Config{
    // Override CLI -o for files in this package
    OutPath: "examples/output",

    // Map Go types to field helper types
    FieldTypeMap: map[any]any{
        sql.NullTime{}: field.Time{},
    },

    // Map `gen:"name"` names to helper types
    FieldNameMap: map[string]any{
        "date": field.Time{}, // map fields with `gen:"date"` tag to Time field helper
        "json": JSON{},       // map fields with `gen:"json"` tag to custom JSON helper
    },

    // When true, apply only to current file instead of the whole package
    FileLevel: false,

    // Optional whitelists/blacklists (shell-style patterns):
    // Whitelist takes priority: if Include* is non-empty, only those are generated,
    // and Exclude* is ignored for that kind.
    // Interfaces can be specified by pattern or by type-conversion form, e.g. models.Query(nil)
    IncludeInterfaces: []any{"Query*", models.Query(nil)},
    ExcludeInterfaces: []any{"*Deprecated*"},

    // You can also specify struct types via type literal in the config file,
    // e.g. models.User{} (treated as "models.User"), in addition to patterns.
    IncludeStructs: []any{"User", "Account*", models.User{}},
    ExcludeStructs: []any{"*DTO"},
}

JSON Field Mapping Example

  1. Declare Configuration
package examples

import "gorm.io/cmd/gorm/genconfig"

var _ = genconfig.Config{
    OutPath: "examples/output",
    FieldNameMap: map[string]any{
        "json": JSON{},        // map fields with `gen:"json"` tag to custom JSON helper
    },
}
  1. Declare JSON on the model using struct tags
package models

type User struct {
    // ... other fields ...
    // Tell the generator to use the custom JSON helper for this column
    Profile string `gen:"json"`
}
  1. Define the JSON helper
// JSON is a field helper for JSON columns that generates different SQL for different databases.
type JSON struct{ column clause.Column }

func (j JSON) WithColumn(name string) JSON {
    c := j.column
    c.Name = name
    return JSON{column: c}
}

// Equal builds an expression using database-specific JSON functions to compare
func (j JSON) Equal(path string, value any) clause.Expression {
    return jsonEqualExpr{col: j.column, path: path, val: value}
}

type jsonEqualExpr struct {
    col  clause.Column
    path string
    val  any
}

func (e jsonEqualExpr) Build(builder clause.Builder) {
    if stmt, ok := builder.(*gorm.Statement); ok {
        switch stmt.Dialector.Name() {
        case "mysql":
            v, _ := json.Marshal(e.val)
            clause.Expr{SQL: "JSON_EXTRACT(?, ?) = CAST(? AS JSON)", Vars: []any{e.col, e.path, string(v)}}.Build(builder)
        case "sqlite":
            clause.Expr{SQL: "json_valid(?) AND json_extract(?, ?) = ?", Vars: []any{e.col, e.col, e.path, e.val}}.Build(builder)
        default:
            clause.Expr{SQL: "jsonb_extract_path_text(?, ?) = ?", Vars: []any{e.col, e.path[2:], e.val}}.Build(builder)
        }
    }
}
  1. Use it in queries
// This will generate different SQL depending on the database:
// MySQL:  "JSON_EXTRACT(`profile`, "$.vip") = CAST("true" AS JSON)"
// SQLite: "json_valid(`profile`) AND json_extract(`profile`, "$.vip") = 1"
got, err := gorm.G[models.User](db).
    Where(generated.User.Profile.Equal("$.vip", true)).Take(ctx)

About

GORM CMD

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 5