codegen

package
v0.10.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultModuleName = "default"

DefaultModuleName is the name of the single implicit module that a config without a modules block describes.

Variables

This section is empty.

Functions

func AdoptLiveObjects added in v0.10.0

func AdoptLiveObjects(snapshot, live, declared Schema) (Schema, []DriftItem)

AdoptLiveObjects records into a snapshot the objects the database holds that the models do not declare, for the tables the snapshot already covers.

The migration differ works from the snapshot. An index or a constraint written by hand is absent from it, so every later migration is planned as though the object did not exist -- and PostgreSQL then re-checks a stale constraint against a column whose type just changed. Recording the object makes the differ able to drop it.

Only tables already in the snapshot are touched: a table no model owns belongs to another system, and drel does not take it over.

The returned items say what was recorded, so a person can see it. Adoption is not approval: the next `migrate new` emits a DROP for every adopted object the models do not declare. That SQL is reviewed before it is applied, and declaring the object on its model keeps it.

func DiffSchemas added in v0.3.0

func DiffSchemas(old, newSchema Schema, dialect string) (upSQL, downSQL string, err error)

DiffSchemas compares an old (previous snapshot) schema against a new (desired) schema and produces incremental up/down migration SQL containing only the changes. It returns ("", "") when the schemas are equivalent.

Coverage (up order; down is the sensible inverse):

  • new enums (Postgres only): CREATE TYPE / down DROP TYPE
  • new tables: CREATE TABLE + indexes / down DROP TABLE
  • dropped tables: DROP TABLE / down recreate
  • per-table column add/drop, type changes, NOT NULL changes
  • per-table index add/drop
  • table and column renames declared with a renamed_from marker
  • SQLite table rebuilds for the changes SQLite cannot ALTER in place

A table or a column declared with a renamed_from marker emits RENAME instead of a drop and an add; an ambiguous marker is rejected with an error rather than guessed. SQLite cannot ALTER a column's type, nullability, default, or CHECK in place, so those changes are emitted as a full table rebuild (create a scratch table, copy the rows, drop the original, rename, recreate the indexes) rather than as a warning comment or a silent skip.

func EmitDBFile

func EmitDBFile(models []ModelInfo, dbPkgName string) string

EmitDBFile generates the aggregated DB struct file that wires up all repositories. dbPkgName is the Go package name for the output file (e.g. "db").

func EmitMigrationsEmbed added in v0.7.0

func EmitMigrationsEmbed(pkgName, module string) string

EmitMigrationsEmbed returns the source of the file that embeds one module's migrations. The host passes the FS of each module to Engine.ApplyMigrationsFS, which merges the sets in version order.

pkgName is the Go package of the migration directory, and module is the name the file reports in its comment.

func EmitModelFile

func EmitModelFile(m ModelInfo) string

EmitModelFile generates the complete per-model _drel.go file content.

func EmitModelFileChecked

func EmitModelFileChecked(m ModelInfo) (string, error)

EmitModelFileChecked generates the per-model file, returning an error if the model uses unsupported features. Multi-column value objects are fully supported (see emitMultiValHelpers / expanded scan/diff/DDL).

func Generate

func Generate(configPath string) error

Generate runs the full code generation pipeline atomically: it loads config, scans packages, validates the model set, emits and gofmts every output file in memory, and only then writes anything to disk (write-to-temp + rename). A failure at any step before the write phase leaves the working tree untouched.

Stale generated *_drel.go files that would obstruct the package scan (because they reference deleted model types) are removed before scanning, but their contents are saved so they can be restored if any later step fails. The scan then accepts hand-written references to the generated symbols of the scanned package's own models, because the removal above is what makes them look undefined. After a successful write, truly stale files that no longer back any scanned model are permanently removed.

func GenerateCreateTable

func GenerateCreateTable(m ModelInfo, fks map[string]string, dialect string) string

GenerateCreateTable emits a CREATE TABLE statement for a single model. fks maps column names to referenced table names for foreign key constraints. dialect controls SQL type mappings and syntax ("postgres" or "sqlite").

func GenerateDropSchema

