| Field | Value |
|---|---|
| Status | Accepted (2026-06-05) |
| Drives | PRD-018 §5 WS-0/WS-1, §6.4 |
| Companion ADRs | ADR-100 (canonical IRI — first migration consumer), ADR-098, ADR-099 |
| Affected paths | crates/visionclaw-adapters/src/{oxigraph_ontology_repository.rs,sparql_migrations.rs}, crates/visionclaw-adapters/migrations/sparql/ (new — crate-local so it is inside the dev-container bind-mount scope; repo-root migrations/ is not mounted), migrations/sqlite/0001_initial.sql (discipline parity reference) |
| Evidence | migrations/sqlite/0001_initial.sql:11-14 (sole CREATE TABLE, CI-enforced), oxigraph_ontology_repository.rs:44-53 (named graphs), PRD-018 §2.1 |
The SQLite settings store has a disciplined migration regime: a single schema_migrations table, versioned files under migrations/sqlite/, and CI enforcement that CREATE TABLE appears only in migration 0001 (0001_initial.sql:11-14).
The Oxigraph triple store (RocksDB-backed) has no equivalent. There is no versioning of the graph schema, no idempotent way to apply a structural change (e.g. re-minting IRIs under ADR-100, or back-filling provenance under ADR-099), and SPARQL is string-concatenated with hand-rolled escaping (an injection surface). ADR-100 needs to rewrite existing subject/object IRIs in one transaction without leaving dangling references — that is precisely a migration, and there is no framework to run it safely or to record that it ran.
The reuse directive: mirror the SQLite discipline that already exists rather than invent a new mechanism, and use Oxigraph's native SPARQL UPDATE rather than a bespoke mutation layer.
Structural changes to the triple store are expressed as numbered SPARQL UPDATE files (0001_*.rups, 0002_*.rups, …) under crates/visionclaw-adapters/migrations/sparql/ (crate-local, so inside the dev-container bind-mount scope; the repo-root migrations/ holds only sqlite/), mirroring migrations/sqlite/. Each migration is a single logical transaction (Oxigraph applies UPDATE atomically) and MUST be idempotent (guarded by an existence check or DELETE/INSERT WHERE that is safe to re-run).
Applied migrations are recorded in urn:ngm:graph:migrations (a new named graph alongside the existing urn:ngm:graph:* family at oxigraph_ontology_repository.rs:44-53 — reuse the discipline, add one graph). Each record carries version, checksum, and applied-at timestamp. On startup the repository compares files-on-disk to the ledger and applies pending migrations in order, exactly once. This is the triple-store analogue of schema_migrations.
Migrations and the repository's mutating queries use parameterised/escaped construction (or Oxigraph's typed term builders) rather than the current hand-rolled string escaping. This closes the injection surface flagged in PRD-018 §2.1. New code MUST NOT concatenate untrusted strings into SPARQL.
The framework ships with its first real consumers (as shipped under
crates/visionclaw-adapters/migrations/sparql/):
0001_canonical_iri_remint.rups— rewrite existing entity IRIs tovc:{domain}/{slug}(ADR-100 D1), rewriting all subject/object positions in one transaction so no references dangle. Idempotent: re-running on already-canonical IRIs is a no-op.0002_bootstrap_shapes_graph.rups— bootstrap the SHACL shapes named graph (ADR-127 trust layer).0003_bootstrap_provenance_graph.rups— bootstrap the provenance named graph and tag inferred quads with provenance markers (ADR-099 D3).
Extend the existing CI discipline: a check that the migrations/sparql/ ledger logic is the only path that performs structural rewrites, analogous to the SQLite "CREATE TABLE only in 0001" rule. Migrations are append-only; an applied migration file is never edited (a fix is a new migration).
Positive:
- ADR-100's re-mint becomes a safe, idempotent, recorded operation instead of an ad-hoc one-off — no dangling references, re-runnable.
- The triple store gains the same auditability the settings store already has.
- Parameterised SPARQL removes the injection surface.
- Pure reuse of an established in-repo pattern; no new dependency.
Negative / risks:
- Idempotency is the migration author's responsibility; mitigated by a required dry-run/round-trip test per migration and the checksum ledger detecting drift.
- Large re-mint migrations over a big RocksDB store take time at startup; run once, recorded thereafter, and gated behind the ledger so they never re-run.
- A new named graph (
urn:ngm:graph:migrations) must be excluded from graph-export/round-trip and from the GPU/inference paths; documented in the named-graph table.
- Unit: applying a migration twice is a no-op (idempotency); ledger records version+checksum; pending migrations apply in order exactly once.
- Integration:
0001re-mint leaves zero dangling subject/object references (SPARQL count of orphaned references = 0);0002back-fills provenance on all pre-existing inferred quads. - CI: structural-rewrite-only-via-migrations check passes; SPARQL injection lint passes.