Migrations¶
rhizodat.migrations.format ¶
The language-neutral migration format: one TOML file per migration.
NNNN_name.toml (the number is the migration's id, at least 1; the name is
lower-case letters, digits and _) holds an optional description and a
list of steps. A step names the store it runs on (a store name of the
Rhizodat configuration) and has exactly one body, keyed by its kind::
description = "bugs and their graph index"
[[steps]]
store = "pg"
sql = "CREATE TABLE IF NOT EXISTS bug (id uuid PRIMARY KEY, title text NOT NULL)"
[[steps]]
store = "graph"
cypher = ["CREATE INDEX bug_title IF NOT EXISTS FOR (b:Bug) ON (b.title)"]
The kind (sql, cypher, iceberg, or a plug-in adapter's own) decides
which store can run the step; the format only checks its shape. A body is
strings, integers, booleans, arrays and tables: floats and dates have no single
text form across languages, so they are refused.
The checksum is the sha256 (hex) of the canonical form: the JSON of
{"id", "name", "steps": [{"body", "kind", "store"}]} with keys sorted, no
whitespace and non-ASCII text as UTF-8 (\uXXXX escapes only for control
characters). The description is not part of it.
MigrationFormatError ¶
Bases: ValueError
A migration file or directory that does not follow the format.
Migration
dataclass
¶
canonical ¶
canonical(migration: Migration) -> str
The text the checksum is taken of (see the module documentation).
parse_migration ¶
parse_migration(filename: str, text: str) -> Migration
The migration in text, read from a file named filename.
to_toml ¶
to_toml(migration: Migration) -> str
The file text of migration; parse_migration reads it back unchanged.
statements ¶
statements(kind: str, body: object) -> list[str]
A sql / cypher-like body: one statement or a non-empty list of them.
load_migrations ¶
load_migrations(directory: str | Path) -> list[Migration]
Every *.toml in directory, by id; other files are ignored.
by_id ¶
by_id(migrations: Sequence[Migration]) -> list[Migration]
migrations sorted by id; MigrationFormatError on a duplicate id.
rhizodat.migrations.runner ¶
Running migrations against a Rhizodat: the history lives in the journal store.
Before any step runs, the source is checked against the history: an applied
migration must still be there unchanged (same checksum), and a new one must be
newer than every applied one. Every pending step must name a configured store
that runs its kind. Then each pending migration runs its steps in order and is
recorded with the HLC time of the record, so schema_versions(as_of=...) says
which schema applied at a moment.
A migration over several stores is not atomic: a failing step stops the run
with MigrationFailed and leaves the migration unrecorded, so the next run
runs all of it again. That is why steps must be idempotent. Run one runner at a
time; a second one that records a migration first is reported, not repeated.