func GenerateDropSchema(models []ModelInfo) string

GenerateDropSchema emits DROP TABLE statements in reverse order to respect foreign key dependencies.

func GenerateModule added in v0.7.0

func GenerateModule(configPath, module string) error

GenerateModule generates the code of one module of the config. An empty module name covers every module.

The model files and the aggregated DB struct always cover every module, because the DB struct reaches all of the slices and a model file is the same whichever module asked for it. The module name selects which migration embeds are written.

func GenerateSchema

func GenerateSchema(models []ModelInfo, dialect string) string

GenerateSchema emits the full schema DDL for a slice of models, including enum type definitions, foreign key constraints, and many-to-many pivot tables. dialect controls SQL type mappings and syntax ("postgres" or "sqlite").

func GenerateWatch added in v0.5.0

func GenerateWatch(ctx context.Context, configPath string, pollInterval time.Duration) error

GenerateWatch runs Generate once, then re-runs it whenever a .go source file under the scanned package directories changes. It blocks until ctx is cancelled, at which point it returns nil. Generated *_drel.go files and the DB output file are ignored when detecting changes so regeneration never self-triggers. pollInterval controls the mtime poll cadence; pass 0 for the default (500ms). Watch uses only the standard library (no fsnotify) to honour the zero-runtime-dependency rule.

func GoTypeToSQL

func GoTypeToSQL(goType string, dialect string) string

GoTypeToSQL maps a Go type string to its corresponding SQL type for the given dialect. Pointer types are unwrapped to their base type. Unknown types default to "text"/"TEXT". Supported dialects: "postgres" (default), "sqlite".

func ResolveModuleRoot added in v0.5.0

func ResolveModuleRoot(startDir string) string

ResolveModuleRoot finds the Go module root (the nearest ancestor directory containing go.mod) by walking up from startDir. It falls back to startDir when no go.mod is found, preserving the prior behaviour for configs that already sit at the module root.

func SaveSnapshot added in v0.3.0

func SaveSnapshot(path string, s Schema) error

SaveSnapshot writes the schema to path as pretty-printed JSON, creating the parent directory if needed. The file is written with 0644 permissions.

func ValidateModels added in v0.5.0

func ValidateModels(models []ModelInfo) error

ValidateModels checks the aggregated model set for problems that would otherwise produce non-compiling or invalid generated code, and that the existing emit pipeline silently tolerated:

  • two models that generate the same DB-struct field name (pluralize(Name)), which yields duplicate struct fields that do not compile;
  • a relation field whose target model is not in the scanned set (typo or a package missing from drel.yaml), which was silently dropped;
  • a model with no db-mapped columns, which emits an empty INSERT column list and an invalid `INSERT INTO t () VALUES ()` at runtime.

It is called from Generate before any file is written so a failure leaves the working tree untouched.

func WriteMigrationsEmbed added in v0.7.0

func WriteMigrationsEmbed(dir, module string) (bool, error)

WriteMigrationsEmbed writes the embed file into a module's migration directory. It writes nothing when the directory holds no .sql file, because a //go:embed pattern that matches nothing does not compile.

Types

type CheckConstraint added in v0.10.0

type CheckConstraint struct {
	Name string `json:"name"`
	Expr string `json:"expr,omitempty"`
}

CheckConstraint is a named CHECK on a table. drel declares one per column and names it chk_<table>_<column>; introspection reports every one the database holds, whoever created it.

type Column added in v0.3.0

type Column struct {
	Name    string `json:"name"`
	Type    string `json:"type"`
	NotNull bool   `json:"notNull"`
	Default string `json:"default,omitempty"`
	Check   string `json:"check,omitempty"`
	Ref     string `json:"ref,omitempty"`
	PK      bool   `json:"pk,omitempty"`

	// Foreign key detail. Ref names the referenced table; RefColumn names the
	// referenced column and defaults to "id" when empty. The actions are stored
	// in SQL form ("CASCADE", "SET NULL", ...).
	RefColumn  string `json:"refColumn,omitempty"`
	OnDelete   string `json:"onDelete,omitempty"`
	OnUpdate   string `json:"onUpdate,omitempty"`
	Deferrable bool   `json:"deferrable,omitempty"`

	// RenamedFrom is the column's previous name. Diff-time only; see Table.
	RenamedFrom string `json:"-"`
}

