snapshot
retrofit snapshot emits canonical DDL for the materialized schema:
the SQL equivalent of replaying your oplog forward to a cut. The
output is intended to be pg_dump --schema-only parity-shaped (see
pg_dump-parity.md for the option-by-option
audit).
Read concepts.md first for the oplog / cut / replay
vocabulary. This doc covers what snapshot sees, what it produces,
what it cannot produce, and why.
What snapshot sees
One genealogy cut: the state of the schema after replaying ops up
to --at <opid> (default: last). --at also accepts the canonical
anchors root / last / seal, each with an optional ^ / ~N
decrement (always older). --at root refuses cleanly (opid-not-found,
exit 2): the empty pre-genesis cut has no live elements to render.
An unreconciled fork anywhere in (genesis, --at] refuses with
fork-in-range (exit 1), the same envelope migrate uses, hinting
retrofit reconcile <shared-prev>. On a fork there is no single cut to
render: the default --at last resolves to the line-order tail rather
than a leaf, so emitting would ship whichever branch the merge happened
to write last, which is silently wrong for non-commutative history such as two
concurrent renames of the same table. The check covers only the range:
an explicit --at <opid> at or before the fork’s shared prev renders
normally. It runs before the data band, so a labeled run refuses the
same way and emits no DDL and no data artifact. Under --as=json the
error envelope still goes to stdout; no partial artifact does.
The replay walks the oplog and materializes:
- the set of live elements (tables, columns, constraints, indexes, views) with their current names, types, and adjacencies;
- the dependency order needed to emit valid DDL.
snapshot is read-only: it never modifies the oplog. Output goes to stdout. Schema selection follows the six-step resolver chain.
retrofit snapshot # last cut, resolver picks schema
retrofit snapshot --schema sales # explicit schema
retrofit snapshot --at 988edcb612ab # cut at a specific opid
retrofit snapshot --include-drop=scoped # per-element DROP ... IF EXISTS prologue
retrofit snapshot --include-drop=schema # one DROP SCHEMA IF EXISTS CASCADE
retrofit snapshot --table=users --table=orders # pg_dump -t parity
retrofit snapshot --exclude-table=audit_log # pg_dump -T parity
retrofit snapshot --exclude-schema=staging # pg_dump -N parity
retrofit snapshot --using=postgres:18 # one-shot emit-target override
retrofit snapshot --no-header # suppress banner
retrofit snapshot --no-comments # suppress COMMENT ON
retrofit snapshot --label env:dev # embed the env:dev dataset between tables and constraints
retrofit snapshot --label base --label env:dev # layer two datasets, in that order
retrofit snapshot --data-only --over env:dev # the data band alone, projected onto the top layer
retrofit snapshot --skip-manifest skips.jsonl # record render-skipped elements (JSONL)--include-drop has three modes:
none(default): pureCREATE. Pair with externaldropdb/createdbif you need a clean reflash.scoped: per-elementDROP … IF EXISTSfor tables / indexes / FKs in the emission set, thenCREATE. Sibling objects in the same schema survive. Refuses inside aBEGIN/COMMITif any table outside the set holds an inbound FK into the set; switch toschemain that case.schema: oneDROP SCHEMA IF EXISTS <name> CASCADEper schema, thenCREATE. CASCADE blast radius covers every dependent outside the emission set; use only when you own the whole schema.
--include-drop=schema is incompatible with --table /
--exclude-table (flag-conflict): schema-level CASCADE would
remove the very tables --exclude-table claims to keep.
What snapshot produces
A canonical DDL document on stdout. Structure:
Banner, one comment line (unless
--no-header):-- retrofit snapshot v1 opid=<12-hex> engine=postgres engine_version=<major> include-drop=<mode>The banner does not carry the schema name. Its
opidis a content hash of the emitted body, not the cut’s tail opid. Two imports converging on the same schema emit the same banner, which is what makesimport → snapshot → re-importidempotent.Optional DROP prologue, per
--include-dropmode.CREATE EXTENSION statements first (some auto-create schemas).
CREATE TABLE statements in dependency order.
ALTER TABLE statements for constraints, comments, replica identity and defaults: the
Set*family pg_dump round-trips.CREATE FUNCTION / VIEW / MATERIALIZED VIEW statements in dependency order.
CREATE INDEX statements after the tables they belong to.
CREATE TRIGGER / POLICY statements and RLS toggles.
COMMENT ON statements (unless
--no-comments).Data band, one block of embedded row data, opening with a
-- retrofit:snapshot-data v1header. It does not follow item 9: text mode splices it in wherepg_dumpputs its data section, after the CREATE TABLE statements (and any function or view a table needs) and before the constraints, indexes, other views and comments, so the rows land while the tables are bare. It carries every selected record regardless of designation:refrows from the base corpus, which apply with no flags, pluspurpose=seedrows once a--labelnames their dataset. The header names the band, not a designation: a band containing onlyrefrows carries the same header, so a label is not the condition for the band’s existence, only for seed-row inclusion. The band appears whenever at least one record was selected and is omitted entirely otherwise, and opens with its own schema prologue on a non-publictarget (see § Row data below).
JSON mode carries the document as {"type":"snapshot","ddl":"<full doc>"}, with {"type":"ok"} terminal. The ddl field is the DDL
document only: when a data band is selected, text mode splices the
band into the printed document while JSON mode carries it on its own
record (below), so the two modes agree on the DDL portion, not on the
whole byte stream. Other records ride the same stdout stream around
the snapshot record; this is an inventory, not a closed set:
{"type":"snapshot-data","band":"<data band text>"}: emitted before thesnapshotrecord whenever at least one record was selected, and omitted entirely when none was. It is not label-gated: a ref-only corpus emits it with no flags, so a consumer keying on a label would drop reference rows.{"type":"data-excluded","purpose":"<purpose>","table":"<name>", "count":N}: one per (purpose, table) pair that was counted out of the data band; see § Row data. These are emitted after thesnapshotrecord (the artifact is written first so a text-mode redirect keeps stdout clean), sosnapshotis not the last record beforeok.- The same record carries two additional members,
"cause"and"schema", only on an ownership exclusion: a corpus file under a shareddata_rootroot whose header names a schema this run does not own."cause"isother-schemawhen the named schema is registered andunregistered-schemawhen no registered schema owns that namespace;"schema"is the owning namespace. Owned-record rows carry neither member, so existingpurpose/table/countconsumers are untouched. A"count"of0is meaningful: it reports a foreign file that declares a table empty and therefore contributed no rows.
The records print in the order snapshot-data?, snapshot,
data-excluded*, ok; dispatch on type and skip types you do not
recognize.
Render skips do not ride this stdout stream. They are stderr
advisories in both modes, and under --as=json they take the
gateway’s stderr advisory shape ({"level":"warning","code": "view-render-skipped"|"element-render-skipped","message":...}), keyed
on level/code, not type/kind. A consumer reading only stdout
will not see them; see § Render failures for the machine-readable
companion.
snapshot is deterministic: same oplog + same cut + same flags
= byte-identical output. This is load-bearing for CI, parity
checks, and the harness’s roundtrip-* scenarios.
SELECT * views are emitted expanded
A view defined with a star projection comes out with the star replaced by an explicit column list, in the source table’s ordinal order:
-- authored
CREATE VIEW app.vv AS SELECT * FROM app.t;
-- snapshotted
CREATE VIEW app.vv AS SELECT id, a, b FROM app.t;This matches PostgreSQL, which expands and freezes a view’s output
column list when the view is defined; pg_dump shows the expanded
list for the same reason. A qualified t.* expands the same way, and
a star over a join expands source by source in FROM order.
The column list is frozen when the view is defined, and a later ADD COLUMN on the source table never joins it. Adding app.t.c after the
view exists leaves app.vv projecting id, a, b in every later
snapshot; redefine the view to include it.
Not every star can be expanded. When it can’t, the view is skipped
rather than emitted with a bare *; see § A SELECT * view snapshot
cannot expand.
Type references are schema-qualified
Inside a rendered body (a view query, a function body, a CHECK
expression, a column default), a reference to a type you manage in
this oplog is emitted fully qualified and quoted, as
::"schema"."typename". Built-in and extension types (text,
int4, jsonb) stay bare.
This is what lets a snapshot apply to a fresh database without the
caller setting a search_path first: every reference that could
otherwise resolve against the wrong schema names its schema outright.
The qualified spelling is part of the output contract and is
byte-stable across runs.
What snapshot is
snapshot is genealogy-driven: there is no per-operator allow-list. If the op can be replayed, snapshot renders its post-state. Reachability by the replay is the admission test, not membership in a hand-maintained set.
Reachable is not the same as rendered, though: § Render failures names the two ways an element can still fail to come out.
This is why nearly every operator renders on snapshot. The exceptions:
Seal:N/A:log-level. Seal pins log integrity; it doesn’t change the rendered schema. snapshot emits no DDL for it.Drop*andRename*: these don’t have a standalone renderer; they contribute by changing the visible element set the replay produced (one fewer element, or a different name). The snapshot emitter sees their effect via the cut.
What snapshot cannot produce, and why
The cut is the input. What the cut doesn’t reach, snapshot can’t emit. Three situations to know about:
Operators outside the catalog
Statements outside the oplog, whether import skipped them or the catalog has no operator for them, are not in the cut, and snapshot emits nothing for them.
Functions, triggers, policies, extensions and materialized views are in the catalog and snapshot emits them. Standalone sequences, privileges and cross-schema objects are not: import lists them on its skip manifest, and the deployment pipeline applies them.
PG18-gated emissions
At an 18 emit target (--using=postgres:18, or a schema whose
engine_version is 18), snapshot renders SetConstraintEnforced value=false as NOT ENFORCED on the foreign key, emits nothing for
SetConstraintInherit, and writes a VIRTUAL generated column as
STORED. Below 18, a cut holding any of them fails with
unsupported-on-engine-version. PostgreSQL versions
gives every verb’s row at each major.
A SELECT * view snapshot cannot expand
A view whose top-level projection is SELECT * is normally expanded
to an explicit column list (see § SELECT * views are emitted
expanded, above). When that
expansion cannot be performed soundly, snapshot emits no CREATE VIEW for it. The run warns view-render-skipped (see § Render
failures) and exits 0; the view is absent from the document. Expansion
fails when:
- the star’s
FROMis a derived relation (a subquery or a CTE) that carries no fixed base-column list to expand from; - the
FROMnames a relation that is not a live table in the cut. Expansion anchors only on base tables, so this also covers a star over another view: the referenced view has a frozen column list of its own, but that list lives in the referenced view’s definition rather than in a table the cut can read columns from; - the star sits over a coalescing join (
NATURAL JOIN, orJOIN … USING), where the output columns are merged rather than concatenated; - a source column was renamed somewhere in the cut’s history, so a re-expanded star would bind to the new name where the original definition froze the old one.
The remedy is to give the view an explicit output column list. A
CREATE VIEW v (a, b, c) AS … names the final output columns in the
DDL itself, which suppresses the skip entirely: those names are
frozen in the statement and cannot drift. Rewriting the body to name
its columns instead of * works equally well.
Emit-time filters
--table / --exclude-table / --exclude-schema mirror
pg_dump’s -t / -T / -N flags. Names match the registered
name exactly; there is no glob matching (see
pg_dump-parity.md). An unknown name refuses
with table-not-in-cut or schema-not-in-cut.
Interactions:
- When
--schemaand--exclude-schemaname the same schema, the body is empty and the banner remains. --include-drop=scoped+--tableemits per-elementDROPs only for the included tables.- FK reachability is checked against the filtered set: an FK
from an included table to an excluded table refuses with
fk-cross-schema-unreachable. Include the target or drop the FK.
Row data: ref always, --label for the rest
snapshot emits schema-only DDL, with two row-data exceptions. Both draw on the data corpus: every addressed
*.data.<format> data file under the schema’s declared
data_root (currently .data.sql, plus readable legacy *.dml.sql),
discovered automatically; no file argument is needed.
refrows apply from the base corpus. Records classifiedref(production reference data) belong to the current state, so a base-corpusrefrecord is embedded with no flag and is never label-filtered.seedrows are opt-in, and the opt-in is the dataset’s name.--label <label>(repeatable, in layer order) embeds thepurpose=seedrecords carrying that exact label in the data band, between the tables and the constraints and indexes, under a-- retrofit:snapshot-data v1header, so a snapshot can double as a reproducible schema-plus-reference-data fixture. Membership is byte-exact: the selector is not trimmed, and a record with no labels matches no selector. Labels are stamped at import time with--label.
Records carrying any other purpose are excluded, because a snapshot
is current state, not a transition log. Every exclusion is counted
(data-excluded, exit stays 0); a silent run means the corpus held
nothing to drop. This covers unclassified records; seed rows no
--label named (not-selected); rows a higher layer’s replace
floor superseded; and transition and cleanse records. The history-bound purposes are counted but never applied:
retrofit migrate applies them at their anchor op, so their absence
from a snapshot is by design: the count tells you they existed, not
that something went wrong.
A dataset can be left unnamed on purpose: a test-only label such as
fixtures on a snapshot of a long-lived database. Its run-level
apply-risk-data-exclusions warning offers two recourses: --label to
name the dataset, or --allow-unnamed-label <label> to leave it out.
That flag attests that the dataset carrying the exact label was left
unnamed on purpose. It is repeatable and byte-exact, and there is no
form that covers every dataset, so a dataset added later still warns.
The warning drops the attested labels and fires while any other unnamed
dataset remains. Under --as=json the data-excluded record carries
them as unnamed_attested. Every other cause still reports, and -v
still lists the attested tables. An attestation that answers nothing
(the corpus has no dataset with that label, or the run also named it
with --label) prints a note that it had no effect.
In text mode each stderr advisory names the tables and per-table
counts, and carries a hint naming the recourse. Advisories are split
by why the records were dropped, so one purpose can produce more
than one: a dataset no --label named and a dataset a higher layer
superseded are separate lines, because the remedy differs (the second
set was selected; nothing was missing). Under --as=json the counts ride the stdout stream as
data-excluded records. An owned-record exclusion is keyed by purpose
and table, one record per pair with its causes summed. An ownership
exclusion (a record under a shared data_root root whose file header
names another schema) is keyed by purpose, table, "cause" and
"schema" instead, so two foreign schemas claiming one table are two
records rather than one sum. Owned records emit first; the ownership
records follow, sorted on (table, cause, schema).
The data band opens with a schema prologue when the resolved
target schema is anything other than public:
CREATE SCHEMA IF NOT EXISTS "<schema>";It precedes the row bodies inside the -- retrofit:snapshot-data v1 block. On a
public (or default) target the prologue is empty and the band is
byte-identical to a plain row block.
Every run of rows in the band opens under one designation line,
-- Data: <target> (<designation>), so a reviewer can tell reference rows
from seed rows by reading the nearest line above a statement:
-- retrofit:snapshot-data v1
-- Data: "shop"."statuses" (reference)
INSERT INTO "shop"."statuses" ("status_id", "name") VALUES (1, 'new');
-- Data: "shop"."fixtures" (seed, label qa:golden)
INSERT INTO "shop"."fixtures" ("id", "kind") VALUES (1, 'golden');The target is the table the rows load, in the two-quoted-halves spelling the
DDL header uses. The designation is reference, seed, label <label>,
transition, cleanse or unclassified. One line opens each maximal run of
consecutive groups for one table, purpose and layer; a table interleaved across
layers gets one line per run. The line is an ordinary comment, never a
-- retrofit: marker: --no-comments and --no-header leave it, and under
--as=json it rides inside the snapshot-data record’s band text.
The band carries no SET search_path. The renderer resolves every
bare relation reference in a data body when it emits it, naming the
relation’s schema at the emission cut, so an emitted body does not
depend on the applying session’s search path. The stored data file is
unchanged by that: authors still write bare identifiers, and the file
on disk keeps them.
The CREATE SCHEMA line survives, because qualification resolves an
identifier but does not create the namespace that identifier names. It
is dropped from the text document when the DDL immediately above
the band already emitted it for that same schema. That is the usual case, and
repeating it would make every snapshot --label … | psql print
NOTICE: schema "…" already exists, skipping. It is retained
whenever nothing above the band creates the schema: in the
snapshot-data JSON record, which is contracted to be extractable on
its own (applied without the DDL above it, a band missing its
CREATE SCHEMA would fail with 42P01), and under --data-only,
which emits no DDL byte at all on either its text or its JSON arm.
Row blocks are versioned by anchor position: within one
(table, label-set) chain, the latest disposition=replace block
at-or-before the render cut supersedes earlier state, so
snapshot --at renders the row set as it stood at that cut.
Before a selected record is emitted, snapshot repairs its references
as migrate does, in the emitted body only: a table or column rename
resolves to its name at the cut, a write-only reference to a dropped
column can be removed, and records for a dropped table are excluded. A
reference that cannot be repaired without changing its meaning refuses
as stale-dml-reference; --allow-stale-dml overrides it. The data
file on disk is unchanged. See
Stale data references and repair.
Datasets layer: each --label occurrence names one layer, in the
order given, 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. External sources are no
longer layered by flag: the corpus under the schema’s declared
data_root is always read, so committing a file is what makes it
available.
A full snapshot renders the band as plain row bodies: the DDL above
it has just created every table empty, so no floor needs a prologue
and none is emitted. The rows carry their own identity and serial
values, so after a table’s last row the band re-seats each of its
identity/serial sequences with an annotated SELECT … setval( pg_get_serial_sequence(…), …) guarded on max(<column>) IS NOT NULL,
the alignment pg_dump’s data section performs, and the first insert
that omits the key takes the next free value instead of colliding with
row 1. Each re-seat is a query, so applying the script prints one
single-row result per sequence under the column name setval, as
pg_dump’s own setval line does; psql -q does not suppress query
results, psql -o /dev/null discards them.
snapshot --data-only lands the band on an existing database instead.
There a table whose resolved floor is a disposition=replace block
carries a prologue, so the rows land as the table’s complete state,
and the prologue renders exactly as it does on migrate: a keyed table
(its primary key, a UNIQUE constraint or a unique index the captured
rows write in full; a DEFERRABLE constraint never qualifies) gets a
staged block that upserts through a TEMP
table and never empties the target; a table with no such key gets a
whole-table clear and a staged reload, or refuses as
stale-dml-reference (reason key-uncovered) when another table’s
foreign key references it; a
unique index that shares its columns with a DEFERRABLE UNIQUE
constraint refuses as replace-no-upsert-form; and a body=none floor
renders a bare DELETE FROM <table>; clear. Clears render children
before parents; an unreadable cut degrades the order to a deterministic
tie-break behind a -- WARNING: comment. Every row body is followed by
the same sequence re-seat, inside the staged block when there is one;
see docs/migrate.md for the key order, the marker causes and each
refusal’s recourse. Apply with psql -v ON_ERROR_STOP=1 -1 -f <script>
under a role holding the TEMP privilege. 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. Re-applying the
same band to a converged target changes no row: the band is idempotent
on its success path, and the recovery from a failed apply is to re-run
the same bytes once the blocking condition is cleared. The staged
block’s DROP TABLE IF EXISTS opener absorbs a temp table a failed run
left behind.
That prologue is where snapshot --data-only departs from
pg_dump --section=data: pg_dump’s data section assumes an empty
target, while this band asserts the captured state over whatever the
target holds. The band is not atomic across tables unless applied
inside one transaction; that is stated as a stderr advisory, never as
a refusal. The snapshot-data record’s prologue member reads
delete only when a prologue was actually emitted, so a full
snapshot’s band reports none.
The data band is assembled in a pre-flight pass before any DDL byte is written, so a malformed corpus file aborts cleanly with no partial output.
Order and duplicates. Rows are emitted in a deterministic total
order: anchor-op position first, then layer rank (the --label order),
then file path, then position within the file. Records identical in {anchor op, canonical body, label set} are coalesced to one, so the same row present in both the
base corpus and a second layer is emitted once. Two records that
re-state the same row divergently at one anchor are a hard
dml-data-conflict abort, never a silent pick.
The order is corpus order, not an FK topological sort. In a full
snapshot the foreign keys are added after the band, so a child row
ahead of its parent still loads, and a child row with no parent
anywhere in the band fails at the post-data ADD CONSTRAINT. A
snapshot --data-only band lands on a database whose foreign keys
already exist, so there a child row ahead of its parent fails at its
INSERT against an immediate foreign key. Either way the band fails at psql -f apply time rather
than at snapshot time. For seed data with FK dependencies, the anchor
positions set the order.
Render failures: skip vs abort
A state the replay reaches can still fail to render, and the two layers behave differently.
Element-level: skip and continue, exit 0. When one element’s
body cannot be reconstructed from the cut, snapshot omits it, finishes
the document and exits 0. Each skip prints one warning on stderr
(warning: <code>: <message>, or {"level":"warning","code":"<code>",...}
under --as=json); read them before applying the output. The split is
by element type, not by cause:
view-render-skipped: a view or materialized view whose body cannot be reconstructed. Views take this code for every render refusal, including one caused by a reference that is not live as of the cut, so a view never surfaces under the element code below. Because the code does not name the cause, the remedy depends on which one the message describes. When the refusal is an output-column reconstruction failure (a projection whose column labels can’t be derived), emit the view’s frozen column list explicitly. When it is a missing or non-live reference (a tombstoned or not-yet-live object the body names), no column list helps: restore the referenced object into the cut, or rewrite the view to stop referencing it. Recreating the view by hand after applying works for either.element-render-skipped: a function or trigger whose body references an element that is not live as of the cut. At an interior cut this is often the correct historical answer, not a defect; at--at lastit points at a real gap.
Forest-level: abort, non-zero exit. A failure to materialize the
cut at all is fatal and emits no DDL: render-blocked-on-unknowns,
render-blocked-on-partial-forest, render-blocked-on-replay-error.
--skip-manifest <path>
--skip-manifest <path> streams the element-level skips above to
<path> as JSONL: the machine-readable companion to the stderr
warnings, sharing import’s record shape. Records carry at least
kind, name, schema, raw_stmt_kind, line, and raw_text;
line and raw_text are import-time fields with no emit-time
meaning, so on a snapshot skip they are always 0 and "" rather
than absent.
The manifest’s kind values are view-render and
element-render, deliberately not the same spellings as the
stderr warning codes view-render-skipped and
element-render-skipped above. The two surfaces describe the same
skips under different vocabularies, so a filter written against the
warning codes matches nothing in the JSONL. Key the manifest on the
bare kind values.
An element_id naming the tombstoned element rides the
element-render path and is omitted whenever
it is unset: always on view-render records, and on any
element-render record whose skip supplied no id. Read it by
presence, never by position. src_file is omitted entirely, since
an emit-time skip has no source file. Treat the field set as open
and tolerate keys you don’t recognize. The flag does not change exit
status: a run whose only issue is skipped elements still exits 0.
The file is written whenever the run actually reaches the manifest
step, including with zero skips (you get an empty file), so its
existence is not evidence that anything was skipped, and its absence
is not evidence that nothing was. The write sits near the end of the
run, after the DDL has been emitted, so anything that returns before
it leaves no file: an empty or header-only oplog short-circuits well
before the render, and a refusal along the way (a bad flag
combination, an unresolvable cut, a forest-level render-blocked-on-*
abort) ends the run without a manifest.
Because the write is an atomic rename, a path naming a
project-managed file (the active oplog or the resolved
configuration file) would destroy it, so that collision refuses with
skip-manifest-clobbers-managed-file on any snapshot that reaches
the manifest step, not only one that had something to record. The
guard follows config resolution rather than the default filename, so
it covers a custom path selected by --config or RETROFIT_CONFIG,
not just a project-root retrofit.toml. The check runs at write time
rather than at flag-parse time, so the DDL document has already been
emitted to stdout when it fires: the refusal protects the managed
file, not the run’s output. Point it
somewhere else.
–using: one-shot emit target
--using=<engine>:<major> overrides the engine version for one
invocation. Resolution chain when --using is absent:
[[schema]] engine_version (per-schema override) >
[project] engine_version (project default). The flag’s
<engine> must match the project engine; <major> must be in
this build’s supported set.
--using is read-only: it does not write the pin to
retrofit.toml. For a durable change use
retrofit schema engine <version> or edit retrofit.toml.
Failure modes
opid-not-found: the--atopid prefix doesn’t resolve.schema-not-in-cut:--schemaor--exclude-schemanamed a schema the cut doesn’t contain.table-not-in-cut:--tableor--exclude-tablenamed a table the cut doesn’t contain.fk-cross-schema-unreachable: an FK from an included table to an excluded table; pick a different filter set.flag-conflict: incompatible flag combination (e.g.--include-drop=schemawith--table).unsupported-on-engine-version: the cut contains ops the resolved engine version cannot emit (e.g. PG18 ops with a PG17 pin).unknown-engine-name:--usingnamed an engine that doesn’t match the project engine.unknown-engine-version:--usingnamed a version not in this build’s supported set.invalid-include-drop:--include-dropvalue is notnone/scoped/schema.data-path-missing: the schema’s declareddata_rootdirectory is absent.data-file-unreadable: a corpus data-file path is missing or unreadable. The envelope names the path.data-file-malformed: a corpus data file source violates the data-file grammar (e.g. an out-of-enumpurpose=value). Aborts in pre-flight, before any DDL is written.dml-data-conflict: two divergent versions of one row at one anchor. Hard abort; the envelope names both files.data-unavailable-at: an--atcut where a chain’s surviving versions all anchor later than the cut.label-empty:--labelwas passed empty or whitespace-only.skip-manifest-clobbers-managed-file: the--skip-manifestpath is the active oplog or the resolved configuration file; see §--skip-manifest <path>.render-blocked-on-unknowns/render-blocked-on-partial-forest/render-blocked-on-replay-error: the cut could not be materialized; see § Render failures. Distinct from the element-level skips there, which exit 0.
Scope contract
snapshot’s in-scope set is every replayable operator: if an
op is in the replay engine, snapshot renders its post-state (see
§ What snapshot is), subject to the element-level render skips in
§ Render failures. The full per-operator inventory is in
smo-catalog.md.
The pg_dump option-by-option parity audit lives in
pg_dump-parity.md.
Related
concepts.md: oplog, cut, replay, genealogy.pg_dump-parity.md: flag-by-flag audit of snapshot’s pg_dump alignment.drift.md: the inverse direction, reconciling an oplog to an out-of-band schema.migrate.md: emit a forward migration between two cuts (different from snapshot’s single-cut emission).smo-catalog.md: every operator with status.cli-output-contract.md: per-verb stdout / stderr / exit-code shape.