Documentation
¶
Index ¶
- Constants
- func AdoptLiveObjects(snapshot, live, declared Schema) (Schema, []DriftItem)
- func DiffSchemas(old, newSchema Schema, dialect string) (upSQL, downSQL string, err error)
- func EmitDBFile(models []ModelInfo, dbPkgName string) string
- func EmitMigrationsEmbed(pkgName, module string) string
- func EmitModelFile(m ModelInfo) string
- func EmitModelFileChecked(m ModelInfo) (string, error)
- func Generate(configPath string) error
- func GenerateCreateTable(m ModelInfo, fks map[string]string, dialect string) string
- func GenerateDropSchema(models []ModelInfo) string
- func GenerateModule(configPath, module string) error
- func GenerateSchema(models []ModelInfo, dialect string) string
- func GenerateWatch(ctx context.Context, configPath string, pollInterval time.Duration) error
- func GoTypeToSQL(goType string, dialect string) string
- func ResolveModuleRoot(startDir string) string
- func SaveSnapshot(path string, s Schema) error
- func ValidateModels(models []ModelInfo) error
- func WriteMigrationsEmbed(dir, module string) (bool, error)
- type CheckConstraint
- type Column
- type Config
- type Drift
- type DriftItem
- type EnumDef
- type FieldInfo
- type FieldKind
- type Index
- type IndexDecl
- type IndexMembership
- type KeyColumn
- type ModelInfo
- type ModuleConfig
- type OutputConfig
- type RelationFieldInfo
- type Schema
- type Table
Constants ¶
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
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
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 ¶
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
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 ¶
EmitModelFile generates the complete per-model _drel.go file content.
func EmitModelFileChecked ¶
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 ¶
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. After a successful write, truly stale files that no longer back any scanned model are permanently removed.
func GenerateCreateTable ¶
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 ¶
GenerateDropSchema emits DROP TABLE statements in reverse order to respect foreign key dependencies.
func GenerateModule ¶ added in v0.7.0
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 ¶
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
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 ¶
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
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
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
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
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
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 (*Config) AllPackages ¶ added in v0.7.0
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
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.
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.
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 )
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.
type IndexDecl ¶ added in v0.10.0
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 (ModelInfo) IsCompositeKey ¶ added in v0.8.0
IsCompositeKey reports whether the model's primary key spans more than one column.
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 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
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
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.
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".