Migration Format Stability
PormG's migration history is persisted in two places that a user commits to version control and applies to a production database: the on-disk migration files and the pormg_migrations tracking table. Once those exist in a real database, they cannot be rewritten by a package upgrade — so their layout is a stability contract, frozen at format version 1.
This page documents that contract: the on-disk file format, the checksum algorithm, the tracking-table schema, and the version marker that lets future format revisions migrate forward.
The moment you run migrate() the version, checksum, and recorded SQL are written into your database, and the generated .jl files are committed to your repository. Changing the checksum scheme or the tracking-table columns afterward would invalidate every already-applied record. The format is therefore versioned (see Format version) so any future change ships an explicit forward migration instead of a silent break.
Format version
Every artifact PormG writes is stamped with a single integer format version, sourced from the constant PormG.Migrations.MIGRATION_FORMAT_VERSION (currently 1):
On disk — each generated migration file carries a comment header on the line directly below
module …:module 2026-06-22_18-57-26_migration # pormg-migration-format: 1It is a comment, not a
const, deliberately: migration files are re-included across runs, and a constant would emitWarning: redefining constantand conflict once files of different format versions coexist. The header is read by line-scan with the regex^# pormg-migration-format: (\d+)\r?$before the module is ever executed — so a future engine detects the format and decides how to parse a file before trusting it. The optional\r?keeps the scan line-ending agnostic (a file checked out with CRLF on Windows still matches).In the database — the
pormg_migrations.format_versioncolumn records the format of each applied record. This column is the authoritative version source (the runtime history table is the source of truth; the file comment is a secondary on-disk annotation).
A future format change bumps MIGRATION_FORMAT_VERSION and provides a documented forward migration; existing format_version = 1 rows and files remain valid and are read under the v1 rules.
On-disk migration files
Location and naming
| Stage | Path | Naming |
|---|---|---|
| Pending (un-applied draft) | <db_folder>/migrations/pending_migrations.jl | fixed filename |
| Applied (archived) | <db_folder>/migrations/applied_migrations/ | YYYY-MM-DD_HH-MM-SS_migration.jl |
<db_folder> is the database's configuration folder (the directory holding its connection.yml). When an applied filename would collide, a random 4-digit suffix is inserted (YYYY-MM-DD_HH-MM-SS_NNNN_migration.jl). Applying a plan also snapshots the models file alongside it as YYYY-MM-DD_HH-MM-SS_old_models.jl.
Content structure
A migration file is a Julia module. Each table touched by the plan is one variable bound to an OrderedDict{String, String} mapping a human-readable operation description to the SQL that performs it. Order is preserved, so the dict reads as an executable changelog:
module 2026-06-22_18-57-26_migration
# pormg-migration-format: 1
import PormG.Migrations
import OrderedCollections: OrderedDict
# table: results
results = OrderedDict{String, String}(
"New model" =>
"""CREATE TABLE IF NOT EXISTS results (
"resultid" BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"driverid" BIGINT NOT NULL,
"points" DOUBLE PRECISION NOT NULL
);""",
"Create index on driverid" =>
"""CREATE INDEX IF NOT EXISTS "results_driverid_a1b2c3d4_idx" ON results ("driverid");""")
endThe outer structure is OrderedDict{Symbol, OrderedDict{String, String}} — one entry per table, keyed by table name. Operation descriptions are stable strings such as "New model", "Add field: <name>", "Rename field: <name>", "Remove field: <name>", "Create index on <name>", and "Drop table".
Files under applied_migrations/ are a historical record. Do not hand-edit them — their SQL is already reflected in the database and in the checksum recorded in pormg_migrations.
Checksum
Each applied migration stores a SHA-256 integrity checksum (lower-case hex, 64 chars):
compute_checksum(sql_content) == bytes2hex(sha256(Vector{UInt8}(sql_content)))The input is the migration's full SQL content — every statement in the plan concatenated in execution order. mark_applied() therefore requires either the migration's sql_content (from which this checksum is computed) or an explicit checksum; it will not fabricate one, so that every recorded digest stays verifiable against real SQL. The legacy manual fallback sha256("manual:" * version * ":" * name) remains a frozen v1 primitive for reproducing digests of records created that way before the requirement was introduced. Both schemes are frozen at format version 1: re-hashing a committed v1 migration must reproduce its stored checksum exactly.
Tracking table: pormg_migrations
The history table name is fixed (pormg_migrations, not configurable) and is created automatically by migrate() or init_migrations(). Its frozen v1 columns:
| Column | PostgreSQL type | SQLite type | Notes |
|---|---|---|---|
id | SERIAL PRIMARY KEY | INTEGER PRIMARY KEY AUTOINCREMENT | surrogate key |
version | VARCHAR(17) NOT NULL UNIQUE | same | timestamp id, yyyymmddHHMMSSsss (17 chars) |
name | VARCHAR(255) NOT NULL | same | human-readable migration name |
checksum | VARCHAR(64) NOT NULL | same | SHA-256 hex |
sql_content | TEXT NOT NULL DEFAULT '' | same | full SQL applied |
applied_at | TIMESTAMP NOT NULL DEFAULT NOW() | DATETIME … DEFAULT (datetime('now')) | apply time |
status | VARCHAR(20) NOT NULL DEFAULT 'applied' | same | applied or failed |
is_destructive | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN … DEFAULT 0 | contained a DROP/destructive op |
format_version | INTEGER NOT NULL DEFAULT 1 | same | migration-format contract version |
The version string is generated as Dates.format(now(), "yyyymmddHHMMSSsss") — date, time, and milliseconds, giving collision-resistant ordering for rapid successive migrations.
Upgrading an existing database
format_version was added in this release. A database initialized by an earlier PormG version has a pormg_migrations table without the column. init_migrations repairs that idempotently: on PostgreSQL via ALTER TABLE … ADD COLUMN IF NOT EXISTS, and on SQLite by probing PRAGMA table_info(pormg_migrations) and adding the column only when absent. Pre-existing rows backfill to 1 through the column DEFAULT — they were genuinely written under the v1 contract.
What "frozen" guarantees
Within format version 1, PormG promises:
- A committed v1 migration file remains parseable and applicable.
- Re-running
compute_checksumover a v1 migration's SQL reproduces its stored checksum. - The
pormg_migrationscolumn set above is stable; new columns (if ever needed) arrive only with aformat_versionbump and a forward migration. - The version-string format stays
yyyymmddHHMMSSsss.
The committed fixture in test/fixtures/migration_format_v1/, exercised by test/unit/test_migration_format_v1.jl, regression-guards points 1–2.