Skip to content
/ cli Public

Type-safe GORM with SQL templates, model helpers, first-class associations

License

Notifications You must be signed in to change notification settings

go-gorm/cli

Repository files navigation

GORM CLI

GORM CLI generates two complementary layers of code for your GORM projects:

  • Type-safe, interface-driven query APIs — from Go interfaces with powerful SQL templates
  • Model-based field helpers — from your model structs for filters, updates, ordering, and associations

Together they deliver compile-time safety and a fluent, discoverable API for all database operations

Key Features

  • Strong Type Safety (default) — Compile-time guarantees with Go generics.
  • Interface-Driven Query Generation — Define Go interfaces with SQL template comments to produce concrete, type-safe methods.
  • Model-Based Field Helpers — Generate helpers from model structs for filtering, ordering, updates, and association handling.
  • Seamless GORM Integration — Works natively with gorm.io/gorm—no runtime magic, just plain Go.
  • Flexible Configuration — Customize output paths, include/exclude rules, and field mappings via genconfig.Config.
  • Rich Association Operations — Strongly-typed Create, CreateInBatch, Update, Unlink, and Delete for associations.

🚀 Quick Start

1) Install

go install gorm.io/cli/gorm@latest

2) Define Models & a Query Interface

// examples/models/user.go type User struct { gorm.Model Name string Age int Pets []Pet `gorm:"many2many:user_pets"` } type Pet struct { gorm.Model Name string } // examples/query.go type Query[T any] interface { // SELECT * FROM @@table WHERE id=@id GetByID(id int) (T, error) }

3) Generate & Use

# Default: strictly typed generics API gorm gen -i ./examples -o ./generated # Standard API (still generics-based, relaxed typing for more flexibility) gorm gen -i ./examples -o ./generated --typed=false
// Type-safe query // SELECT * FROM users WHERE id=123 u, err := generated.Query[User](db).GetByID(ctx, 123) // Field helpers // SELECT * FROM users WHERE age>18 users, _ := gorm.G[User](db).Where(generated.User.Age.Gt(18)).Find(ctx) // Association helpers: create a user with a pet gorm.G[User](db). Set( generated.User.Name.Set("alice"), generated.User.Pets.Create(generated.Pet.Name.Set("fido")), ). Create(ctx)

Two Generators, One Workflow

GORM CLI uses two generators that work together for full developer ergonomics:

1) Query API Generator

Define methods with SQL template comments in Go interfaces to generate concrete, type-safe methods.

2) Field Helper Generator

Generate strongly-typed helpers from your models to build filters, updates, ordering, and associations without raw SQL.

Supported types & associations (field helpers):

  • Basics: integers, floats, string, bool, time.Time, []byte
  • Named/custom types that implement database/sql.Scanner / driver.Valuer or GORM Serializer
  • Associations: has one **(including polymorphic), has many **(including polymorphic), belongs to, many2many

Working with Fields

Common predicates & setters:

// Predicates generated.User.ID.Eq(1) // id = 1 generated.User.Name.Like("%jinzhu%") // name LIKE '%jinzhu%' generated.User.Age.Between(18, 65) // age BETWEEN 18 AND 65 generated.User.Score.IsNull() // score IS NULL (e.g., sql.NullInt64) // Updates (supports expressions and zero-values) gorm.G[User](db). Where(generated.User.Name.Eq("alice")). Set( generated.User.Name.Set("jinzhu"), generated.User.IsAdult.Set(false), generated.User.Score.Set(sql.NullInt64{}), generated.User.Count.Incr(1), generated.User.Age.SetExpr(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"), generated.User.Age.Set(0), generated.User.Status.Set("active"), ). Create(ctx)

Standard API note (--typed=false) The default output is strictly typed. With the Standard API, you keep generics but gain flexibility to mix raw conditions with helpers:

generated.Query[User](db). Where("name = ?", "jinzhu"). // raw condition Where(generated.User.Age.Gt(18)). // typed helper Find(ctx)

