migrate

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DialectSQLite   = coremig.DialectSQLite
	DialectPostgres = coremig.DialectPostgres
)

Dialect identifiers re-exported from core/migrate for ergonomic use inside the framework package and in tests.

Variables

This section is empty.

Functions

func ApplySchemaDiff

func ApplySchemaDiff(ctx context.Context, db *sql.DB, changes []SchemaChange) (int, error)

ApplySchemaDiff applies every change in sequence inside a single transaction and returns the count applied. Aborts on first error, rolling everything back. Destructive changes (DROP COLUMN/TABLE) are refused — use ApplySchemaDiffWithOptions with AllowDestructive to opt in.

func ApplySchemaDiffWithOptions

func ApplySchemaDiffWithOptions(ctx context.Context, db *sql.DB, changes []SchemaChange, opts ApplyOptions) (int, error)

ApplySchemaDiffWithOptions is ApplySchemaDiff with a destructive-change opt-in. Everything still runs in a single transaction.

func AutoMigrate

func AutoMigrate(db *sql.DB, registry entity.Registry) error

AutoMigrate creates tables for all registered entities. Equivalent to AutoMigrateContext with a background context. See AutoMigrateContext for the full contract (advisory lock + single-transaction atomicity).

func AutoMigrateContext

func AutoMigrateContext(ctx context.Context, db *sql.DB, registry entity.Registry) error

AutoMigrateContext creates tables for all registered entities. Entities are migrated in dependency order so FK targets exist before referencing tables, and CREATE TABLE/INDEX IF NOT EXISTS keeps re-runs safe.

Two production guarantees beyond the bare DDL:

  • Advisory lock. The whole run is serialized by a database advisory lock (coremig.WithAdvisoryLock), so booting N replicas at once cannot race two concurrent streams of DDL against the same database. No-op on SQLite, which serializes writers itself.
  • Atomicity. All DDL runs inside one transaction; a failure on entity K rolls back entities 1..K-1 too, so a botched migration never leaves a half-created schema behind. Both Postgres and SQLite support transactional DDL.

db == nil is a silent no-op, matching the rest of the boot path.

func AutoMigratePlanContext

func AutoMigratePlanContext(ctx context.Context, db *sql.DB, plan Plan) error

AutoMigratePlanContext is AutoMigrateContext for a full Plan — entity and raw-Table schema PLUS stored routines (functions / procedures / triggers / views). Tables are created first, then each routine's Up runs (idempotent CREATE OR REPLACE), all inside the one advisory-locked transaction so the whole schema converges atomically.

func ColumnDefaultClause

func ColumnDefaultClause(f schema.Field, dialect Dialect) string

ColumnDefaultClause returns the trailing " DEFAULT …" fragment a column definition should include, or "" when none applies. Centralises two decisions every DDL site has to make:

  1. An explicit f.Default always wins — rendered via SQLDefault.
  2. Otherwise, f.AutoGenerate == AutoUUID on Postgres gets DEFAULT gen_random_uuid() so raw-SQL INSERTs that omit the id column don't crash with a NOT NULL constraint violation. (PG 13+ ships gen_random_uuid in core; on older versions it lived in pgcrypto.) SQLite has no built-in UUID generator — the column stays app-managed there to avoid silently doing nothing.
  3. AutoTimestamp is intentionally NOT auto-defaulted; created_at / updated_at are populated by framework hooks. Auto-emitting now() would create a divergence between SQLite (no DEFAULT, app sets it) and PG (DEFAULT now(), app ALSO sets it, last write wins).

The returned fragment is prefixed with a leading space so callers can always concat without inserting one themselves.

func MigrateEntity

func MigrateEntity(db *sql.DB, ent *entity.Entity) error

MigrateEntity creates the table for a single entity if it doesn't exist. It does not emit FK constraints since it has no view of the wider registry; callers that need foreign keys should call AutoMigrate. The dialect is auto-detected from db.

func MigrateEntityDialect

func MigrateEntityDialect(db *sql.DB, ent *entity.Entity, dialect Dialect) error

MigrateEntityDialect is the explicit-dialect variant used by callers that already know the target (e.g. CLI codegen, tests).

func ReadLiveColumns

func ReadLiveColumns(ctx context.Context, db *sql.DB, table string, dialect Dialect) (map[string]string, error)

ReadLiveColumns returns a map of column_name → data_type from the live DB. Empty map means "table doesn't exist".

