retrofit0.10.0-wip.8

retrofit data: the data corpus

The data verb group works on the data corpus: every addressed *.data.<format> data file under a schema’s declared data_root (written as .data.sql; legacy *.dml.sql files are still read). Data files carry row data (reference tables, fixtures, migration backfills) beside the DDL history the oplog carries, and the emit verbs (snapshot, migrate) fold them into a data band at emit time.

VerbWhat it does
data listShow the fold the emit verbs would apply. Read-only.
data captureCapture observed rows from an INSERT-style dump.
data retireWithdraw one table’s data chain at the current head.
data scaffoldMint a data file for a table holding one inert placeholder record to fill.
data checkValidate the corpus against the data-file grammar.
data witnessBind reference records’ bytes to the oplog.
data repinRe-point an orphaned data file’s anchor-op= at an opid you name.
schema ref-tablesRead or edit which tables hold reference data.

retrofit data list

data list is the consumer-side dry run of the data band. It answers “what would snapshot / migrate actually apply?” over the same fold those verbs read, and it emits no SQL and writes nothing.

retrofit data list                                 # the reference fold, then the label inventory
retrofit data list --label env:dev                 # the fold for one dataset
retrofit data list --label base --label env:dev    # two layers, in that order
retrofit data list --label env:dev --at op48       # the fold at an earlier cut
retrofit data list --schema sales --label env:dev  # a non-default schema

The two-invocation journey

A bare run (no --label) folds the reference band, then renders the label inventory: which dataset names the corpus holds, and how many files carry each. The unlabeled ref band applies with no flag, so its tables list as they would emit; with no label named, every labeled chain sits outside the fold and its records count under not-selected.

$ retrofit data list
public.accounts  unspoken  floor=none  appends=0  excluded=3(not-selected:3)  s3=0
public.currencies  occupied  floor=ref/replace/data@op9  witness=witnessed op10  appends=0  excluded=0  s3=0
public.regions  unspoken  floor=none  appends=0  excluded=2(not-selected:2)  s3=0
public.sessions  unspoken  floor=none  appends=0  excluded=1(not-selected:1)  s3=0
label base  files=2
label env:ci  files=2
label env:dev  files=2

A bare run folds no label, because layer order comes only from the order of the --label flags. Read the inventory, then name the layers:

$ retrofit data list --label base --label env:dev
public.accounts  occupied  floor=env:dev/replace/data@op40  witness=unwitnessed  appends=1  excluded=1(superseded:1)  s3=0
public.currencies  occupied  floor=ref/replace/data@op9  witness=witnessed op10  appends=0  excluded=0  s3=0
public.regions  occupied  floor=base/replace/data@op12  witness=unwitnessed  appends=0  excluded=1(not-selected:1)  s3=0
public.sessions  unspoken  floor=none  appends=0  excluded=1(not-selected:1)  s3=0

One line per table, in bytewise table order; the inventory lines follow in bytewise label order. An item with a floor carries the floor record’s witness state as witness=<token>: witnessed <opid> when a witness governs the record and its bytes still match, witness-stale when they no longer do, unwitnessed when no witness governs it (every seed floor reads unwitnessed; witnesses govern reference records only). When the run repairs a record for a schema change since its anchor (a rename, say), a notice: <table>: … line on stderr precedes the items.

The fields on a line

An item line is the table, its state (below), then these tokens in this order. A bare run’s inventory lines carry the last one.

TokenMeaning
floor=<label>/<disposition>/<body>@<anchor>The floor: the record whose replace sets the table’s rows. <label> is the layer that placed it (ref for the reference band), <body> is data, or none for a table declared empty, and <anchor> is the op the record is anchored at. floor=none when no floor occupies the table.
witness=<token>The floor record’s witness state, as described above. Printed after a floor only.
removed=<key>[,<key>…]The keys a reference replace floor dropped from the rows its chain held before, or removed=not-computable (<cause>) when they cannot be worked out offline. Printed after such a floor only.
appends=NThe contributions stacked on the floor, or composing the table alone when there is none: one per layer, anchor and disposition (append or restate), however many records each holds. --as=json names each in appends[].
excluded=N(<cause>:<n>,…)The table’s records this fold leaves out, then the count under each cause. -v adds one indented line per record, with its cause, label=, anchor= and file=; --as=json names each in excluded[].
s3=NThe contributions anchored below a lower layer’s floor: an append or restate from a later --label layer, anchored at an earlier op than the floor the layers beneath it (the reference band included) put on the table. It was written against an older version of the dataset it stacks on, so a statement in it may no longer match the rows beneath. Information only: the fold is the same and nothing refuses. --as=json names each in s3[], where lower_label is that floor’s layer as floor= spells it (ref for the reference band), and snapshot and migrate note each one under -v as data-layer-below-floor. A replace never counts, because it discards the layers beneath it. s3=0 is the usual reading.
files=NOn a bare run’s label <label> inventory lines: the number of data files carrying that label.

