Migration Safety¶
Aksara applies file-based migrations through a single, canonical execution path
designed to be safe to run repeatedly, safe to run from more than one place,
and safe to run against a database that already has data. This page explains
what that file-based path does and why, so you can reason about what happens
when you run aksara migrate with migration files present.
If you are looking for how to write migrations — operations, dependencies, data migrations — see Migrations. This page is about how migrations are applied and verified.
Canonical executor path¶
File-based migration runs go through the same executor in
aksara.migrations.executor:
aksara migrateapplies file-based migrations through it when migration files exist.- The testing helpers (for example, applying migrations in a test database) use the same executor.
Because file-based migrations share one path, local, CI, and production file-based runs get the same guarantees: transactions, the advisory lock, SQL statement splitting, checksum recording, and checksum verification.
The legacy model-based fallback path is still available when aksara migrate
finds no migration files. That path is intended for initial/simple bootstrap
scenarios and does not provide the full file-based migration integrity model.
In particular, it does not make the same checksum-verification claims as the
file-based executor.
The aksara migrate --dry-run path does not execute or record pending application
migrations. It still imports their modules, connects to PostgreSQL and can
initialize migration-tracking metadata. It does not run the full executor or
prove that SQL, integrity checks, or bundled internal migrations will succeed.
Transactional migration application¶
A Python migration's operations and the row that records the migration as applied run inside a single transaction.
- If every operation succeeds, the migration is recorded as applied and the transaction commits.
- If any operation fails, the entire migration rolls back. Partial operations are not left behind, and the migration is not recorded as applied.
This covers supported PostgreSQL changes inside that transaction. Earlier
migrations in the run remain committed. External side effects and PostgreSQL
operations disallowed inside a transaction are outside this guarantee; for
example, CREATE INDEX CONCURRENTLY cannot be put in this transaction.
Investigate the failure before rerunning.
Advisory lock¶
Before inspecting which migrations are applied and running the pending ones, the executor acquires a PostgreSQL advisory lock.
This prevents two processes — two deploy steps, a deploy racing a developer, two CI jobs — from running migrations against the same database at the same time. The second runner does not proceed concurrently; it is told the lock is held. The lock is always released when the run finishes, including when a run fails.
SQL migration statement handling¶
SQL migrations are split into individual statements and executed one at a time, inside the same transaction as the recording step.
This makes statement handling explicit in the migration executor while keeping the operations in one transaction. It is an executor contract, not a general claim that asyncpg supports only one SQL statement per call.
Checksums and applied migration integrity¶
When a migration is applied, Aksara stores a checksum of the migration (both Python and SQL migrations store one). Before running any pending migrations, the executor verifies recorded checksums for already-applied files still present on disk. Applied entries whose files are missing produce warnings rather than blocking the run; keep historical files in version control.
If an already-applied migration file has changed since it was applied, the checksums no longer match and the run fails with the migration name and clear next steps. This catches the case where an applied migration was edited in place — which would otherwise drift your code and your database apart silently.
The rule this enforces is the same one in Migrations: do not edit a migration that has already been applied. Create a new migration instead.
Handling historical NULL checksums¶
Migrations that were applied before checksums were recorded have no stored checksum. These rows are still valid — they are not rejected. The executor reports them with a warning so you know which historical migrations cannot be integrity-checked, and continues.
There is intentionally no automatic backfill of these checksums (see Deferred metadata work).
Failure reporting¶
Two behaviors make failures easier to understand:
- Strict graph loading by default. If a migration file cannot be loaded (import error, bad definition), the run fails with a clear error naming the migration, its path, and the underlying cause — instead of silently skipping it and applying an incomplete set. A non-strict mode remains available for tooling that needs to inspect a partially broken graph.
- Skipped pending migrations are reported. When a migration fails, the pending migrations that were not attempted are reported, and the CLI shows you which migrations were skipped, so you know exactly where the run stopped.
Adding non-null fields without defaults¶
Adding a nullable=False column with no default fails on a table that already
contains rows — PostgreSQL cannot fill the existing rows.
When a generated migration contains such a field, Aksara emits a warning comment on it. The comment points you to a safe rollout:
- Add the column as nullable, or with a temporary default.
- Backfill the existing rows.
- Then enforce non-null in a follow-up migration.
Primary-key fields are exempt from the warning.
SQL-generation guardrails¶
The DDL Aksara generates is hardened against malformed and unsafe identifiers:
- Many-to-many constraints and columns. Join-table constraint names are deterministic, double-quoted, and bounded to PostgreSQL's identifier length limit; source and target column references are quoted.
- Partial-index predicates. An index
wherepredicate is validated when it is defined. Obvious unsafe patterns — statement terminators, comments, and DDL/DML keywords — are rejected. Ordinary predicates such asarchived_at IS NULLare unaffected. - Array field types. An array field's SQL type is validated against an
allowlist of PostgreSQL base types and normalised to a canonical form. For
example,
ArrayField(sql_type="text[]")normalises to"TEXT[]". A base type that is not on the allowlist raises an error when the field is defined; open an issue to have additional types added.
Deferred metadata work¶
Some migration-metadata features are intentionally not implemented yet. They are called out here so you do not assume behavior that does not exist:
- Migration metadata schema versioning — there is no
schema_versioncolumn on the tracking table. - App-label / name identity split — there is no
app_labelcolumn and noUNIQUE(app_label, name)redesign of the tracking table. - Automatic checksum backfill — historical NULL-checksum rows are not backfilled automatically. Doing so would bless already-edited files with their current checksum and defeat tamper detection.
- A migration verify/backfill command — there is no dedicated command for this yet.
The internal aksara_migrations tracking table layout may change when this work
lands. See the v0.6 Stability Contract
for what is and is not a stable contract.
Related Documentation¶
- Migrations — writing migrations, operations, dependencies
- CLI Reference —
aksara migrateandaksara status - v0.6 Stability Contract — what is stable