func ReadLiveColumnsBulk

func ReadLiveColumnsBulk(ctx context.Context, db *sql.DB, tables []string, dialect Dialect) (map[string]map[string]string, error)

ReadLiveColumnsBulk reads column metadata for multiple tables in a single query. Returns a map of table name → {column name → type}.

This replaces N individual ReadLiveColumns calls with one bulk WHERE table_name IN (...) query, reducing SchemaDiff from ~59ms to ~5ms for 50 entities on Postgres.

func ReadLiveColumnsPostgres

func ReadLiveColumnsPostgres(ctx context.Context, db *sql.DB, table string) (map[string]string, error)

func ReadLiveColumnsSQLite

func ReadLiveColumnsSQLite(ctx context.Context, db *sql.DB, table string) (map[string]string, error)

func RenderMigrationFile

func RenderMigrationFile(version uint64, name, up, down string) string

RenderMigrationFile formats a versioned migration in the `-- +migrate` directive layout the runner parses. down may be empty.

func RunSeeds

func RunSeeds(ctx context.Context, db *sql.DB, registry entity.Registry) error

RunSeeds runs each entity's Seed function exactly once, tracked in the _gofastr_seeded ledger. Subsequent restarts short-circuit when the entity already has a ledger row. Call after AutoMigrate.

Contract:

  • Seed implementations MUST be idempotent. The framework cannot guarantee atomicity between user inserts and the ledger row, and concurrent RunSeeds calls across multiple processes can both see "not seeded" and both invoke Seed. Use INSERT … ON CONFLICT DO NOTHING (or a pre-check) inside Seed.
  • Seeds run serially in topological order. Independent seeds run one at a time; batch parallel work inside a single Seed func when needed.
  • RunSeeds is intended for serialized startup (one process at a time). HA deployments should gate seeding behind an external mechanism (init container, one-shot job, advisory lock).
  • The supplied ctx propagates into each Seed call. Cancelling ctx unblocks Seed implementations that respect it.
  • db == nil is a silent no-op, matching AutoMigrate's behaviour.
  • Attach a logger via WithSeedLogger to capture per-seed start/done/skip lifecycle events.

func SQLDefault

func SQLDefault(f schema.Field, dialect Dialect) string

SQLDefault returns the SQL DEFAULT value for a field. Booleans render as TRUE/FALSE for Postgres and 1/0 for SQLite (both engines accept either, but emitting the native form keeps DDL idiomatic and avoids surprises in pg_dump output).

func SQLType

func SQLType(f schema.Field, dialect Dialect) string

SQLType maps a schema FieldType to a SQL column type for the given dialect. Postgres needs concrete types (TIMESTAMPTZ, REAL, BOOLEAN); SQLite is more permissive but still benefits from explicit declarations.

func SaveSnapshot

func SaveSnapshot(path string, snap SchemaSnapshot) error

SaveSnapshot writes a snapshot JSON file (pretty-printed for clean diffs).

func TableExistsBulk

func TableExistsBulk(ctx context.Context, db *sql.DB, tables []string, dialect Dialect) (map[string]bool, error)

TableExistsBulk checks which tables exist in a single query. Returns a set of existing table names.

func WithSeedLogger

func WithSeedLogger(ctx context.Context, logger *slog.Logger) context.Context

WithSeedLogger attaches a slog.Logger to ctx so RunSeeds emits per-seed lifecycle events under it. When no logger is attached, RunSeeds writes to a discard handler — operators opt in.

Types

type ApplyOptions

type ApplyOptions struct {
	// AllowDestructive permits DROP COLUMN / DROP TABLE changes. When false
	// (the default), a change set containing any destructive change is
	// rejected with a *DestructiveChangeError before any DDL runs.
	AllowDestructive bool
}

ApplyOptions tunes ApplySchemaDiffWithOptions.

type Column

type Column struct {
	Name         string              // column name
	Type         schema.FieldType    // portable type; ignored when RawType is set
	RawType      string              // explicit SQL type, overrides Type (e.g. "NUMERIC(10,2)")
	NotNull      bool                // emit NOT NULL
	Unique       bool                // emit UNIQUE
	PrimaryKey   bool                // single-column PRIMARY KEY marker
	Default      any                 // default value (rendered via the same SQLDefault path)
	AutoGenerate schema.AutoGenerate // e.g. AutoUUID → DEFAULT gen_random_uuid() on Postgres
}