s3=1 below says env:dev holds one contribution to public.accounts anchored before base’s floor at op61; --as=json gives its anchor:

$ retrofit data list --label base --label env:dev
public.accounts  occupied  floor=base/replace/data@op61  witness=unwitnessed  appends=1  excluded=0  s3=1

Flags

FlagMeaning
--label <label>Name a dataset to fold in. Repeatable and ordered: each occurrence is one layer, and a later layer’s disposition=replace floor supersedes the layers below it for that table. A file carrying several named labels folds in each of those layers and is emitted once, at the highest of them.
--at <opid>Resolve the fold at an earlier oplog cut instead of at the head, so you can see the band as it stood then.
--schema <name>The registered schema whose corpus to read. Resolves from the config chain when omitted.

state: what the fold decided

Each item carries one state literal:

stateMeaning
occupiedA floor selected, and rows survive to the emitted band.
emptyA floor selected, and it declares the table empty (body=none).
composedNo single floor; the band is composed from appends alone.
unspokenThe corpus holds records for this table, but this run’s labels named none of them.
noneNo record in the corpus addresses this table at all.
droppedThe table was dropped from the schema, or ended by a JoinTable, SplitTable or MergeTable; its records can no longer apply.

A dropped table’s records are listed in excluded[] under table-dropped. A table a JoinTable, SplitTable or MergeTable ended takes that op’s own cause instead (table-joined, table-split, table-merged, table-inlined or table-unjoined), and snapshot and migrate report it as “table T was joined into P at op N”. The corpus cannot compute the product’s rows (a join or split predicate, or an undeclared source’s rows), so nothing is owed on the source’s records: they stay as history, and a witnessed one stays verifiable. A declared product is captured on its own; op add advises the capture (data-motion-product-capture-owed), and op seal refuses reference-rows-uncaptured until the product has a reference record anchored at or after the op.

A seed record on a table inside its reference span is dormant: while the table is declared at the render point, data list, snapshot and migrate never fold its seed records, stack them over the reference rows, or apply them alone, whether or not the table holds reference records yet. A run that names the chain’s label lists them in excluded[] under reference-data, and a table whose only named records are dormant reads unspoken, never none. The seed chain is kept and never reclassified.

The emit verbs warn once per dormant chain (apply-risk-data-dropped, at every verbosity) that the table “is reference data since op <op>”, and name the chain’s recourse by the table’s name at HEAD:

  • For a one-label chain, retrofit data capture --from-label <label> --table <table> promotes the fixture’s rows into the reference chain.
  • For a chain with several labels, which --from-label cannot fold, the warning names a capture of the table from a database instead: retrofit data capture <dump> --table <table>.
  • retrofit data retire --label <label> <schema.table> withdraws a chain that was only a stand-in; for a chain with several labels, the line names every label.
  • When HEAD no longer has the chain’s table (a historical --at inside a span whose table was later dropped), the warning names no command.

A --schema you typed is repeated on the capture line. Outside every span, before the declaration or after schema ref-tables remove, the seed chain is an ordinary dataset again.

Refusals

data list refuses exactly where snapshot’s corpus scan and fold refuse, with the same code, and never degrades into a partial listing. A corpus that will not scan is a corpus the emit verbs cannot apply either, and reporting a partial fold would answer the reviewer’s question wrongly rather than not at all. A declared data_root that does not exist refuses data-path-missing here as it does there. An unreconciled fork in (genesis, --at] refuses fork-in-range (exit 1) with snapshot’s retrofit reconcile <shared-prev> hint rather than listing both arms; an --at at or before the fork’s shared prev= lists normally.

The witness gate is the exception: data list is the dry run of what the band would emit, not of the witness check in front of it, so no witness finding refuses it. Where validate, op seal, snapshot, migrate and data check refuse a covered reference record, data list lists the record and reports the finding in its witness= token instead:

Finding the other verbs refusedata list reports
data-witness-mismatch: the record’s bytes differ from its witnesswitness-stale
data-witness-missing: no witness in the covering seal’s rangeunwitnessed
witness-out-of-range, witness-digest-unsupported: no witness governsunwitnessed
data-file-missing: a witness names a record no file carriesa stderr line listing the record from the oplog, witnessed <opid>

A record in the unsealed tail whose witness is stale or absent reads witness-stale or unwitnessed too; there only op seal refuses it. To get the refusal and its code, run retrofit validate or retrofit data check.

JSON

