Skip to main content

Module migrations

Module migrations 

Source
Expand description

The registry schema migrations (ADR-024) and the co-tenant connection handle.

§The machinery (the shared pattern)

One alkgit-owned stamp table versions the co-tenant schema as a unit — the record rows, ref rows, the registry_seq singleton, the mutation-marker rows, and the LFS liveness rows ship and evolve together, so one integer stamp covers them all (ADR-024 §2). Steps are ordered, hand-written, and monomorphic per engine twin (the twins already carry engine-specific SQL; migrations ride them — no framework, no builder). The runner’s contract, identical for both twins:

  • One transaction per step — the step’s DDL and its stamp advance commit together; a failed transaction rolls back whole and nothing is left half-applied by the failed step.
  • Construction-run, loud failure — migrations run when the adapter constructs its connection, and any unverified failure rides OpenError::Migration: a half-migrated store refuses service rather than degrading silently (the fallible-constructor posture ADR-024 §1). Every construction re-runs the runner, so a half-migrated store keeps refusing until the state is real or the store is reset.
  • Concurrent-open-safe — the stamp is read inside each step’s own BEGIN IMMEDIATE transaction (the write lock serializes racing openers; a loser sees the winner’s committed stamp and skips). A step whose SQL nevertheless fails verifies state against committed data — the alkstore ADR-012 §5 re-key pattern: the step’s effect present ⇒ benign race swallowed (and the stamp healed, converging the store); absent ⇒ propagate. Load-bearing on sqlite, where multi-process co-tenancy of one file is native.
  • Absent stamp ⇔ fresh store — the stamp table bootstrap creates the stamp row at value 0; a fresh store runs the whole step set and stamps it.
  • Additive-first, no refuse-gate in v1 — an unknown-forward stamp is safe to open against, and the runner leaves it untouched (the coexistence contract: explicit column lists, unknown columns/tables unread — ADR-024 §4). A non-additive step is major-version territory and owns its refusal semantics then.

§The engine-neutral logical DDL (the shared schema, both twins)

The initial co-tenant schema, stamped step 1 (ADR-024 §2/§5), in engine-neutral terms — both engines’ SQL lands in this module over exactly this logical schema: the sqlite twin’s step set is the sqlite module below; the postgres twin writes step 1’s pg SQL (BIGINT sequence columns, CHECK’d visibility text) in its own module:

  • alkgit_schema_stamp (id = 1, stamp) — the alkgit-owned stamp row (the machinery’s meta table, beside its own tables).
  • git_repos (repo_id PK, visibility in {'public','private'}, grants, seq) — the record rows: the backend.md §RepoRecord serde shape serialized (grants is the JSON object identity-id → action array; seq is the row’s mutation sequence number, assigned inside the row-write transaction).
  • git_refs (repo_id, name) → oid, symref_target — the ref rows (POC-O-validated shape; ADR-019 §2). HEAD is a symref row: oid NULL with a set target — exactly the unborn RefLine { oid: None } shape.
  • registry_seq (id = 1, value) — the watermark singleton; every registry mutation bumps it inside the row-write transaction (transactionally exact, ADR-019 §5).
  • repo_mutation_markers (repo_id PK, seq) — the per-push mutation-marker rows (composition-internal, ADR-019 §7: pushes do not touch RepoRecord; the receive flow’s watermark leg; the record schema is untouched).
  • lfs_objects (repo_id, oid) → size, refcount — the LFS liveness rows with the day-one refcount column (ADR-024 §5 — nothing published predates it, so no migration is owed and none should be written; the column’s consumption is wave 9). The table ships in the step set unconditionally — the tables evolve as one composition; the lfs feature gates the surface consuming it, so an inert table is harmless and the stamp stays unit-shaped.

The stamp table itself is bootstrapped idempotently before the first read (CREATE TABLE IF NOT EXISTS + INSERT OR IGNORE at 0), so “absent stamp ⇔ fresh store” holds under concurrent opens.

§The co-tenant connection handles (one per engine twin)

The handles’ placement is symmetric across engines — one file-local module per engine twin in this, the ADR-024 machinery home:

  • CoTenantSqlite (the sqlite feature) — the thin wrapper over the rusqlite connection that the registry adapter and the refs layer both consume: it opens the co-tenant connection (WAL, NORMAL synchronous, pinned 5000 ms busy timeout — substrate.md §assembly tuning datums) beside the engine-owned schemas on the same file (alkblobs ADR-013 §3’s composition posture), runs the migrations at construction, and hands out the connection for the twins’ blocking SQL (which the composition’s concurrency model bridges off the async executor).
  • [CoTenantPg] (the postgres feature) — the shape’s other half: the thin wrapper over the tokio-postgres client the postgres twins consume, natively async (no blocking bridge — the shared client lives under a tokio::sync::Mutex whose guard is held for one statement batch or transaction, the twin’s short-held-lock model). Its engine-locally-realized runner (below) rides the advisory migration lock instead of BEGIN IMMEDIATE.

The postgres twin’s pg machinery also carries the crate’s advisory lock-key plumbing over the co-tenant schema: advisory_lock_key (the deterministic key over a scope’s identity string) plus the named scope-key constructors beside it — the stamp-table/migration runner’s key, the git_refs CAS scope’s key (git_refs_lock_key, the refs CAS twins’ and every registry transaction touching the refs table’s serialization point), and the lfs_objects rows scope’s key (lfs_objects_lock_key) — every transaction touching a co-tenant table holds that table’s advisory lock (the wave-9 lock-discipline invariant), and the composition spells the scope identity through these constructors only, never hand-inlined. The runner’s step set is the pg step builder (build_steps), step order monotone with sqlite’s (STEP_VERSIONS).

Structs§

CoTenantSqlite
The co-tenant connection handle — the thin wrapper over the rusqlite connection that the sqlite registry adapter and the refs layer both consume (alkgit owns its tables and its connection, co-tenant beside the engine-owned schemas on the same file — alkblobs ADR-013 §3’s composition posture).