Migrations
Entities describe the schema you want. A migration records each step from
one version of the database to the next. jadpo migrate new add_priority
compares the entities with the previous migration's snapshot and writes
the next folder:
database/migrations/20261020T1400_add_priority/ snapshot.json generated: the whole schema at this point postgres.sql generated: the difference from the previous snapshot changes.jadpo only when a decision is needed
When a change needs a decision, jadpo migrate new writes changes.jadpo
with a line to fill in for each one, such as Todo.priority: {}. jadpo check names what is missing until it is written, and running jadpo migrate new again finishes the migration, writing the decisions into its
SQL. Here a required priority is added, so existing rows need a value,
and the legacy flag is retired:
database/migrations/20261020T1401_add_priority/changes.jadpo:
// changes.jadpo/// Adds a priority to every todo and retires the legacy flag.migration { Todo.priority: { backfill: Priority.normal // existing rows need a value } Todo.legacy_flag: FieldChange.retired}
- Forgetting is caught at once.
jadpo checkfails while the entities and the latest snapshot disagree, naming what changed and the command to run, so the editor, the build and CI all stop. Loose files, with noapp.jadpo, have no migrations, so the check is a project's. - Decisions only where needed: a value for existing rows of a required field, or retiring a field.
- Removing a field takes two migrations.
FieldChange.retiredcomes first: code stops using the field, and the column stays, now nullable, for servers still running the old build. Later,FieldChange.droppedremoves it, and needs an explicit acceptance, a person's recorded approval of a breaking change (Acceptances). - Renames keep their data. The identity registry, a file Jadpo keeps
under
database/, records what each renamed thing was born as (Records and manifest). Tables and columns keep the names they were born with, so renaming a field keeps its column. A field renamed by hand, without the registry, reads as one removed and one added, and the removed one's decision says so. - Backfills can be worked out from each row's own fields, for a new field that reshapes data the entity already holds (Computed backfills).
jadpo migrate applyruns pending migrations as a deploy step, each in a transaction, one per store, and records each in the store'sjadpo_migrationstable. It refuses a database that applied a migration the project doesn't have, or one that changed since.jadpo migrate statuslists the migrations and which a database has applied.- A native deployable checks its database. A native deployable built
by
jadpo buildrefuses to start against a database that hasn't applied every migration it was built with, or that applied one it doesn't have or that changed since, naming the store and the migrations (A native deployable). - The test database.
jadpo testgives each action test its own copy of a template built from the migrations, on the serverJADPO_TEST_DATABASE_URLnames or a disposable one, with copies prepared in the background.--keep-failedkeeps a failed test's copy.
A migration's postgres.sql holds a section per store, each opened by a
-- store: <name> line. It names every table, column, constraint and
index as the snapshot does: a table after its entity's born name in
lower_snake_case (Todo is todo), a column after its field, and keys
and indexes <table>_<name>_key, _fkey and _idx, cut short with a
digest past PostgreSQL's 63 bytes. Each store that holds an entity also
gets the runtime's system tables, prefixed jadpo_.
Not built yet: jadpo rename, which writes the identity registry
(Tooling); FieldChange.dropped and its acceptance;
removing an entity, moving one between stores, and changing a column's
type other than widening it. jadpo migrate new says so for each.
Markdown for agents: /docs/migrations.md.Built from Jadpo main at a707efb.