Column is one column of a raw Table.

type DestructiveChangeError

type DestructiveChangeError struct {
	Summaries []string
}

DestructiveChangeError is returned by ApplySchemaDiff when the change set contains destructive changes and the caller did not opt in to them. The Summaries list the blocked changes for a human-readable message.

func (*DestructiveChangeError) Error

func (e *DestructiveChangeError) Error() string

type Dialect

type Dialect = coremig.Dialect

Dialect identifies the SQL dialect the migrator emits for. It's an alias for coremig.Dialect so framework code and the lower-level migration system share one source of truth for dialect identity.

func DetectDialect

func DetectDialect(db *sql.DB) Dialect

DetectDialect returns the dialect of an open *sql.DB. It probes for PostgreSQL via SELECT version() (cheap, no side effects) and falls back to SQLite when that fails. The probe runs once per AutoMigrate call.

type ForeignKey

type ForeignKey struct {
	Column   string // local column
	RefTable string // target table/entity name (references its primary key)
}

ForeignKey declares a foreign key from a Table column to another table's primary key. RefTable is the registered name of the target (an entity name or another Table's Name); the reference targets that table's primary key.

type Index

type Index = entity.Index

Index is the secondary-index declaration for a raw Table, aliased from the entity package so raw-table users don't need to import it.

type Plan

type Plan struct {
	Registry entity.Registry
	Views    []View
	Routines []Routine
}

Plan is the full migration surface: tables (entities and/or raw Tables, via a registry) plus stored routines. It's what AutoMigratePlanContext and GeneratePlan consume so non-entity tables and routines reconcile with entities in one pass.

type Routine

type Routine struct {
	Name string // unique identifier used for change tracking
	Up   string // CREATE OR REPLACE … (run verbatim, idempotent)
	Down string // DROP … or CREATE OR REPLACE of the prior body
}

Routine is a database routine managed as a first-class migration object — a function, stored procedure, trigger, or view. Up is the idempotent definition (CREATE OR REPLACE …); Down rolls it back (a DROP, or a CREATE OR REPLACE of the previous body for true reversibility). The SQL is dialect- specific and run verbatim, so a `$$ … $$` body is a single statement.

AutoMigrate runs every routine's Up on boot (idempotent). GenerateMigration tracks each routine by a checksum of its Up and emits a migration only when the body changes — with a Down that restores the previous definition.

type RoutineDef

type RoutineDef struct {
	Up   string `json:"up"`
	Down string `json:"down"`
}

RoutineDef is the snapshot record for a routine — its Up and Down bodies, so a later generation can restore the previous definition on rollback.

type SchemaChange

type SchemaChange struct {
	Summary string // e.g. "posts: add column views INTEGER"
	SQL     string // executable DDL statement

	// Down is the inverse DDL that rolls this change back, used when
	// generating a reversible versioned migration file. CREATE TABLE → DROP
	// TABLE, ADD COLUMN → DROP COLUMN, DROP COLUMN → ADD COLUMN (recreates the
	// column from its previous type; row data is NOT restored). Empty when no
	// safe inverse is known.
	Down string

	// Destructive marks a change that can lose data — a DROP COLUMN today
	// (DROP TABLE in future). ApplySchemaDiff refuses to run destructive
	// changes unless the caller opts in via ApplySchemaDiffWithOptions, so a
	// routine `migrate diff --apply` never silently deletes a column. This is
	// the GORM-style "never drop by default" safety posture.
	Destructive bool
}

SchemaChange is one DDL fragment plus a human-friendly summary. Callers can apply directly via db.Exec or stitch them into a migration file.

func DiffSchema

func DiffSchema(ctx context.Context, db *sql.DB, registry entity.Registry) ([]SchemaChange, error)

DiffSchema returns the changes needed to bring db in line with every entity in the registry. Auto-detects dialect from the open DB; tables missing entirely from the DB are reported as CREATE TABLE statements (delegates to the same builder AutoMigrate uses).

type SchemaSnapshot

type SchemaSnapshot struct {
	Tables map[string]map[string]string `json:"tables"`
	// TableDDL holds the full CREATE TABLE statement per table, so a dropped
	// table's Down recreates it WITH all constraints (PK, NOT NULL, UNIQUE,
	// FK, defaults) rather than a lossy column-list reconstruction. Optional —
	// snapshots written by an older gofastr fall back to recreateTableSQL.
	TableDDL map[string]string     `json:"table_ddl,omitempty"`
	Views    map[string]RoutineDef `json:"views,omitempty"`
	Routines map[string]RoutineDef `json:"routines,omitempty"`
}