Working with Associations

Association helpers appear on generated models as field.Struct[T] or field.Slice[T] (e.g. generated.User.Pets, generated.User.Account).

Supported operations (composed into Set(...).Update(ctx) or Set(...).Create(ctx)):

  • Create — create & link a related row per parent
  • CreateInBatch — batch create/link from a slice
  • Update — update related rows (with optional conditions)
  • Unlink — remove only the relationship (clear FK or delete join rows)
  • Delete — delete related rows (m2m: deletes join rows only)
// Create a pet for each matched user gorm.G[User](db). Where(generated.User.ID.Eq(1)). Set(generated.User.Pets.Create(generated.Pet.Name.Set("fido"))). Update(ctx) // Filter on the child before acting gorm.G[User](db). Where(generated.User.ID.Eq(1)). Set(generated.User.Pets.Where(generated.Pet.Name.Eq("old")).Delete()). Update(ctx) // Batch link two pets to an existing user gorm.G[User](db). Where(generated.User.ID.Eq(1)). Set(generated.User.Pets.CreateInBatch([]models.Pet{{Name: "rex"}, {Name: "spot"}})). Update(ctx)

Semantics by association type:

  • belongs to: Unlink clears the parent FK; Delete removes associated rows
  • has one / has many (including polymorphic): Unlink clears the child FK; Delete removes child rows
  • many2many: Unlink/Delete remove join rows only (both sides remain)

Parent operation semantics:

  • Create(ctx) inserts new parent rows using your Set(...) values, then applies association ops
  • Update(ctx) updates matched parent rows, then applies association ops

Template-Based Queries

Write SQL and light templating in interface comments; parameters bind automatically; implementations are generated and type-safe.

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) // 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}} // {{if user.Age >= 18}} is_adult=1 {{else}} is_adult=0 {{end}} // {{end}} // WHERE id=@id UpdateUser(user User, id int) error }

Context auto-injection If a method doesn’t include ctx context.Context, the generated implementation adds it.

Example usage

// SQL: SELECT * FROM users WHERE id=123 user, err := generated.Query[User](db).GetByID(ctx, 123) // SQL: SELECT * FROM users WHERE name="jinzhu" AND age=25 (appended to current builder) users, err := generated.Query[User](db).FilterByNameAndAge("jinzhu", 25).Find(ctx) // SQL UPDATE users SET name="jinzhu", age=20, is_adult=1 WHERE id=1 err := generated.Query[User](db).UpdateUser(ctx, User{Name: "jinzhu", Age: 20}, 1)

Template DSL

Directive Purpose Example
@@table Model table name SELECT * FROM @@table WHERE id=@id
@@column Dynamic column binding @@column=@value
@param Bind Go params to SQL params WHERE name=@user.Name
{{where}} Conditional WHERE wrapper {{where}} age > 18 {{end}}
{{set}} Conditional SET wrapper (UPDATE) {{set}} name=@name {{end}}
{{if}} Conditional SQL fragment {{if age > 0}} AND age=@age {{end}}
{{for}} Iterate 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 can generate without any config. For overrides, declare a package-level genconfig.Config in the package you generate:

package examples import ( "database/sql" "gorm.io/cli/gorm/field" "gorm.io/cli/gorm/genconfig" ) var _ = genconfig.Config{ OutPath: "examples/output", // Map Go types to helper kinds FieldTypeMap: map[any]any{ sql.NullTime{}: field.Time{}, }, // Map `gen:"name"` tags to helper kinds FieldNameMap: map[string]any{ "json": JSON{}, // use a custom JSON helper where fields are tagged `gen:"json"` }, // Narrow what gets generated (patterns or type literals) IncludeInterfaces: []any{"Query*", models.Query(nil)}, IncludeStructs: []any{"User", "Account*", models.User{}}, }

JSON Field Mapping Example

  1. Declare Configuration
package examples import "gorm.io/cli/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

Type-safe GORM with SQL templates, model helpers, first-class associations

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 8