Under --as=json each item rides the stdout stream as {"type":"item", …}, followed by the terminal {"type":"ok"}; a bare run emits one {"type":"labels","present":[…]} record after the items and before {"type":"ok"}. See cli-output-contract.md for the record shapes.

The -- Data: designation line

snapshot and migrate write one comment line above every run of rows they emit, so a reviewer can tell reference rows from seed rows from the nearest line above:

-- Data: "shop"."statuses" (reference)
-- Data: "shop"."fixtures" (seed, label qa:golden)

The designation is reference, seed, label <label>, transition, cleanse or unclassified. A multi-label chain’s line names its placing layer’s label, not every label that selects it: the highest-positioned --label that selects the chain, the layer that put the rows in the band. A reviewer who drops that label from argv still ships the rows when another named label selects the chain, and the line then names the other label. A labelled ref record reads reference whatever its labels. A replace block that also carries another label’s rows opens that label’s line above them, inside the block, and its own line again below. data capture drops these lines from the bodies it mints and keeps any other comment, including a hand-written -- Data: loaded by hand.

retrofit data capture

data capture <source> records what an INSERT-style dump says each table’s rows are at the current oplog head. It observes state; it does not declare a transition or cleanse step, so this verb has no --purpose flag.

retrofit data capture rows.sql --label env:dev
retrofit data capture rows.sql --table countries --table 'curr*'
retrofit data capture rows.sql --schema sales --dry-run

Classification has three rungs:

  1. A table whose oplog declaration reads reference_data=true at the run’s cut is captured as ref.
  2. Otherwise, a non-empty --label set captures it as seed with exactly that label set.
  3. Otherwise the table is listed, not captured, and the run exits 0 with a call to action. There is no producer default.

Every captured table is a full observed row set (disposition=replace) at HEAD. The table’s key, read at render, sets how that replace replays: a table whose primary key, UNIQUE constraint or unique index the captured rows write in full upserts with that key as its ON CONFLICT arbiter. A table with no such key is cleared whole-table and reloaded when no other table’s foreign key references it, and refuses (key-uncovered) when one does. A secondary UNIQUE the arbiter does not cover, or a surrogate key re-minted at capture, can make an upsert collide or replace rather than update at apply; the recourse is chosen at capture time, by declaring the constraint DEFERRABLE INITIALLY DEFERRED (which removes it from arbiter candidacy; if the key is then a unique index on the same columns, the render refuses as replace-no-upsert-form) or, for a table no other table’s foreign key references, by capturing the table keyless (see docs/migrate.md). Capture accepts INSERT statements, including the output of pg_dump --inserts and pg_dump --column-inserts; COPY ... FROM stdin, UPDATE, DELETE, and MERGE are not capture input. Use import for authored custom DML statements.

FlagMeaning
--label <label>Capture otherwise-unclaimed tables as one seed dataset. Repeatable; the resulting label set is canonicalized.
--table <pattern>Restrict the source tables considered. Repeatable; accepts a bare relname or glob.
--schema <name>Registered schema to capture for. Resolves through the normal schema chain when omitted.
--dry-runClassify, compare, and report without writing. The run exits as the real run would.
--skip-manifest <path>Write skipped input statements as JSONL.

Text mode prints one captured … or already-captured … verdict per table. JSON mode emits per-table rehearsal records on a dry run and closes every run with an ok envelope whose data.tables array carries the verdicts. Rows equal to the chain’s fold are already-captured and mint nothing, with two exceptions:

  • A ref chain at one anchor whose replace floor cannot render at that anchor (migrate would refuse replace-no-upsert-form, or key-uncovered under an inbound foreign key) still mints the fresh floor. The verdict reads supersedes @<anchor> (no-upsert-form at the anchor) or (key-uncovered at the anchor) (supersedes_cause in JSON). The sealed anchor’s catalog never changes, so the new floor at HEAD is the cure; see docs/migrate.md.
  • When import s.sql has moved a floor to an append (see docs/import.md), capturing s.sql again moves the key back even at identical rows. The capture mints the floor and reports overwrote @<anchor> (append → replace), or supersedes @<anchor> (append → replace) once HEAD has moved (moved_from: "append" in JSON). Only the append’s own source moves it back: another source whose rows equal the append’s, a chain that still holds a floor, and a capture from stdin stay already-captured.

An unmatched --table pattern is an advisory, not a silent no-op.

A captured ref table that lost a key since its chain is still captured, with a reference-key-lost warning naming the lost keys: reference data should be monotonic, so restore the row or mark it inactive through its status column instead. drift raises the same warning for the tables it captures.

A declared table the source observes empty (its only statement an unfired TRUNCATE or WHERE-less DELETE) mints replace body=none and warns reference-table-emptied, the extreme removal: an empty lookup table usually calls for a schema verb instead: drop it (DropTable), archive it (Archive), or stop managing its rows (schema ref-tables remove). The observation is still minted, --dry-run warns as the run would, and an already-captured re-run, a seed chain and data retire do not warn. drift raises the same warning.

A <source> that is a stream (-, <(…), a FIFO) delivering zero bytes refuses source-empty (exit 2, Param source): on a pipe that is a producer that failed upstream. A <source> that is a URL, a non-.sql file or an extension-less regular file refuses kind-mismatch.

Each claimed table’s source band (the bytes of every INSERT statement naming it, summed over the whole source) is bounded by the schema’s data_band_max_bytes (see getting-started.md). A claimed table over the bound refuses data-band-over-cap (exit 2) before any row is retained or minted, --dry-run included; the envelope names every over-cap table with its bytes, the bound and its statement count. A table --table filters out, or one the run does not claim, is not counted. Recovery: raise data_band_max_bytes for the named schema, or remove the key for the built-in default.

retrofit data retire

data retire <schema.table> explicitly withdraws an existing data chain by minting an empty restate floor at HEAD. A missing table in a later source does not imply deletion; use this command when emptiness is the intended state.

retrofit data retire public.countries
retrofit data retire public.accounts --label env:dev
retrofit data retire public.accounts --label base --label env:dev --dry-run

With no label, retire addresses the table’s unlabelled ref chain. Repeated --label flags address the seed chain carrying exactly that label set; they do not select a union of matching chains. The target must be the schema-qualified positional name; there is no --schema or --purpose flag. If no such chain exists, retire refuses with no-such-chain and lists the chains that do.

A reference chain ends with its declaration, so an unlabelled retire is not how its rows are withdrawn. While the table is declared the retire refuses retire-declared-table, naming schema ref-tables remove; once the declaration is removed it mints nothing and reports the table not-managed: <table> (no longer reference data since op <op>), exit 0.

Removing a table’s declaration (schema ref-tables remove) ends its reference span, and retrofit stops managing the rows. A fresh build (snapshot at or past the removal, and data list) omits the records anchored inside the ended span and reports them undeclared (“no longer reference data at op <op>”, -v). snapshot --at inside the span still renders them, and a migrate window crossing the removal applies them as a keyed delta against the fold at the window’s start (see migrate). drift reports the chain not compared: <table> (no longer reference data since op <op>), information rather than drift. The records and their witnesses stay as history. Declaring the table again opens a new span; the old span’s records do not resume. A ref record carrying rows anchored on or past the removal refuses in every reader with data-file-malformed, class H-13, and the body=none floor an unlabelled retire minted before the removal is exempt; see data check below.

--dry-run rehearses the withdrawal. --skip-manifest <path> is accepted for the same reporting contract as the other producers. Text verdicts say retired, already-retired or not-managed; JSON uses the same rehearsal and terminal data.tables shapes as capture.

retrofit data scaffold

data scaffold mints a valid carrier beneath the resolved schema’s data_root holding one worked, inert record whose values are <column> placeholders, ready to fill by hand. A --purpose cleanse carrier holds a kind=DeleteStmt record instead, DELETE FROM <schema.table> WHERE <predicate>;, whose <predicate> is the placeholder to fill; its guidance comments name the kind= tokens for a record rewritten as an UPDATE:

retrofit data scaffold --table public.backfill --purpose transition --disposition append
retrofit data scaffold --table public.fixture --label env:test --disposition restate

--table <schema.table> is required. --purpose accepts only transition or cleanse and is exclusive with repeatable --label; a label set resolves the file to purpose=seed.

Scaffold runs the same classification chain the other producers run, so the table’s reference-data declaration is read at scaffold’s minting cut on every mint. A declared table is minted purpose=ref: declaring it is the whole declaration, and --purpose and --label are ignored for it. A run that passed either raises a dml-ref-table-flag-ignored note naming the flag it ignored. The run reports the minted file the same way for every band; the file’s header line is what carries purpose=ref:

dml-preserved: data/app.countries.202609281743.append.data.sql (declared-axes scaffold, 1 inert record to fill)

--purpose ref refuses with flag-invalid-value, since ref comes from the declaration rather than a flag; withdraw the declaration with schema ref-tables remove, or edit purpose= in the minted file, to get a different band. With no declared purpose, no label, and no policy match, the file is explicitly purpose=unclassified. --disposition accepts only append or restate; observed replace data belongs to data capture. --anchor-op <12hex> overrides the default reconciled head; a witnessed key refuses (data-witnessed-key) and an anchor a seal covers refuses (sealed-anchor). A restate of a declared table states the whole table by hand and renders as a replace; the mint raises a reference-restate-authored note naming data capture as a version’s usual door (courtesy band: -q drops it), and data check raises it again over the filled file. A witness governs the ref chain only, so the refusal applies to a ref key: a record of another band at the witnessed anchor is another key and mints (or re-pins) unrefused.

The destination is selected rather than supplied: the file lands under data_root at the deterministic path for its mint key. The file initially holds guidance comments and one inert src=hand record: an INSERT over the table’s live columns with a <column> placeholder per value. data check is the fill gate: it refuses the file naming every placeholder still present, and migrate and snapshot refuse an unfilled scaffold (dml-body-unfilled) rather than reading it as empty. Fill the record and run data check before committing it.

Authoring records by hand

A minted carrier holds a header line, the guidance comments, and one inert record with a <column> placeholder per value. Replace each placeholder with a value, then check it:

retrofit data scaffold --table app.loan_periods --purpose transition --disposition append
$EDITOR data/app.loan_periods.202609281743.append.data.sql   # the path `dml-preserved:` names
retrofit data check
retrofit data list

A record is a frame, not a row. The marker pair delimits a body; that body may hold one statement, several statements, or one multi-row INSERT. The compact form is the idiomatic one:

-- retrofit:dml-section
-- retrofit:dml seq=1 src=hand kind=InsertStmt
INSERT INTO loan_periods (id, name) VALUES
  (1, 'weekly'),
  (2, 'monthly'),
  (3, 'quarterly'),
  (4, 'annual');
-- retrofit:dml-end
-- retrofit:dml-section-end

One marker pair per row is also valid and applies the same rows, as four records instead of one. seq positions the record for replay, ascending; its uniqueness runs across every file at the same anchor, not just within this one, so start from the number the scaffold’s guidance printed rather than from 1. src=hand marks the record as hand-authored rather than captured from a dump.

Both marker lines are load-bearing, and every way to get the framing wrong refuses by name rather than mis-parsing:

guardwhat it catchesremedy
G-aA record’s declared end line never arrives before EOF.Close the record with its declared end line.
G-bA marker-prefixed line inside a record body that was never closed: the next record’s marker read as body content.Close the first record with -- retrofit:dml-end, or escalate the frame: add end=<tag> to its marker line and close the body with -- retrofit:dml-end:<tag>.
G-b'A line that differs from the declared end line only by trailing whitespace or a carriage return: an end line the scan cannot match.Normalize the file’s line endings; the body is otherwise correct.
G-cContent between records that no marker pair frames, typically statements pasted straight into the section.Wrap the statements in a marker pair.
G-dA marker line carrying the retired bytes= token.Drop the token; the frame no longer counts bytes.
G-eA marker line carrying an unrecognized, duplicated or mis-ordered token, or a malformed end= tag.Correct the token to the grammar above.
G-fThe same end= tag claimed by two records in one file.Give the record a tag free across the whole file; the refusal names the lowest free tag.
G-gA UTF-8 byte-order mark at the start of the file.Strip the BOM.
G-hA tagged record’s end line never arrives before the section end; the scan crossed the fence looking for it.Close the record inside its section.
G-iA record whose body is empty: its marker line is immediately followed by its end line.Give the record a body, or remove the record.
G-jWrite-path only. A body carrying a -- retrofit:dml-section-end line that would be minted inside a section, where a tagged scan would swallow the fence’s terminator.Place the record outside a section.
G-kThe parse-side mirror of G-j: a record whose body consumed the enclosing section’s end line.Place the record outside a section.
G-lThe same seq= claimed twice within one file.Renumber so each record in the file claims a distinct seq=.

All of them except G-j surface through data check as data-file-malformed, carrying the guard as the refusal’s class along with the offending line, so the refusal names the fix. G-j is a write-path gate instead: it is checked before any byte is minted, over the whole set at once, so a refused mint leaves no partial file behind. By the time data check reads a file, the shape it guards cannot be there. Its read-path mirror G-k covers a file that acquired that shape some other way.

A reference append (or restate) whose body is an UPDATE, DELETE, MERGE, INSERT … SELECT, or an INSERT leaving a column DEFAULT is legal and applies once in its window, but the chain no longer folds offline: drift cannot compare it and removed keys cannot be computed until the next version. data witness still witnesses the record and warns reference-chain-unfoldable naming the table, the record’s anchor and the statement that does not fold (structured: table, opid, reason); the exit is unchanged. A literal-INSERT append folds and raises nothing.

retrofit data check

data check [path...] validates corpus files without emitting SQL or minting anything. With no operands it walks the configured corpus root; named files or directories narrow that walk.

retrofit data check
retrofit data check corpus/fixtures corpus/public.accounts.202609251400.data.sql

The command evaluates every addressed file and reports all findings together. Malformed or conflicting files make the run fail; advisory-only findings such as an unaddressed candidate filename do not change the exit status. A clean text run reports data check: N file(s) checked, no findings; JSON emits a data-check record with checked: N and an empty listed: {}. A run that listed warnings but refused nothing counts them by code instead, as in data check: 1 file(s) checked, no refusals; listed: anchor-shape-mismatch ×1, and the JSON record carries "listed":{"anchor-shape-mismatch":1}. The exit is still 0, so a CI step that must stop on a listed warning gates on listed.

A record whose anchor-op= names an opid the oplog no longer holds also fails the run, as data-file-anchor-unresolved. Removing an op takes its id out of the log, and a revise that changes an op’s content mints a new id for the new act, so either leaves a record anchored on the old identity pointing at nothing. Merely reordering an op does not: an id is minted once and a reorder is not a new act, so the anchor survives it. data check reports a stale anchor at the pre-commit gate, before migrate meets it. The recourse is retrofit data repin <file> --anchor-op <opid> (below), and one invocation re-pins every record in that file.

A purpose=ref record carrying rows whose anchor-op= resolves to a cut where its table is not declared reference data fails the run as data-file-malformed, class H-13, naming the file, the table and the anchor. That covers a declaration withdrawn at or before the anchor (schema ref-tables remove; the detail names the op that withdrew it), a table never declared, and a table declared only after the record’s anchor: rows from before a table’s declaration belong on a seed chain, or on none. The header’s table name is compared as the anchor spells it, so a header naming the table by a later name is not declared there either. snapshot, migrate and data list refuse the same file before emitting anything. The recourse is a verb, and it is not data repin, which moves only an anchor that no longer resolves: remove the file, and to keep its rows declare the table again (schema ref-tables add) and capture them at HEAD (data capture), which mints a record inside the new span. A body=none floor is exempt, since it carries no rows; one anchored after its table’s span ended (the floor an unlabelled retire minted before schema ref-tables remove) is omitted as undeclared by every reader and skipped by data witness. A project with no resolvable oplog yields no such finding.

A purpose=ref restate body=none anchored where its table is declared fails the run as data-file-malformed, class H-14, naming the file, the table and the anchor: withdrawing a reference table’s rows is ending its span, and no producer mints this record (data retire refuses a declared table). The recourse removes the file (rm <file>); to stop managing the table’s rows, then end its span (schema ref-tables remove <table>), which, like every corpus reader, refuses while the file stands. snapshot, migrate and data list refuse the same file. The same file anchored where no span is in force is the exempt floor above; a replace body=none is an observation (see data capture) and a seed restate body=none is data retire’s, and neither is refused.

A filled purpose=ref restate carrying rows on a table declared at its anchor passes, with the reference-restate-authored note data scaffold raises (never as data-file-unfilled).

Two floor files of one chain at one anchor (the same table, band and label set, each a replace or restate claim on the table’s whole content) fail the run as dml-data-conflict, naming both files, exactly as snapshot, migrate and data list refuse them. A merge of a data capture branch with a data retire branch reaches that state with no hand editing. The pages of one paginated capture are not rivals, two bands at one anchor are two chains, and a sibling schema’s files under a shared data_root are left to that schema’s own check. Beside other findings the fork rides the same single envelope, with its own recourse: re-run the producer for that key, or rm one of the two files.

Two cases raise no anchor finding. A project with no oplog skips the check, since import-driven flows write data files before any op exists. A record that belongs to a sibling schema under a shared data_root is left to that schema’s own data check.

retrofit data witness

data witness [path...] binds reference records’ bytes to the oplog. It appends one WitnessReferenceData entry per selected record, carrying the record’s digest over every file at its key (table, label set, anchor). validate, op seal, migrate and snapshot verify a witnessed record whose anchor a seal covers.

retrofit data witness                 # every reference record in the unsealed tail without a witness at its bytes
retrofit data witness --dry-run       # the same rows, appending nothing
retrofit data witness corpus/public.colors.202609251400.data.sql

With no operands it selects every purpose=ref record under data_root whose anchor is in the unsealed tail and which has no witness at its current bytes. Operands name record files; a directory refuses with witness-operand-directory. A named record outside the selection refuses with witness-band (a file that is not a reference record under data_root, or a record anchored outside its table’s reference span) or sealed-anchor (a seal covers its anchor). One already witnessed at its current bytes is reported already-witnessed.

FlagMeaning
--dry-runRender the rows the run would produce, and append nothing.
--schema <name>The registered schema whose corpus to witness. Resolves from the config chain when omitted.

retrofit data repin

data repin <file> --anchor-op <target> [--dry-run] re-points one corpus data file’s anchor-op= at an opid you name. It is the recourse of data-file-anchor-unresolved above, and of nothing else.

retrofit data repin corpus/public.colors.202609251400.data.sql --anchor-op last --dry-run
retrofit data repin corpus/public.colors.202609251400.data.sql --anchor-op 4c1382aed592

The target is an opid, an anchor ref (last, seal, each with optional ^/~N) or a 1-12 hex opid prefix (migrate --to’s grammar). What is written is always the resolved 12-hex opid; the literal spelling is never stored. --anchor-op has no default: without it the verb refuses with anchor-target-required. Run retrofit op show to read the opids, or use the successor that op revise’s re-mint advisory names.

Eligibility is orphan-only. A file whose anchor still resolves is history-bound at a live op, so moving it is a relocation rather than a repair and the verb refuses anchor-still-resolves; capture the rows at the new point with data capture instead. A file already at the target is already-pinned, exits 0 and writes nothing, so a re-run is byte-stable.

A re-pin of a witnessed reference record re-witnesses it: the verb appends one WitnessReferenceData entry at the new anchor, over the rewritten bytes, and names its opid. The old witness is left in place (after op revise it names a retired opid, which is how the orphan was detected); op remove it once the re-pin lands. An unwitnessed record stays unwitnessed, and --dry-run appends nothing. Re-pin onto the successor op revise names rather than last: after the revise, last is the old witness itself.

Only the header’s anchor-op= value changes. Every other byte of the file (body rows, comments, record markers and every other header token, including the ones it omits) is passed through unaltered. --dry-run computes and reports the same decision, prefixed would repin: , and writes nothing.

The target is also checked for legality before any byte is written, and each of these refuses with the file unchanged:

TargetRefusal
The pre-genesis cut (root).anchor-target-pre-genesis
A cut where the file’s table is not live.data-file-malformed, class H-11
A cut where a ref record’s table is not declared reference data. A body=none floor is exempt, since it carries no rows.anchor-target-undeclared
A cut where a ref restate body=none floor’s table is declared.data-file-malformed, class H-14, the refusal data check would give the moved file
A cut another file already claims for the same table and labels.data-file-collision

A paginated version refuses with data-file-paginated before any of these: the verb is single-file and does not move a page set, so re-capture it at HEAD instead.

retrofit schema ref-tables

ref-tables declares which tables hold reference data (rows the application reads but never writes, such as currency codes, country lists and status enumerations), applied on every emit with no --label. It is the second rung of the classification chain in import.md, and the declared set is the reference capture scope every producer run reads at its own minting cut: import, data capture, drift (which captures the declared tables at its new head on every run, with no flag), and data scaffold.

The declaration is an attribute of the table’s element, carried in the oplog: add and remove append one SetReferenceData op per resolved table. Nothing is stored in retrofit.toml, and a declaration survives a rename because it lives on the element rather than on its name. A table can also be declared as it is created (op add CreateTable … reference_data=true, or --ref <table> on import and drift), and ref-tables reads those declarations like its own.

retrofit schema ref-tables                          # read the declared set and report
retrofit schema ref-tables add countries 'curr*'    # declare the tables the arguments resolve to, then report
retrofit schema ref-tables remove currencies        # withdraw a declaration, then report

The bare parent reads; there is no list subcommand. All three forms print the same report, the whole declared set rather than a delta, so a read and an edit compare byte for byte.

Each argument is an exact table name or a glob, resolved against the tables live at the read cut: HEAD, unless --at <opid> names another. A glob is never stored: it expands when you run the command, and the resolved table names are what the oplog carries, so a table created later is not declared by an earlier glob. A declaration names a live element, so the table must exist first: an argument that resolves to nothing is refused (ref-table-not-live). Import the DDL, drift it in, or create it with reference_data=true, then declare it. Declaring a table that is already declared, or removing one that is not, is a per-name no-op that mints nothing and says so (already-declared, not-declared) rather than refusing; --ref and op add SetReferenceData answer the same way. The exception is a merge or join product whose sources disagreed about the declaration: op seal refuses it until a SetReferenceData on it records the judgment, so every one of those writes mints that line even though the product already reads declared. A glob never declares an attached partition child; it reports each one as skipped: instead.

The report lists the declared set as one declared: line per table, in bytewise order. On add and remove one line per resolved table follows: the table name, then the SetReferenceData op the run minted (or, under --dry-run, would mint) as its on-disk oplog entry verbatim, SetReferenceData(<opid>, prev=<prev>, on=<element-id>, value=<bool>), the same line that lands in the oplog. It is a record of what was written, not a command to run. Then comes one line per disagreeing table, and a run-level summary:

$ retrofit schema ref-tables add countries 'curr*' --schema app
declared: countries
declared: currencies
countries  SetReferenceData(00664d8eaeaa, prev=6ccda870b705, on=081f8bafa7ea, value=true)
currencies  SetReferenceData(32bbc6fd0f0e, prev=00664d8eaeaa, on=2513d190b2ad, value=true)
ok  app  declared_tables=2  disagreeing_tables=0  dry_run=false
$ retrofit schema ref-tables add countries loan_periods --dry-run --schema app
declared: countries
declared: currencies
declared: loan_periods
countries  already-declared
loan_periods  SetReferenceData(8c78aa7cd7e2, prev=32bbc6fd0f0e, on=49448cf0ecb0, value=true)
ok  app  declared_tables=3  disagreeing_tables=0  dry_run=true

A run with --at appends at=<opid> to the summary. The full grammar, text and JSON, is the schema ref-tables row of cli-output-contract.md.

The first declaration in a project whose data_root is the undeclared default and does not exist yet creates that root with an empty .gitkeep, and says so on stderr, so every checkout has the root the declaration’s records will land in:

created data/.gitkeep so every checkout has the data root; keep it with the reference-table declaration

Commit the file with the declaration. A root you named (the data_root key or --data-root) is never created.

Flags

FlagMeaning
--schema <name>The registered schema whose declared set to read or edit. Persistent on the parent, so it parses on the bare read and after either subcommand.
--at <opid>The read cut (HEAD when omitted). Persistent, like --schema: on the bare read it reports the declared set as of that op, and on add / remove it is the cut the arguments resolve against.
--data-root <dir>Root that overrides the schema’s data_root (singular; not accepted with --label). Relocates the base corpus the disagreement report reads; it adds no seed layer. Persistent, like --schema.
--dry-runOn add / remove only: report the would-be oplog entries, and write nothing.

Refusals

Argument failures accumulate: every argument is evaluated and each failure is reported beside the argument that caused it, rather than the run stopping at the first. The envelope carries the covering code ref-tables-rejected, each entry in its violations array carries its own per-failure code (see cli-output-contract.md), and the whole invocation is refused, so nothing is minted, not even for the arguments that resolved:

$ retrofit schema ref-tables add app.countries '[abc' 'zzz*' loans --schema app
note: schema-context: app
error: ref-tables-rejected: 3 ref-tables arguments were rejected
  violation: app.countries: ref-table-pattern-qualified: the argument qualifies itself with the selected schema's own name
  violation: [abc: ref-table-pattern-invalid: malformed glob pattern
  violation: zzz*: ref-table-not-live: no live table of the selected schema matches this argument at the read cut
  hint: fix each argument the `violation:` lines name and re-run; the whole invocation is refused, so nothing was minted
CodeWhen
ref-table-not-liveThe argument resolves to no table live at the read cut: an exact name no live table carries, a glob that matches none, or a glob whose every match is an attached partition child.
ref-table-pattern-invalidThe argument is not a well-formed glob (an unterminated character class, say).
ref-table-pattern-qualifiedThe argument qualifies itself with the selected schema’s own name. The schema is already selected (by --schema, or from the config chain when it is omitted), so the argument takes a bare name.
reference-data-partitionThe argument names an attached partition child exactly. The refusal names its partition root; declare the root instead.
ref-tables-rejectedThe covering envelope code carrying the array above. It names the envelope’s kind, never the first failing argument’s.
ref-tables-schema-not-in-cutThe named schema is registered in retrofit.toml but absent from the oplog at the read cut, so there is no live table universe to resolve against. Distinct from unregistered-schema, which is the registry miss.

An unregistered schema refuses with unregistered-schema.

Notes

Three stderr advisories ride the report, all at the courtesy band (they drop under -q):

CodeWhen
dml-ref-table-no-match-universeThe schema’s oplog has no ops at HEAD, so no table is live and nothing can be declared yet: an argument to add or remove is neither declared nor refused. Import the DDL first, then declare.
dml-ref-table-policy-disagreementThe committed corpus carries records the declared set would have classified differently: a declared table whose records are seed or unclassified, or a ref record on a table that is not declared.
dml-ref-table-seed-dormantadd (and add --dry-run) moves a table holding a seed chain into a reference span (the base corpus or a configured seed_paths root). One note per chain, naming its label set and earliest anchor: the chain is kept, never reclassified, and dormant while the table is declared. Its hint names the chain’s recourse, as listed under data list above. Nothing is reported for a table that was already declared. op add SetReferenceData <table> true and import/drift --ref on a live table raise the same note, on the write and --dry-run arms alike.
  • import.md: how data files are minted and classified.
  • snapshot.md: how the band is spliced into the DDL, between the tables and the constraints.
  • migrate.md: how the band is placed within a migration.

Before 1.0, any release may change behavior; the release notes list what changed.