SchemaSnapshot is the serialized schema state migrations have been generated up to. Tables maps a table name to its column-name→SQL-type set; the same shape diffEntityFromLive consumes, so the snapshot diff and the live-DB diff share one code path.

func GenerateMigration

func GenerateMigration(reg entity.Registry, prev SchemaSnapshot, dialect Dialect) (up, down string, next SchemaSnapshot, err error)

GenerateMigration diffs the registered entities against prev (the last snapshot) and returns the forward (up) and inverse (down) DDL for the delta, plus the new snapshot to persist. up is empty when there is nothing to do.

Covered changes: create table, add column, drop column, and drop table (when an entity is removed). Type changes are out of scope — same limitation as DiffSchema.

func GeneratePlan

func GeneratePlan(plan Plan, prev SchemaSnapshot, dialect Dialect) (up, down string, next SchemaSnapshot, err error)

GeneratePlan is GenerateMigration for a full Plan — it diffs both the tables (entities + raw Tables) and the routines against the snapshot, emitting one reversible migration that covers everything. Routine bodies are compared verbatim; a changed routine's Down restores the previous body, and a removed routine is dropped (with its recreation as the Down).

func LoadSnapshot

func LoadSnapshot(path string) (SchemaSnapshot, error)

LoadSnapshot reads a snapshot JSON file. A missing file is not an error — it returns an empty snapshot so the first generation emits a full create.

func SnapshotFromPlan

func SnapshotFromPlan(plan Plan, dialect Dialect) SchemaSnapshot

SnapshotFromPlan builds the desired-state snapshot from a full Plan (tables plus routines).

func SnapshotFromRegistry

func SnapshotFromRegistry(reg entity.Registry, dialect Dialect) SchemaSnapshot

SnapshotFromRegistry builds the desired-state snapshot from the registered entities for the given dialect — the schema the entities describe right now.

type Table

type Table struct {
	Name        string   // table name; also the registry key for FK references
	Columns     []Column // exactly the columns emitted — no auto-injection
	Indices     []Index
	ForeignKeys []ForeignKey
}

Non-entity table path

Some tables don't want the entity machinery — no auto-CRUD, no HTTP routes, no validation, no auto-injected id/timestamps/tenant columns. Join tables, analytics roll-ups, tables owned by stored procedures, or just a user who is stubborn about declaring entities. Table is a raw schema declaration for exactly those. It carries ONLY the columns you write — nothing is injected.

A Table reconciles with entities because ToEntity adapts it into the same *entity.Entity the migration engine already consumes: register both in one registry and AutoMigrate / DiffSchema / GenerateMigration treat them uniformly, including foreign keys that cross between an entity and a Table.

func (Table) ToEntity

func (t Table) ToEntity() *entity.Entity

ToEntity adapts the raw Table into an *entity.Entity with no auto-injection, suitable for registering alongside real entities. Mark exactly one column PrimaryKey for FK targets; a table with no primary key is allowed but cannot be referenced by a foreign key.

type View

type View struct {
	Name         string   // view name; also the registry key / table name for queries
	Select       string   // the SELECT body: "SELECT u.id, u.name FROM users u WHERE u.active"
	DependsOn    []string // source table/view names — for create-after ordering
	Columns      []Column // output columns, for the read-only ORM entity + OpenAPI
	Materialized bool     // Postgres MATERIALIZED VIEW (a plain VIEW otherwise; ignored on SQLite)
}

View is a virtual table built from other entities — a read model defined by a SELECT over entity tables. It belongs to both the migration story (created after its source tables, reversible, checksum-tracked in generate) and the ORM story (register it with App.View to query it read-only). A View is the "virtual table that takes in other entities to be constructed".

func (View) ToEntity

func (v View) ToEntity() *entity.Entity

ToEntity builds the read-only ORM entity for a view from its declared Columns. The entity is Unmanaged (the migration system never emits table DDL for it — the view DDL handles its existence), so registering it only adds the ability to query the view through the ORM. Returns nil when no columns are declared (the view is then migration-only — query it with raw SQL).

Jump to

Keyboard shortcuts

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