Skip to main content
Headgate ships the same embedded migration line in two independently usable packages:
  • Rust: headgate-migrate, including the hg-migrate binary.
  • Go: github.com/mujhtech/headgate/go/headgatemigrate, including the go/headgatemigrate/cmd/hg-migrate command.
Both packages support PostgreSQL and MySQL. Redis has no DDL schema and is deliberately not presented as a migration backend.

Migration contract

headgate_schema_migration records a migration line, monotonically increasing version, name, immutable SHA-256 checksum, and store-clock application time. Validation fails when:
  • an applied version is missing, out of order, or newer than the binary;
  • a historical migration’s name or bytes changed;
  • the database is behind the embedded latest version; or
  • a required table, column, index, trigger, state value, or saturation value is absent.
PostgreSQL applies each version and its history row in one transaction while holding an exclusive lock on the history table. MySQL DDL commits implicitly, so its runner acquires a connection-scoped named lock, executes idempotent statements, validates the resulting schema, and only then records the version.
Do not apply the migration files independently or edit packaged copies by hand. scripts/check-migrations.py requires the driver, Rust, and Go assets to remain byte-identical and their version sets to remain contiguous.

CLI

HG_DATABASE_URL and DATABASE_URL are fallback environment variables. The backend is inferred from postgres://, postgresql://, or mysql://; libpq keyword connection strings and native MySQL DSNs require --backend.

Lock namespaces and schemas

PostgreSQL locks the installation’s qualified migration-history table and does not consume an application advisory-lock number. For an isolated installation, create its schema and pass the same name to every operation:
The schema must already exist. Headgate quotes every relation explicitly instead of trusting search_path, and rejects names beyond PostgreSQL’s identifier limit. MySQL uses GET_LOCK in a server-wide namespace. Its default is headgate:migrate:<database>. Choose one stable namespace per installation if an application or another Headgate installation could use that prefix:
Every concurrent migrator for one installation must use the same namespace. Deliberately different namespaces are different locks and would bypass serialization against the same database.

Adopt an existing installation

The migrator refuses to apply version 1 over existing headgate_* tables. Treating an unknown, hand-installed schema as fresh could bless a partial installation.
1

Validate the existing schema

2

Adopt only a complete manifest

3

Validate the recorded installation

adopt writes history only when the complete current manifest passes. It does not repair or infer missing DDL.

Online-safety ledger

Additive does not automatically mean online-safe. Table rewrites, blocking index builds, and non-null columns without a safe backfill remain offline changes.

Library APIs

Rust exposes migrate_postgres, migrate_mysql, validate_*, applied_*, adopt_*, and the pure plan function. Go exposes the corresponding MigratePostgres, MigrateMySQL, Validate*, Applied*, Adopt*, and Plan functions. Both languages also provide explicit-schema PostgreSQL and named-lock MySQL variants.

Multiple installations

Keep migrations, stores, duties, schedules, workers, and wakeups inside one backend-native boundary.