Getting started
A 5-minute walkthrough: install retrofit, build (or bootstrap) a small schema, look at the oplog file you just created, and emit DDL.
There are two ways to start an oplog:
- From scratch: register a schema and append operators by hand, one DDL change at a time. Best when you’re designing a new schema or learning retrofit. Jump to Track A.
- From an existing
.sqlfile: bootstrap an oplog from hand-written DDL or apg_dump --schema-only.importwalks the SQL, mints equivalent operators, and gives you an oplog you can evolve from there. Jump to Track B.
The two tracks share init, snapshot, and op seal; only the
middle differs.
If you want the model first and the commands second, read
concepts.md before this page. If you want the rules of
the road, read principles.md.
Install
Releases are prebuilt binaries for Linux and macOS, on amd64 and arm64. Other platforms, Windows included, have no release build.
Install script
curl -fsSL https://retrofit.sh/install | shThe script installs the latest release to $RETROFIT_INSTALL_DIR, else
~/.local/bin, without sudo. It checks the archive’s SHA-256 against the
release’s checksums.txt before extracting anything. Pass flags after
sh -s --:
curl -fsSL https://retrofit.sh/install | sh -s -- --version 0.10.0 # one release
curl -fsSL https://retrofit.sh/install | sh -s -- --wip # the newest WIP build--wip lists releases through the GitHub API; set GITHUB_TOKEN if that
is rate-limited.
Homebrew (macOS)
brew install --cask MonotonicHQ/tap/retrofitMonotonicHQ/tap/retrofit@wip tracks the newest WIP build instead. The
two casks conflict, so install one or the other; to switch, uninstall the
one you have first. The cask also installs the man pages and the bash,
zsh and fish completions.
Manual download
Each release carries one archive per platform,
retrofit_<os>_<arch>.tar.gz, where <os> is linux or darwin and
<arch> is amd64 or arm64. An archive holds the retrofit binary,
LICENSE, THIRD_PARTY_NOTICES, a man page per command under
man/man1/, and the bash, zsh and fish completion scripts under
completions/. The release’s checksums.txt lists
the SHA-256 of every file it carries, so check the line for the archive
you downloaded rather than the whole file. For the latest release on
Linux amd64:
base=https://github.com/MonotonicHQ/retrofit/releases/latest/download
curl -fsSLO "$base/retrofit_linux_amd64.tar.gz"
curl -fsSLO "$base/checksums.txt"
grep ' retrofit_linux_amd64.tar.gz$' checksums.txt | sha256sum -c # macOS: shasum -a 256 -c
tar -xzf retrofit_linux_amd64.tar.gz retrofit
mkdir -p ~/.local/bin && mv retrofit ~/.local/bin/Notes
- The directory holding the binary (
~/.local/binby default) must be onPATH, or the shell reportscommand not found. The install script prints theexport PATH=...line to add to your shell profile when it is not. - The macOS binaries are signed and notarized, but a bare binary cannot carry its notarization ticket, so its first run needs the network: Gatekeeper fetches the ticket then.
Verify
retrofit --versionFor shell completions, retrofit completion <bash|zsh|fish|powershell>
prints a script to source (run it with --help for install steps).
A new project
mkdir myschema && cd myschema
git init
retrofit initinit writes two files:
retrofit.toml: project config. Engine, version, search path, schema registry. More[[schema]]rows get added as you register schemas..gitattributes: marks*.oplogas text so git diffs them cleanly, and marks*.data.sqltext eol=lfso a checkout rewrite (e.g.core.autocrlf=true) does not touch data file line endings.
# retrofit.toml
schema_version = "1"
[project]
engine = "postgres"
engine_version = "17"
[git]
# warn when `.gitattributes` lacks `*.oplog merge=union text eol=lf`.
warn_missing = true
[search_path]
schemas = ["public"]PostgreSQL 15, 16 and 17 are supported, and 17 is the default. 18 is
accepted as an explicit pin but not supported: import and drift read
PostgreSQL 17 syntax only. PostgreSQL versions
defines each level and says what every verb does at each major. The project
value is the default. Use schema create --engine-version,
import --engine-version, or schema engine to set a per-schema pin or emit
target.
retrofit init --no-gitattributes scaffolds warn_missing = false
instead, so a project that deliberately declined the merge-driver line
is not warned about that same decision on every later command.
Track A: start from scratch
You’ll register a schema, append a handful of operators, snapshot, rename a column to see identity preservation in action, and seal.
Register a schema
retrofit schema create usersThis is the first command that touches the oplog. The CLI prints the resolved schema to stderr and the appended op to stdout:
note: schema-context: users
+ <schema-op> CreateSchema usersThis page writes <schema-op>, <users-id> and similar placeholders
where your terminal shows 12-character hex. Opids and ElementIDs are
random draws, and a seal digest covers bytes that contain them, so your
values differ on every run. The shape does not: which slot holds what,
and which reference points where.
Look at the file it just minted:
cat users.oplog# retrofit-oplog-format: 1
# retrofit-model-version: 1
# engine-version: 17
CreateSchema(<schema-op>, prev=000000000000, id=<users-id>, name="users")That’s the entire on-disk format: three header lines, then one operator
per line. <schema-op> is the opid, a 12-character hex id drawn
at random when the op is minted. prev=000000000000 is the predecessor
opid; the first op in a schema points at the all-zero null.
id=<users-id> is the ElementID of the schema element this op
minted. Every element an op creates has one, and it stays the same for
the element’s whole life.
schema create also added a [[schema]] block to retrofit.toml
pointing at the new oplog file (<name>.oplog, derived from the
schema name), and auto-pinned it as the project default:
schema_version = "1"
default_schema = "users"
[project]
engine = "postgres"
engine_version = "17"
[git]
# warn when `.gitattributes` lacks `*.oplog merge=union text eol=lf`.
warn_missing = true
[search_path]
schemas = ["public"]
[[schema]]
name = "users"
engine_version = "17"
# oplog_root = "." # directory holding this schema's oplog (the file is <name>.oplog)
# data_root = "data" # directory this schema's data files are minted intoEvery optional key is scaffolded as a comment naming its default and
what moving it moves, so the default applies by absence: the file
teaches you the knobs without pre-committing you to any of them. Here
oplog_root is commented because the oplog landed in the project root,
which is its default; schema create <name> <directory> writes the key
uncommented when you name somewhere else.
data_root is where this schema’s data files live: the .data.sql
files import preserves and migrate splices back in. It is scaffolded
commented; unset, it inherits [project] data_root, then defaults to
data under the project root. Uncomment it to site the corpus somewhere
else.
Beside data_root, a [[schema]] block may carry
data_band_max_bytes = <integer>: the largest source band, in bytes per
table, that data capture, import and drift will ingest for that
schema. A band is the sum of every INSERT statement naming the table in
one source file; a table over the bound refuses data-band-over-cap
before anything is written. Absent, a built-in default applies; there is
no unlimited setting. 0, a negative value, or one too wide for a 64-bit
integer refuses config-invalid at load; a non-integer or a quoted
"4096" refuses config-malformed. The key is per-schema only.
Reference data (rows applied on every migration, not one-time seed) is
declared in the oplog, not in config: retrofit schema ref-tables add <table>
mints a SetReferenceData op, and import / drift take --ref <table>.
See import.md § “the data file”.
default_schema is auto-set to the first registered schema. In a
single-schema project you never type --schema again: every verb
resolves to this schema via the resolver
chain.
Add some operators
retrofit op add CreateTable people
retrofit op add AddColumn people id bigint identity=by_default
retrofit op add AddColumn people email text not_null=true
retrofit op add AddPrimaryKey people --column people.id name=people_pkeyEach op add prints the resolved schema to stderr and one line
per appended op to stdout:
note: schema-context: users
+ <table-op> CreateTable users.people
note: schema-context: users
+ <id-op> AddColumn users.people.id
note: schema-context: users
+ <email-op> AddColumn users.people.email
note: schema-context: users
+ <pk-op> AddPrimaryKey users.people.id (constraint people_pkey)The + prefix means appended. The opid is what you’d reference if
you wanted to revise or remove this op later.
The oplog file now reads:
# retrofit-oplog-format: 1
# retrofit-model-version: 1
# engine-version: 17
CreateSchema(<schema-op>, prev=000000000000, id=<users-id>, name="users")
CreateTable(<table-op>, prev=<schema-op>, in=<users-id>, id=<people-id>, name="people", reference_data=false)
AddColumn(<id-op>, prev=<table-op>, in=<people-id>, id=<col-id-id>, name="id", type="bigint", not_null=false, identity=by_default)
AddColumn(<email-op>, prev=<id-op>, in=<people-id>, id=<col-email-id>, name="email", type="text", not_null=true)
AddPrimaryKey(<pk-op>, prev=<email-op>, in=<people-id>, id=<pk-id>, columns=[<col-id-id>], name="people_pkey")Each line: operator name, opid, predecessor, target/payload. The
prev= chain walks straight back up the file, and every op binds to
its target by ElementID, not by name: AddPrimaryKey knows its
column as <col-id-id>, the id AddColumn minted, not as id.
That’s how renames preserve identity (next section).
Get DDL
retrofit snapshot-- retrofit snapshot v1 opid=290cfe80f1b4 engine=postgres engine_version=17 include-drop=none
--
-- Schema: "users"
--
CREATE SCHEMA IF NOT EXISTS "users";
--
-- Table: "users"."people"
--
CREATE TABLE "users"."people" (
"id" bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"email" text NOT NULL,
CONSTRAINT "people_pkey" PRIMARY KEY ("id")
);The snapshot is deterministic: the same oplog, engine version and
flags produce the same SQL, byte for byte, on every machine. The
banner’s opid= is not an oplog opid. It is a hash of the DDL body
below it, so two oplogs that converge on one schema carry the same
banner opid=, a flag that changes the body changes it, and it is the
one hash on this page your terminal will match. The rest of the banner
echoes the run’s options.
Rename, the point of all this
retrofit op add RenameColumn people.email email_addressnote: schema-context: users
+ <rename-op> RenameColumn users.people.email
warning: apply-risk-rename-in-use: renaming column "users.people.email" breaks readers still using the old nameThe warning: line is an advisory, not a refusal: the op was
appended, and the exit code is unchanged. The rename is recorded
against the column’s identity. The advisory is about what lies outside
the oplog: an application still selecting email fails once the
rename reaches the database. Advisories ride stderr for op add, keyed by a stable code you can switch on
(apply-risk-rename-in-use); the message text is not part of the
contract. See cli-output-contract.md §
“Advisory note codes” for the full inventory and the JSON shape.
An advisory never fails a run by itself. To make CI fail on the
lock-taking hazards before they reach a populated table, add
retrofit validate --fail-on-apply-risk as the CI step: it evaluates
the migration window since the latest Seal (the one migrate emits by
default) and exits 2 when that window carries a validating
AddForeignKey or AddCheck, or an AddNotNull, on a table that may
hold rows. A table created earlier in the same window, with no opaque
op (RunRawScript) run since, is empty and is not counted, which is
why the gate needs a Seal marking what production already carries.
Without one, validate also names the unsealed window
(apply-risk-window-unsealed); that note never changes the exit and
can accompany counted hazards. apply-risk-add-unique and
apply-risk-rename-in-use stay advisory.
The new oplog line:
RenameColumn(<rename-op>, prev=<pk-op>, on=<col-email-id>, name="email_address")on=<col-email-id> is the same ElementID minted at AddColumn. The
column’s identity is preserved. AddPrimaryKey (which still references
that column id) doesn’t break, doesn’t need rewriting, doesn’t get
confused with a drop-then-add. Snapshot now emits "email_address"
where it used to emit "email":
retrofit snapshotCREATE TABLE "users"."people" (
"id" bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"email_address" text NOT NULL,
CONSTRAINT "people_pkey" PRIMARY KEY ("id")
);The two end states alone cannot tell this rename from dropping email
and adding email_address, a pair that loses the column’s rows. The
oplog records which one happened.
Seal what you’ve shipped
While the oplog tail is unsealed, you can edit it freely
(op revise, op remove, even editor-buffer line deletion). Once
you’re ready to call a range “done” (published, deployed, depended on
by other people), you seal it:
retrofit op sealnote: schema-context: users
sealed 6 ops as <seal-op>A Seal op gets appended at the end of the oplog. It pins a Merkle
digest over the sealed ops’ canonical bytes, so editing any sealed op
in place changes the digest and breaks verification.
Seal(<seal-op>, prev=<rename-op>, range=[{first=<schema-op>, last=<rename-op>, lines="6", digest="<seal-digest>"}])After seal, op revise on a sealed op is rejected. New ops still go on
the unsealed tail behind the seal.
The tail is editable because nothing depends on it. That makes sealing
a range the thing you do before anyone else pulls it and applies it to a
database. If a teammate applied an unsealed op and you then op revise
it, the opid re-mints: the op they applied is no longer on the log, and
their database can’t be placed on it. The recourse is to rebuild that
database from the log (retrofit migrate --from root --to last
against a fresh database), not to reconcile it with retrofit drift,
which would adopt the revision backwards.
Track B: bootstrap from existing SQL
If you already have a schema (hand-written DDL, a pg_dump --schema-only of a live database, a file checked in next to your
application), retrofit import walks it and mints the equivalent
oplog. From there you evolve the schema with the same op add /
snapshot / seal workflow as Track A.
Start the same way as Track A, with init and a git init if you skipped
those above:
mkdir myschema && cd myschema
git init
retrofit initSuppose you have a sales.sql file:
CREATE TABLE customers (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
email text NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE orders (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
customer_id bigint NOT NULL REFERENCES customers(id),
placed_at timestamptz NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX orders_customer_id_idx ON orders (customer_id);Import with --schema sales. If sales isn’t registered yet,
import lazy-creates it (mints the oplog, appends [[schema]],
auto-pins it as default if it’s the first schema):
retrofit import --schema sales sales.sqlimport prints the resolved schema to stderr, one line per minted
op to stdout, and a terminal summary:
note: schema-context: sales
+ <op> CreateTable sales.customers
+ <op> AddColumn sales.customers.id
+ <op> AddColumn sales.customers.email
+ <op> AddNotNull sales.customers.email
+ <op> AddPrimaryKey sales.customers.id (constraint customers_pkey)
+ <op> CreateTable sales.orders
# (orders columns, NOT NULLs, PK, FK elided for brevity)
+ <op> CreateIndex sales.orders.customer_id (index orders_customer_id_idx)
warning: 4 apply-risk ops minted (validating constraints / row-touching alters); review before applying to a populated DB; by kind: add-not-null×3, validating-fk×1
imported 14 ops; skipped 0 statementsAs in Track A, <op> stands in for a real 12-char opid; only the
column of operator names and targets is stable across runs.
The warning: line lands on stdout, interleaved just before the
summary: import’s advisories are part of its stream, not a stderr
aside (the carve-out is spelled out in
cli-output-contract.md). It is emitted once
per run, never once per op, and it tallies the minted ops that will be
slow or locking to apply against a populated database (here the three
AddNotNulls and the validating AddForeignKey). Bootstrapping into an
empty database, it costs nothing; replaying against live data, it is
the list to read first.
Each line names the most specific element the op touches:
CreateTable lands on <schema>.<table>, AddColumn on
<schema>.<table>.<column>, and constraint ops on the column they
bind. Where the constraint carries a name, it rides as a parenthesized
annotation after the target. A constraint written
without one in the source (PRIMARY KEY (id), REFERENCES customers(id)) still gets annotated, because import derives
PostgreSQL’s own default (customers_pkey,
orders_customer_id_fkey). What has no name to show, like
AddNotNull, points at the column alone, and stays that way: it sets
the column’s NOT NULL bit rather than minting a named constraint, so
there is no name slot to fill in later. CreateIndex follows the
same rule with the index name.
(If you’d rather scaffold the schema first, retrofit schema create sales is the rich-form authoring path; see §Multiple
schemas below.)
From this point on the oplog is the source of truth, not the original
.sql. Commit sales.oplog (and any other registered oplog files;
each schema gets <name>.oplog) into git; treat the source SQL as a
starting fixture you imported from, not a thing to keep in sync by
hand.
Multiple .sql files in one import
If your schema is split across several .sql files (e.g.
01_tables.sql, 02_indexes.sql, 03_views.sql), pass them all to
one import invocation. Shell globs work directly:
retrofit import --schema sales schema/*.sqlAll sources import as one batch in argv order, with one validate and one atomic write, so a file that fails to parse or validate leaves the oplog untouched. The result matches importing the files one at a time in the same order, apart from the minted ids.
import reads sources in argv order without re-sorting. If
your filenames are Flyway / Liquibase-style (V1__a.sql,
V10__b.sql, …), a bare shell glob silently misorders them (V1, V10, V11, V2). See import.md § Source order
for the version-sort recipes.
Verify the round-trip
Snapshot the new oplog and compare it with your source:
retrofit snapshotWhat retrofit cannot model as operators is reported on the skip-manifest (below), not dropped.
Skipped statements
The import vocabulary covers tables, columns, NOT NULL / CHECK /
PK / FK / UNIQUE constraints, indexes, identity & generated columns,
views, materialized views, functions, triggers, policies, extensions,
DO blocks, and table/column comments. It does not cover
standalone sequences, privileges, or cross-schema objects. Those are
reported on stdout and never written to the oplog:
imported 12 ops; skipped 2 statements (1 sequences, 1 GrantStmt)Pass --as=json to retain the per-statement skip detail.
Custom DML (INSERT/UPDATE/DELETE/MERGE) is not merely
reported: import sets it aside verbatim in a per-table data file
(<schema>.<table>.<YYYYMMDDHHMM>….data.sql) under the schema’s
data_root. migrate splices it
back into a migration automatically, and snapshot --label <l> embeds its
seed rows. See import.md § “the data file”.
For a complete list of what import does and doesn’t recognize, run
retrofit import --help-all.
Objects your project does not own
A dump from a managed database carries objects your application does not
author: a migration runner’s history table (Diesel’s
__diesel_schema_migrations), extensions the hosting provider installs
(pg_stat_statements), schemas it creates (auth, vault). Declare them
once in retrofit.toml and every verb that reads SQL sets them aside:
[[exclude]]
kind = "table"
schema = "public"
name = "__diesel_schema_migrations"
reason = "Diesel migration history; written by the migration runner"
[[exclude]]
kind = "extension"
name = "pg_stat_statements"
reason = "installed by the hosting provider"kind is table, extension or schema; name and schema are the stored
identifiers exactly as the catalog holds them (case-sensitive, never quoted, no
globs); reason is required. import, identify, drift,
migrate --from-dump and data capture remove every statement the entry owns
before anything reads the input, and report each entry they applied on stderr:
~ (excluded) table public.__diesel_schema_migrations — 2 statement(s) — Diesel migration history; written by the migration runnerThe set is project state: it is read from the retrofit.toml that locates your
oplogs, no flag adds to or overrides it, and the registry-less -f mode
applies none. A kept statement that depends on an excluded table or extension
(a foreign key into it, a view reading it, a call to its functions) refuses
exclusion-dependent before anything is written, and an entry that names an
object your oplog already holds refuses exclusion-in-log; the oplog is never
edited. When identify or drift reports an object you do not own as
divergence, declare it here. See drift.md and
migrate.md for the verbs’ side.
Continue from here
You now have an oplog you can evolve. Append new ops the same way as
Track A (retrofit op add ...), and you can rename, drop, and
restructure with full identity preservation, even though you didn’t
type the original CreateTable calls yourself.
A common next step after a clean import is to seal the imported prefix, so the bootstrap is a stable baseline and only your subsequent changes live on the unsealed tail:
retrofit op sealYou can also pass --seal to import to seal in one step:
retrofit import --schema sales sales.sql --sealBare --seal derives the Seal reason from the source basename
(reason="imported sales.sql"); pass --seal="<text>" to override
with a custom string (e.g. --seal="bootstrap").
Multiple schemas
A retrofit project can hold any number of schemas. Each is a row
in [[schema]] with its own oplog file (<name>.oplog). The CLI
picks “which schema does this verb operate on?” via a six-step
resolver chain; the common
cases are zero-ceremony.
Scaffolding more schemas
The rich-form authoring verb is schema create. Pass an optional
<location> (dir or trailing-/) and any project-setup
flags:
retrofit schema create billing # → ./billing.oplog
retrofit schema create sales ./services/sales/ # → ./services/sales/sales.oplog
retrofit schema create audit ./audit/ --engine-version 17 # → ./audit/audit.oplogFor a monorepo where each schema’s source code is colocated with
its oplog, dirname([[schema]].path) is an implicit scope dir:
running any schema-context verb from inside services/sales/
resolves to sales automatically. No further configuration needed.
Explicit scope: [directory_scope]
When source dirs and oplog dirs don’t line up, scope the schema to
arbitrary directories with --scope on schema create, or with
schema scope add later:
retrofit schema create sales ./services/sales/ \
--scope ./services/sales \
--scope ./src/sales \
--is-defaultThis writes a top-level [directory_scope] table:
[directory_scope]
"services/sales/" = "sales"
"src/sales/" = "sales"Now from src/sales/handler.go’s directory, retrofit op add ...
resolves to sales without --schema. Add or remove entries
later:
retrofit schema scope add ./tests/sales --schema sales
retrofit schema scope remove ./src/salesschema scope add runs the resolver chain, so from inside an
existing scope dir --schema is optional. schema scope remove
takes only the directory key (the schema name is recovered from
the entry).
Switching the default
default_schema is auto-pinned to the first schema you create.
To re-pin it without hand-editing TOML:
retrofit schema set-default sales
retrofit schema set-default --unset # remove the key entirelyAfter --unset, schema-context verbs must resolve via another step
of the chain (--schema <name>, -f <path>, a [directory_scope]
entry, or the implicit oplog-dir match); otherwise they refuse with
default-schema-unset.
Moving a schema’s oplog
If a project layout changes and the oplog needs to live elsewhere,
schema move does the paired filesystem rename + oplog_root
rewrite atomically:
retrofit schema move ./different/ --schema salesThe oplog lands at ./different/sales.oplog; the filename is
always re-derived as <name>.oplog at the destination, never stored.
The opids, seal digests, and oplog bytes are unchanged: the file
is renamed, not rewritten. [directory_scope] entries (which point
at schema names, not paths) are untouched.
Overrides and ad-hoc inspection
--schema <name>on any schema-context verb forces a specific schema (mutex with-f).-f <path>is the path-as-identity selector. In a project, it looks up[[schema]]by path (refuses withunregistered-pathif no row matches). Outside a project (noretrofit.tomlwalkable), it opens the file directly: the registry-less ad-hoc inspection mode.To read an arbitrary oplog file from inside a project (say a copy of a previous revision extracted with `git show HEAD~1:sales.oplog
/tmp/old.oplog
, to reverse a window the log no longer carries), combine-Cwith-f`:retrofit -C /tmp -f /tmp/old.oplog migrate --from seal --to last --downRegistry-less mode keys off the working directory, not off the target, so
-Cmust name a directory with noretrofit.tomlwalkable above it (a sibling temp dir works; a subdirectory of the project does not). The-ftarget itself may live anywhere, including inside the project. Dropping-fdoes not enter this mode: with a walkable registry it resolves back to the registered oplog, which is a different file.--chdir, -C <path>reroots cwd for the entire run: resolver, config lookup, discovery walk, and any relative paths the verb receives.RETROFIT_REQUIRE_EXPLICIT_SCHEMA=1makes schema-context verbs refuse withexplicit-schema-requiredunless--schemaor-fwas passed. It is a CI hardening knob: set it in pipelines that should never silently fall through todefault_schema.
Inspect resolution
retrofit validate enumerates the resolved scope mappings the
chain will use:
retrofit validateresolved scope:
services/sales/ → sales (directory_scope)
src/sales/ → sales (directory_scope)
services/billing/ → billing (implicit-oplog-dir)
audit/ → audit (implicit-oplog-dir)The same directory can appear from both sources; explicit
[directory_scope] wins on equal-length matches.
Where to go next
- Concepts: the mental model: oplog, snapshot, ElementID, sealed/unsealed, replay.
- Principles: the load-bearing rules retrofit upholds.
- CLI output contract: what every verb prints, when, and in what shape. Reference for scripting against retrofit.
- SMO catalog: every operator in the catalog, with its status.
Per-verb references:
- import: bootstrap an oplog from
.sqlfiles; what’s in vocabulary, what’s skipped. - drift: reconcile an oplog with an out-of-band schema; what drift appends on its own and what it asks; data-motion authoring path.
- snapshot: emit canonical DDL for a cut;
pg_dump --schema-only-shaped output. - migrate: emit a migration script between two
cuts (forward, or reverse with
--down); in-place vs--into-schema; refusal envelope.
Other verbs:
retrofit reconcile: collapse concurrent forks in the oplog onto a linear tail (non-interactive;--keep/--swappick the bias,--clean/--accept-dirtypick the strictness). See § Forks in concepts.md, and Recovering from a merge for which command clears each refusal a merged fork produces.retrofit identify <dump>: report which cut apg_dump --schema-onlyis at (Exact / Ambiguous / None); the resolver behindmigrate --from-dump. Add--expect <ref>to turn the report into a gate: it exits 0 only when the dump is at the cut you name, and refusesexpect-mismatchwhen it is at some other valid cut.--expecttakes the same references asmigrate --from(opid prefix, or the anchorslast/sealwith an optional^/~N), exceptroot, which no dump is identified at and which refusesexpect-unidentifiable. A dump of an empty database, one without the schema, refusesfrom-dump-schema-absentand points atretrofit migrate --from root, which creates it. A deployment check is one command:retrofit identify db.sql --expect lastretrofit op reviseandop revise -i: edit the unsealed tail.retrofit op unsealandop truncate: demote seals / rewind the tail. Both are state-rewinding and take no consent flag: the command is the attestation, and each reports the Seals it removed (op truncatepeels Seals with--drop-seals).
Run any verb with --help for its grammar and flags, or --help-all for
the extended vocabulary, semantics, and caveats.