migrate

package
v0.1.40 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

README

HCL migrations

migrate.Apply is the single filesystem-backed migration seam for PostgreSQL and SQLite. The connection string selects the target, while callers pass the same embedded Atlas HCL bundle and options.

err := migrate.Apply(ctx, dsn, schemaFS,
    migrate.WithDir("migrations"),
    migrate.WithName("runtime"),
)

Target selection

PostgreSQL accepts postgres://, postgresql://, and keyword connection strings such as host=localhost dbname=runtime sslmode=disable.

SQLite accepts sqlite:// URLs and plain file paths ending in .db:

err := migrate.Apply(ctx, "sqlite://state/runtime.sqlite", schemaFS, migrate.WithDir("migrations"))
err := migrate.Apply(ctx, "/var/lib/example/runtime.db", schemaFS, migrate.WithDir("migrations"))

Relative SQLite paths are resolved from the process working directory. SQLite is always file-backed; :memory: and mode=memory are rejected. The normalized connection enables foreign keys, a 5-second busy timeout, and WAL while preserving caller-supplied query parameters and unmanaged pragmas.

Unknown URI schemes written as scheme://... fail instead of being passed to PostgreSQL accidentally. Scheme-like strings without :// are treated as PostgreSQL DSNs unless their path ends in .db; use one of the explicit forms above rather than relying on that ambiguity. A PostgreSQL keyword DSN remains PostgreSQL even when its dbname ends in .db.

db.NewGorm uses the same DSN resolver, so migration and application connections cannot disagree about the target.

Portable canonical HCL

A cross-database bundle declares PostgreSQL-shaped Atlas HCL. PostgreSQL evaluates the declaration directly. SQLite evaluates the same declaration with the PostgreSQL evaluator, validates the resulting realm against the compatibility contract below, and then projects it to SQLite. Acceptance by the HCL parser alone does not mean an object is SQLite-compatible.

All .hcl files below WithDir are loaded recursively in lexical path order and evaluated as one realm. variable blocks are supported on both targets when callers provide values through WithVariables; the resulting schema must still fit the portable set.

Supported cross-database blocks
HCL block Supported portable form SQLite behavior
variable Atlas variable declarations referenced by schema expressions Evaluated before projection using WithVariables values
schema Exactly one schema "public" {} Projected to SQLite's main schema
table One or more tables assigned to schema.public Created with their declared portable columns and constraints
column type, null, and a literal or sql(...) default Type is mapped as documented below; default SQL is preserved verbatim
primary_key Column references only Preserved when the table is first created or safely rebuilt
unique Named constraint over column references Projected to a named unique index
index Column references, optional unique, and optional where; default or explicit B-tree method only Projected to a SQLite index; where is preserved verbatim
foreign_key Local and referenced columns plus on_update and on_delete actions Preserved when the table is first created or safely rebuilt
check Named check with expr and no dialect attributes Preserved verbatim when the table is first created or safely rebuilt

Index expressions, per-column index attributes such as sort direction or operator classes, foreign-key attributes, and table or check attributes outside the forms above are not in the portable set.

Supported portable column types

The following table lists the PostgreSQL type families accepted after HCL evaluation, not every type accepted by Atlas's PostgreSQL parser. Underscored names such as character_varying and double_precision are direct HCL aliases. Space-containing SQL spellings and the less common aliases noted below are accepted through sql("...") and normalize to the same family.

PostgreSQL HCL type SQLite declaration Compatibility notes
uuid text No SQLite UUID-format constraint is added
text, char, character, varchar, character_varying, bpchar, name; SQL spelling character varying text Length modifiers and PostgreSQL's internal name limit are not preserved
json, jsonb text plus json_valid(column) The generated check is named <table>_<column>_json
date, time, timetz, timestamp, timestamptz; SQL spellings time with time zone, time without time zone, timestamp with time zone, timestamp without time zone datetime Precision and timezone enforcement are not preserved by SQLite
int2, int4, int8, int, integer, smallint, bigint, xid, xid8 integer PostgreSQL width and transaction-ID distinctions collapse to SQLite integer affinity
bool, boolean bool SQLite gives the declared bool type NUMERIC affinity; it has no native boolean type
bytea blob Binary values remain binary
numeric, decimal numeric Precision and scale modifiers are not preserved
real, float, float4, float8, double_precision real Precision modifiers and PostgreSQL width distinctions are not preserved

Use only type modifiers whose loss is acceptable to the application, or enforce the invariant with a portable named check. Evaluated type families outside this table fail projection; they are not coerced to text.

Portable example
schema "public" {}

table "projects" {
  schema = schema.public

  column "id" {
    type = uuid
    null = false
  }
  column "payload" {
    type    = jsonb
    null    = false
    default = sql("'{}'")
  }
  column "created_at" {
    type    = timestamptz
    null    = false
    default = sql("CURRENT_TIMESTAMP")
  }

  primary_key {
    columns = [column.id]
  }
}

table "events" {
  schema = schema.public

  column "id" {
    type = uuid
    null = false
  }
  column "project_id" {
    type = uuid
    null = false
  }
  column "event_key" {
    type = text
    null = false
  }
  column "active" {
    type    = bool
    null    = false
    default = false
  }

  primary_key {
    columns = [column.id]
  }
  foreign_key "events_project_id_fkey" {
    columns     = [column.project_id]
    ref_columns = [table.projects.column.id]
    on_update   = NO_ACTION
    on_delete   = CASCADE
  }
  index "events_active_key" {
    unique  = true
    columns = [column.project_id, column.event_key]
    where   = "active"
  }
  check "events_event_key_nonempty" {
    expr = "length(event_key) > 0"
  }
}

Defaults, checks, and partial-index predicates are SQL strings, not a portable expression language. commons-db does not translate them. Every expression in a cross-database bundle must be accepted with the intended meaning by both PostgreSQL and SQLite. CURRENT_TIMESTAMP and sql("'{}'") are portable examples; PostgreSQL-only casts such as ::jsonb, functions such as now(), operators, and JSON expressions must not be assumed portable.

SQLite rejection list

SQLite rejects the following before schema mutation:

Incompatible declaration or option Reason
No table declarations The SQLite reconciler requires at least one declared table
More than one schema, or a schema other than public A portable bundle maps exactly one canonical schema to SQLite main
Realm or schema attributes and objects, including enums, domains, extensions, sequences, views, functions, and procedures There is no defined lossless projection
Table triggers Trigger SQL is dialect-specific
Column types outside the supported type table The projector never guesses an affinity
Column attributes such as generated or identity behavior PostgreSQL attributes cannot be preserved by the current projection
Table attributes other than plain named checks Storage parameters, partitioning, and other PostgreSQL metadata are not portable
Check attributes Only the check name and expression are portable
Non-B-tree index methods SQLite cannot preserve PostgreSQL access-method behavior
Expression indexes or index-part attributes Only direct column references are portable
Foreign-key attributes beyond columns, references, and actions PostgreSQL-only metadata would be lost
A user check named <table>_<json-column>_json with a different expression That name is reserved for the generated json_valid check
Any .sql migration file SQL phases are PostgreSQL-only; SQLite does not skip them silently
role or permission blocks Managed security is PostgreSQL-only
WithExclude(...) Partial inspection could hide SQLite drift
WithSchema(...) with a value other than public SQLite has no PostgreSQL schema namespace
WithDrops() The SQLite path is intentionally additive-only

These declarations are parser-valid PostgreSQL HCL but are not SQLite-compatible: bit strings, network and MAC types, spatial types, intervals, serial and identity types, XML, money, arrays, ranges and multiranges, full-text search types, enums, domains, and other user-defined types.

Accepted declarations with different SQLite semantics

Some declarations are accepted because their storage representation is useful on both targets, but SQLite cannot enforce every PostgreSQL semantic:

  • UUIDs are stored as text without a canonical-format check.
  • String length, numeric precision and scale, floating-point width, and time precision are normalized to SQLite affinities.
  • json and jsonb both become validated JSON text; PostgreSQL JSONB indexing and operators are not provided.
  • Date and time families become datetime; SQLite does not enforce PostgreSQL timezone or temporal-type distinctions.
  • Integer widths collapse to SQLite integer affinity.
  • Boolean declarations have NUMERIC affinity and do not restrict stored values to 0 or 1; add a portable check when that invariant matters.
  • Defaults, checks, foreign-key actions, and partial-index predicates are carried as written. Syntax or semantics that differ between engines fail at apply time or behave according to the target engine.
SQLite migration-time limits

The SQLite apply runs in one transaction and is additive by default. A newly created table may contain every supported constraint above. For an existing table, only missing columns and indexes are applied unless the caller opts in with WithRebuilds().

WithRebuilds() allows Atlas to copy existing rows through a transactional SQLite table rebuild for added primary keys, foreign keys, and checks, plus column default and nullability changes. Added indexes are also supported. It disables foreign-key enforcement before opening the transaction, verifies that no new foreign-key violations appear before commit, and restores enforcement afterward. An incompatible value or constraint rolls the transaction back. Drops, renames, index modifications, and column type changes remain refused; in particular, an existing unique index cannot be removed by opting into rebuilds. A table with an unmanaged trigger is refused rather than silently losing that trigger during rebuild. PostgreSQL accepts the option but already applies these schema changes through its ordinary migration path.

Without WithRebuilds(), adding a check or foreign key to an existing table is unsupported, and adding a JSON column may be refused because its generated json_valid check is a table constraint. An otherwise portable added column must also satisfy SQLite's native ALTER TABLE ADD COLUMN restrictions; for example, a required column generally needs a usable default when rows already exist. WithRebuilds() does not transform or backfill values. Tables and views outside the bundle are not managed; callers must assess views that depend on changed columns before applying a rebuild.

PostgreSQL schema-scoped bundles

WithSchema applies one bundle inside a normalized PostgreSQL schema and leaves the default behavior on public unchanged:

err := migrate.Apply(ctx, dsn, schemaFS,
    migrate.WithName("runtime"),
    migrate.WithSchema("agent_tenant_context"),
)

The schema name must be a lowercase PostgreSQL identifier no longer than 63 bytes. The migration creates the schema and scopes Atlas inspection, HCL objects, SQL migration metadata, dependent-view handling, and managed security to it. A reusable HCL bundle still declares only schema "public" {}; migration remaps that declaration to the selected schema.

SQL files run with the selected schema as search_path. Keep objects owned by the bundle unqualified so the same bundle can target different schemas. Explicitly qualified SQL remains explicit and is not rewritten.

PostgreSQL SQL phases and security

SQL files default to the post-Atlas phase. Directives are read from the leading comment header:

-- phase: pre
-- dependsOn: 001_extensions.sql
-- runs: always
-- transaction: false
  • phase is pre or post; omitted means post.
  • dependsOn uses paths relative to the migration root. Dependencies are topologically ordered and changes rerun all transitive dependents.
  • Scripts are transactional by default; the SQL and its SHA-256 migration log commit atomically.
  • runs: always re-runs a script on every apply. It is an escape hatch for reconciliation that truly must occur at every boot, not a replacement for dependency tracking.

Views are restored automatically when Atlas reshapes a base table they depend on. Prefer small hash-gated files, such as one file per view or shared dependency, so a table reshape re-creates only affected objects and steady-state startup remains DDL-free.

Atlas-style PostgreSQL role and permission blocks may live in the same HCL files as tables. The OSS migration layer supports roles and memberships plus schema, table, column, and sequence permissions. Managed grants are reconciled exactly within the bundle's metadata scope; unrelated roles and grants remain untouched. Different scopes must not manage the same role or grantee/object pair.

Reusable PostgreSQL test templates

migrate.NewProvisioner adapts an HCL and SQL bundle to dbtest.Options.Provisioner:

handle := dbtest.ForT(t, dbtest.Options{
    Name:    "query_sessions",
    LogName: "query-sessions-test",
    Provisioner: migrate.NewProvisioner(
        querySchema,
        migrate.WithDir("migrations"),
        migrate.WithName("query"),
    ),
})

The provisioner fingerprints the migration name, directory, Atlas options, variables, and every HCL and SQL file. dbtest prepares one sealed PostgreSQL template for that fingerprint, clones an isolated database for each call, and keeps the template for later tests. Normal hash-gated SQL is already present in the clone; runs: always scripts and security reconciliation still run for every instance.

This option requires database creation. dbtest fails before invoking the provisioner when COMMONS_DB_CREATE=false because that mode targets the configured database directly.

Programmatic SQLite declarations

The migrate/sqlite subpackage remains the lower-level seam for callers that already own a *sql.DB or *sql.Tx and construct Atlas *schema.Table declarations in Go. Its ReconcileTables function uses the same additive-only policy but does not load or project HCL.

declared, err := sqlitetable.Table{Name: "events", Columns: columns, StoredAs: stored, PrimaryKey: []string{"id"}}.Declare()
if err != nil {
    return err
}
if err := sqlitemigrate.ReconcileTables(ctx, tx, sqlitemigrate.ReconcileOptions{}, declared); err != nil {
    return err
}