Column describes a single column. Type is the dialect-resolved SQL type (already including any quoted enum type name). Ref, when non-empty, is the referenced table name for a foreign key on this column (always referencing that table's "id").

type Config

type Config struct {
	Packages []string       `yaml:"packages"`
	Modules  []ModuleConfig `yaml:"modules"`
	Output   OutputConfig   `yaml:"output"`
	Dialect  string         `yaml:"dialect"`
	// Seed is an optional path to a Go main package that seeds the database.
	// `drel seed` runs it with `go run`, passing through DATABASE_URL.
	Seed string `yaml:"seed"`

	// NoSlices records that the file declares an empty packages or modules
	// list. An application with no feature slice yet is a valid state: codegen
	// writes the aggregated DB file and generates no model. It differs from a
	// file that declares neither key, which is a mistake.
	NoSlices bool `yaml:"-"`
}

func LoadConfig

func LoadConfig(path string) (*Config, error)

func (*Config) AllPackages added in v0.7.0

func (c *Config) AllPackages() []string

AllPackages returns every package of every module. The aggregated DB struct needs all of them, even when generation targets one module.

func (*Config) Module added in v0.7.0

func (c *Config) Module(name string) (ModuleConfig, error)

Module returns one module by name. The error lists the names that exist.

func (*Config) ModuleList added in v0.7.0

func (c *Config) ModuleList() []ModuleConfig

ModuleList returns the modules of the config. A config that lists packages instead of modules describes one module named "default", so every caller works with one shape.

type Drift added in v0.10.0

type Drift struct {
	// Unmanaged lists objects the database holds that no model declares. Each
	// one is invisible to the migration differ, so a later migration is planned
	// as though it did not exist.
	Unmanaged []DriftItem
	// Missing lists objects the models declare that the database does not hold.
	// The usual cause is an unapplied migration.
	Missing []DriftItem
	// Different lists objects both sides hold in a different shape.
	Different []DriftItem
}

Drift is the difference between the live database and the schema the models declare. It answers the question the snapshot cannot: what does the database hold that drel does not manage, and what does drel expect that is not there?

func CompareSchemas added in v0.10.0

func CompareSchemas(live, declared Schema) Drift

CompareSchemas reports how the live database differs from the declared schema.

Column types are compared through a small normalisation table, because the server renders a type its own way: "character varying" for varchar, "timestamp with time zone" for timestamptz.

An expression is never compared as text, because the server rewrites one when it stores it: `state IN ('a', 'b')` comes back as `state = ANY (ARRAY['a'::questsate, 'b'::queststate])`. Matching that would need a SQL parser, and comparing the text reports a difference on a database that matches its models. So a CHECK expression is reported and never compared, and an index predicate is compared by presence alone: an index that gains or loses one is a difference, an index whose predicate is written differently is not.

func (Drift) Empty added in v0.10.0

func (d Drift) Empty() bool

Empty reports whether the two schemas agree.

type DriftItem added in v0.10.0

type DriftItem struct {
	Kind   string // "table", "column", "index", "foreign key", "check constraint", "enum"
	Table  string
	Name   string
	Detail string
}

DriftItem is one object, named for a person to act on.

func (DriftItem) String added in v0.10.0

func (d DriftItem) String() string

type EnumDef added in v0.3.0

type EnumDef struct {
	Name     string   `json:"name"`
	Values   []string `json:"values"`
	IsInt    bool     `json:"isInt,omitempty"`
	BaseType string   `json:"baseType,omitempty"`
}

EnumDef describes an enum type. Name is the lower-cased local Go type name. String enums are emitted as a Postgres CREATE TYPE; integer enums never get a CREATE TYPE (they map to an integer column + CHECK) and are excluded here.

type FieldInfo

type FieldInfo struct {
	Name       string
	GoType     string
	ColumnName string
	// RenamedFrom, when set, is this column's previous name. The migration
	// differ turns it into an ALTER TABLE ... RENAME COLUMN rather than a drop
	// and an add.
	RenamedFrom string
	IsExported  bool
	RelTag      string
	Relation    *RelationFieldInfo
	IsVO        bool // implements sql.Scanner + driver.Valuer (single-column VO)
	// IsDate marks drel.Date, which is a value object whose column type no
	// inference rule reaches: codegen reads types and never runs a method.
	IsDate         bool
	VOBaseType     string   // single-column VO: underlying basic Go type (e.g. "string", "int64"); empty if not derivable
	HasEqual       bool     // single-column VO defines an Equal(T) bool method usable for diffing
	IsComparable   bool     // single-column VO's Go type is comparable with == / != (types.Comparable)
	HasIsZero      bool     // single-column VO defines IsZero() bool, enabling the zero<->NULL bridge
	IsMultiColVO   bool     // implements drel.MultiColumnMapper (multi-column VO)
	MultiColPrefix string   // db tag used as column prefix for multi-column VOs
	MultiColNames  []string // expanded sub-column names from the db tag (multi-column VOs)
	MultiColTypes  []string // resolved SQL types per sub-column (default "text"; multi-column VOs)
	LocalGoType    string   // type name without package qualifier, for same-package generated code
	TypePkgPath    string   // import path of the field type's package (empty for primitives/same-package)
	// TypeRefPkgs lists the import paths an unnamed composite type refers to,
	// e.g. []time.Time needs "time". LocalGoType then holds placeholders that
	// the emitter replaces with the file's import aliases.
	TypeRefPkgs      []string
	IsPointer        bool // whether the field is a pointer type
	IsEnum           bool
	EnumValues       []string
	EnumIsInt        bool   // enum's underlying basic kind is integer (not string)
	EnumBaseType     string // Go base type of the enum, e.g. "string", "int", "int64"
	IsNamedPrimitive bool   // named type over a comparable basic kind with no enum consts (e.g. type Priority int)
	Unique           bool   // db tag option: unique index on this column
	Indexed          bool   // db tag option: index on this column
	// IndexNames lists the named indexes this column joins. A field may join
	// several: `db:"user_id,index=idx_a,index=idx_b"`. Fields sharing a name
	// compose one index, ordered by field declaration order.
	IndexNames []IndexMembership
	// References names the table this column points at, for a foreign key
	// declared with `db:"...,references=table[.column]"`. The target needs no
	// drel model, so a column can reference a table another system owns.
	References   string
	RefColumn    string // referenced column; "id" when the tag omits it
	OnDelete     string // ON DELETE action, already in SQL form (e.g. "CASCADE")
	OnUpdate     string // ON UPDATE action, already in SQL form
	Deferrable   bool   // emit DEFERRABLE INITIALLY DEFERRED
	CheckExpr    string // db tag option: column CHECK constraint expression (db:"...,check=expr")
	Default      string // db tag option: column DEFAULT value (db:"...,default=expr")
	TypeOverride string // db tag option: explicit SQL type (db:"...,type=jsonb"); overrides inference
	IsJSON       bool   // map/slice/struct mapped as JSON/jsonb (default for non-primitive, non-VO, non-enum container types)
	IsArray      bool   // slice field (JSON array by default; native T[] when TypeOverride is set on Postgres)
}

type FieldKind added in v0.9.0

type FieldKind int

FieldKind is the closed set of column shapes codegen understands. Every db-mapped field gets exactly one kind, decided one time by classifyField.

The set is closed on purpose. Before it existed, an if/else ladder ended in "any struct, slice, map or array is JSON", so every type codegen did not recognise silently became a jsonb column -- time.Time among them. A type that matches no kind is now KindUnsupported and the scan rejects it by name.

const (
	// KindUnsupported is a type codegen cannot map. It is rejected, never guessed.
	KindUnsupported FieldKind = iota
	// KindPrimitive is a basic Go type, or a pointer to one.
	KindPrimitive
	// KindBytes is []byte. Postgres bytea and SQLite BLOB are not implemented
	// yet, so it is rejected with its own message rather than silently mapped.
	KindBytes
	// KindTime is time.Time or *time.Time, which every driver maps natively.
	KindTime
	// KindMultiColVO implements drel.MultiColumnMapper: one field, many columns.
	KindMultiColVO
	// KindSingleColVO implements sql.Scanner and driver.Valuer.
	KindSingleColVO
	// KindEnum is a named type over a string or integer with declared constants.
	KindEnum
	// KindNamedPrimitive is a named type over a basic kind with no constants.
	KindNamedPrimitive
	// KindJSON is a slice, map, array or plain struct, stored as JSON.
	KindJSON
)

func (FieldKind) String added in v0.9.0

func (k FieldKind) String() string

type Index added in v0.3.0

type Index struct {
	Name    string   `json:"name"`
	Columns []string `json:"columns"`
	Unique  bool     `json:"unique,omitempty"`
	// Where is a partial-index predicate. An index with a predicate covers only
	// the rows that satisfy it, which is how a "one active row per user" rule is
	// expressed as a unique index.
	Where string `json:"where,omitempty"`
}

Index describes a single (possibly composite, possibly unique) index.

func (Index) SameShape added in v0.10.0

func (i Index) SameShape(o Index) bool

SameShape reports whether two indexes describe the same object. The differ compares shape, not only name: an index whose columns, uniqueness or predicate changed must be dropped and recreated.

type IndexDecl added in v0.10.0

type IndexDecl struct {
	Name    string
	Columns []string
	Unique  bool
	Where   string
}

IndexDecl is an index declared on the model, with its columns named explicitly and in order.

type IndexMembership added in v0.10.0

type IndexMembership struct {
	Name   string
	Unique bool
	Where  string // partial-index predicate; empty for a full index
}

IndexMembership is one named index a column joins. Unique and Where are declared on any member; a composite index takes them from whichever member declares them, and a conflict is rejected at scan time.

type KeyColumn added in v0.8.0

type KeyColumn struct {
	FieldName  string // exported Go field on the key struct; empty for a scalar key
	ColumnName string // database column name
	GoType     string // local Go type name, e.g. "int" or "UUID"
	// PkgPath is the import path of the field's type. It is empty for a
	// builtin type and for a type declared in the model's own package.
	PkgPath string
	// UnderlyingGoType is the normalization kind of the column: "int" for any
	// signed integer, "string", or "uuid.UUID". It selects the conversion
	// rule and names the type the conversion helper returns, which is not
	// always GoType: a named type or a sized integer needs a conversion back.
	UnderlyingGoType string
}

KeyColumn is one column of a model's primary key.

type ModelInfo

type ModelInfo struct {
	Name       string
	PkgPath    string
	PkgName    string
	PKType     string // display type for generated code (e.g., "int", "uuid.UUID")
	PKTypeFull string // fully qualified (e.g., "github.com/google/uuid.UUID")
	PKTypePkg  string // import path for external PK types (empty for primitives)
	TableName  string
	// RenamedFrom, when set, is this table's previous name. The migration
	// differ turns it into an ALTER TABLE ... RENAME TO.
	RenamedFrom   string
	Fields        []FieldInfo
	HasSoftDelete bool
	HasVersioned  bool
	HasAudit      bool
	Dir           string // filesystem directory of the package
	// Module is the feature slice this model belongs to. It is empty for a
	// config that lists packages instead of modules.
	Module string
	// Key lists the primary key columns in key order. A scalar key type has
	// exactly one entry whose FieldName is empty.
	Key []KeyColumn
	// KeyIsStruct reports whether the key type argument is a struct.
	KeyIsStruct bool
	// Indexes are declared on the model rather than derived from a field, so
	// they can name any column -- including a trait column such as created_at,
	// which has no Go field to tag.
	Indexes []IndexDecl
	// EnumOwners names the enum types whose Values()/IsValid() helpers this
	// model's generated file declares. Two models of one package that share an
	// enum type must not declare the helpers two times, so assignEnumOwners
	// gives each enum type to exactly one model of the package. A nil map means
	// the model owns every enum it uses, which is the case when a caller emits
	// one model on its own.
	EnumOwners map[string]bool
}

func ScanPackages

func ScanPackages(patterns []string, dir ...string) ([]ModelInfo, error)

ScanPackages loads the packages that match patterns and returns the drel models declared in them. A package with any load, parse or type error fails the scan: the caller needs the complete model set.

func ScanPackagesForGenerate added in v0.10.2

func ScanPackagesForGenerate(patterns []string, dir string) ([]ModelInfo, error)

ScanPackagesForGenerate is ScanPackages as the generator needs it. A hand-written file in a model package may use that package's own generated symbols (XMeta, XRepository, TxXRepository, the column set). Generation removes every *_drel.go file before the scan, so those references read as "undefined" and would fail a strict scan. They name exactly what generation is about to emit, so the scan accepts them and fails on everything else.

func (ModelInfo) IsCompositeKey added in v0.8.0

func (m ModelInfo) IsCompositeKey() bool

IsCompositeKey reports whether the model's primary key spans more than one column.

func (ModelInfo) PKColumns added in v0.8.0

func (m ModelInfo) PKColumns() []string

PKColumns returns the primary key column names in key order.

type ModuleConfig added in v0.7.0

type ModuleConfig struct {
	Name       string   `yaml:"name"`
	Packages   []string `yaml:"packages"`
	Migrations string   `yaml:"migrations"`
}

ModuleConfig describes one feature slice. A slice owns its models and its migrations, so a person can delete its directory and remove the feature.

type OutputConfig

type OutputConfig struct {
	DB         string `yaml:"db"`
	Migrations string `yaml:"migrations"`
}

type RelationFieldInfo

type RelationFieldInfo struct {
	Type        string // "has_many", "has_one", "belongs_to", "many_to_many"
	FK          string // foreign key column
	JoinTable   string // for many_to_many
	RefColumn   string // target FK column in pivot table (many-to-many)
	TargetModel string // target model name for foreign key resolution
}

RelationFieldInfo holds parsed relationship metadata from a `rel:"..."` struct tag.

type Schema added in v0.3.0

type Schema struct {
	Tables []Table   `json:"tables"`
	Enums  []EnumDef `json:"enums,omitempty"`
}

Schema is a JSON-serializable, dialect-resolved logical representation of the full database schema. It is the canonical input to the structured migration diff engine and the persisted migration snapshot.

func BuildSchema added in v0.3.0

func BuildSchema(models []ModelInfo, dialect string) Schema

BuildSchema produces the full logical schema for the given models and dialect. It includes all tables (models + many-to-many pivots), all columns (user columns + trait columns), all indexes (explicit/unique/composite from db tag options), and enum type definitions (Postgres only).

Ordering is deterministic: tables preserve model declaration order (pivots appended after, in discovery order), columns preserve PK-first then field order then trait order, and indexes within a table are sorted by name.

func LoadSnapshot added in v0.3.0

func LoadSnapshot(path string) (Schema, bool, error)

LoadSnapshot reads a schema snapshot from path. The returned bool is false when the file does not exist (which is not treated as an error).

type Table added in v0.3.0

type Table struct {
	Name string `json:"name"`
	// Checks lists the table's named CHECK constraints. It is filled by
	// introspection only: the generated schema carries a column's CHECK on the
	// column itself.
	Checks     []CheckConstraint `json:"checks,omitempty"`
	Columns    []Column          `json:"columns"`
	Indexes    []Index           `json:"indexes,omitempty"`
	PrimaryKey []string          `json:"primaryKey,omitempty"`

	// RenamedFrom is the table's previous name, declared by a renamed_from
	// marker on the model. It is a diff-time input only, never persisted: a
	// marker in the snapshot would make every future diff re-emit the rename.
	RenamedFrom string `json:"-"`
}

Table describes a single relation (table or pivot) and its columns/indexes. PrimaryKey, when non-empty, declares a table-level composite primary key: used by many-to-many pivot tables, and by a model with a multi-field struct key. A model with a single key column (scalar or single-field struct key) leaves PrimaryKey empty and instead declares the key inline on that column, via its PK field and a type string embedding "PRIMARY KEY".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL