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.
| Verb | What it does |
|---|---|
data list | Show the fold the emit verbs would apply. Read-only. |
data capture | Capture observed rows from an INSERT-style dump. |
data retire | Withdraw one table’s data chain at the current head. |
data scaffold | Mint a data file for a table holding one inert placeholder record to fill. |
data check | Validate the corpus against the data-file grammar. |
data witness | Bind reference records’ bytes to the oplog. |
data repin | Re-point an orphaned data file’s anchor-op= at an opid you name. |
schema ref-tables | Read 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 schemaThe 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=2A 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=0One 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.
| Token | Meaning |
|---|---|
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=N | The 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=N | The 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=N | On 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=1Flags
| Flag | Meaning |
|---|---|
--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:
state | Meaning |
|---|---|
occupied | A floor selected, and rows survive to the emitted band. |
empty | A floor selected, and it declares the table empty (body=none). |
composed | No single floor; the band is composed from appends alone. |
unspoken | The corpus holds records for this table, but this run’s labels named none of them. |
none | No record in the corpus addresses this table at all. |
dropped | The 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-labelcannot 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
--atinside 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 refuse | data list reports |
|---|---|
data-witness-mismatch: the record’s bytes differ from its witness | witness-stale |
data-witness-missing: no witness in the covering seal’s range | unwitnessed |
witness-out-of-range, witness-digest-unsupported: no witness governs | unwitnessed |
data-file-missing: a witness names a record no file carries | a 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-runClassification has three rungs:
- A table whose oplog declaration reads
reference_data=trueat the run’s cut is captured asref. - Otherwise, a non-empty
--labelset captures it asseedwith exactly that label set. - 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.
| Flag | Meaning |
|---|---|
--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-run | Classify, 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
refchain at one anchor whosereplacefloor cannot render at that anchor (migratewould refusereplace-no-upsert-form, orkey-uncoveredunder an inbound foreign key) still mints the fresh floor. The verdict readssupersedes @<anchor> (no-upsert-form at the anchor)or(key-uncovered at the anchor)(supersedes_causein JSON). The sealed anchor’s catalog never changes, so the new floor at HEAD is the cure; seedocs/migrate.md. - When
import s.sqlhas moved a floor to anappend(seedocs/import.md), capturings.sqlagain moves the key back even at identical rows. The capture mints the floor and reportsoverwrote @<anchor> (append → replace), orsupersedes @<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 stayalready-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-runWith 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 listA 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-endOne 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:
| guard | what it catches | remedy |
|---|---|---|
G-a | A record’s declared end line never arrives before EOF. | Close the record with its declared end line. |
G-b | A 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-c | Content between records that no marker pair frames, typically statements pasted straight into the section. | Wrap the statements in a marker pair. |
G-d | A marker line carrying the retired bytes= token. | Drop the token; the frame no longer counts bytes. |
G-e | A marker line carrying an unrecognized, duplicated or mis-ordered token, or a malformed end= tag. | Correct the token to the grammar above. |
G-f | The 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-g | A UTF-8 byte-order mark at the start of the file. | Strip the BOM. |
G-h | A 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-i | A 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-j | Write-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-k | The parse-side mirror of G-j: a record whose body consumed the enclosing section’s end line. | Place the record outside a section. |
G-l | The 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.sqlThe 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.sqlWith 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.
| Flag | Meaning |
|---|---|
--dry-run | Render 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 4c1382aed592The 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:
| Target | Refusal |
|---|---|
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 reportThe 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=trueA 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 declarationCommit the file with the declaration. A root you named (the data_root key or
--data-root) is never created.
Flags
| Flag | Meaning |
|---|---|
--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-run | On 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| Code | When |
|---|---|
ref-table-not-live | The 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-invalid | The argument is not a well-formed glob (an unterminated character class, say). |
ref-table-pattern-qualified | The 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-partition | The argument names an attached partition child exactly. The refusal names its partition root; declare the root instead. |
ref-tables-rejected | The covering envelope code carrying the array above. It names the envelope’s kind, never the first failing argument’s. |
ref-tables-schema-not-in-cut | The 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):
| Code | When |
|---|---|
dml-ref-table-no-match-universe | The 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-disagreement | The 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-dormant | add (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. |
Related
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.