The record store uses this lower-level API to add newly declared columns to tables that may already contain rows. New filesystem-backed migrations should call the target-aware parent migrate.Apply instead.

Documentation

Overview

Package migrate applies declarative Atlas HCL schemas to PostgreSQL and SQLite databases.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Apply

func Apply(ctx context.Context, connection string, schemaFS fs.FS, opts ...Option) error

Apply resolves the database target from connection and reconciles HCL schemas. PostgreSQL supports the complete HCL, SQL, security, and destructive-change lifecycle. SQLite projects portable PostgreSQL-shaped HCL to tables, columns, and indexes, with compatible rebuilds available by opt-in.

func ConnectionForSchema added in v0.1.28

func ConnectionForSchema(connection, schemaName string) (string, error)

ConnectionForSchema returns a PostgreSQL URL whose connections select schemaName.

func ValidateSchemaName added in v0.1.28

func ValidateSchemaName(name string) error

ValidateSchemaName validates an application-normalized PostgreSQL schema identifier.

Types

type Option

type Option func(*options)

Option configures an HCL migration.

func WithDir

func WithDir(dir string) Option

WithDir selects the root containing colocated HCL and SQL migrations.

func WithDrops added in v0.1.31

func WithDrops() Option

WithDrops allows HCL files to remove tables and types. Drops are suppressed by default so a partial schema bundle cannot delete objects owned by consumers.

Tables and types share one switch because suppressing only tables is worse than suppressing neither: the surviving table keeps a column typed by the enum the diff still wants to drop, and PostgreSQL rejects the DROP TYPE.

func WithExclude

func WithExclude(patterns ...string) Option

WithExclude excludes database objects from Atlas inspection. Values use Atlas's schema inspection patterns (for example "table.column").

func WithName

func WithName(name string) Option

WithName sets the metadata scope used for SQL hashes and managed security. It should be stable and unique for each migration bundle sharing a database.

func WithRebuilds added in v0.1.39

func WithRebuilds() Option

WithRebuilds permits SQLite to rebuild populated tables for compatible constraint, default, nullability, and index changes. It never permits drops or type changes. PostgreSQL already applies these changes directly.

func WithSchema added in v0.1.28

func WithSchema(name string) Option

WithSchema selects the PostgreSQL schema used for the complete migration lifecycle. SQLite accepts only the default public schema declared by portable HCL bundles.

func WithVariables

func WithVariables(input map[string]cty.Value) Option

WithVariables supplies values for HCL variable blocks and security expressions.

type SchemaProvisioner added in v0.1.26

type SchemaProvisioner struct {
	// contains filtered or unexported fields
}

SchemaProvisioner prepares reusable dbtest templates and reconciles each cloned database using the same migration behavior as Apply.

func NewProvisioner added in v0.1.26

func NewProvisioner(schemaFS fs.FS, opts ...Option) *SchemaProvisioner

NewProvisioner adapts an HCL migration bundle to dbtest.Provisioner without coupling the migrate package to dbtest. Options are frozen at construction.

func (*SchemaProvisioner) Fingerprint added in v0.1.26

func (p *SchemaProvisioner) Fingerprint(ctx context.Context) (string, error)

Fingerprint returns a deterministic identity for all inputs that affect the database produced by this provisioner.

func (*SchemaProvisioner) PrepareInstance added in v0.1.26

func (p *SchemaProvisioner) PrepareInstance(ctx context.Context, connection string) error

PrepareInstance reconciles instance-specific and always-run migrations after cloning a prepared template.

func (*SchemaProvisioner) PrepareTemplate added in v0.1.26

func (p *SchemaProvisioner) PrepareTemplate(ctx context.Context, connection string) error

PrepareTemplate applies the full migration bundle to a new template.

Directories

Path Synopsis
Package sqlite reconciles SQLite tables with programmatic Atlas declarations.
Package sqlite reconciles SQLite tables with programmatic Atlas declarations.
Package viewdeps finds, drops, and restores the views and materialized views that transitively depend on a set of tables, so DDL PostgreSQL refuses while a dependent view exists — DROP TABLE, DROP COLUMN, ALTER COLUMN TYPE — can proceed.
Package viewdeps finds, drops, and restores the views and materialized views that transitively depend on a set of tables, so DDL PostgreSQL refuses while a dependent view exists — DROP TABLE, DROP COLUMN, ALTER COLUMN TYPE — can proceed.

Jump to

Keyboard shortcuts

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