retrofit0.10.0-wip.8

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 .sql file: bootstrap an oplog from hand-written DDL or a pg_dump --schema-only. import walks 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 | sh

The 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/retrofit

MonotonicHQ/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/bin by default) must be on PATH, or the shell reports command not found. The install script prints the export 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 --version

For 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 init

init writes two files:

  • retrofit.toml: project config. Engine, version, search path, schema registry. More [[schema]] rows get added as you register schemas.
  • .gitattributes: marks *.oplog as text so git diffs them cleanly, and marks *.data.sql text eol=lf so 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 users

This 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      users

This 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 into

Every 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_pkey

Each 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_address
note: 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 name

The 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 snapshot
CREATE 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 seal
note: 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 init

Suppose 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.sql

import 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 statements

As 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/*.sql

All 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 snapshot

What 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 runner

The 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 seal

You can also pass --seal to import to seal in one step:

retrofit import --schema sales sales.sql --seal

Bare --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.oplog

For 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-default

This 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/sales

schema 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 entirely

After --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 sales

The 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 with unregistered-path if no row matches). Outside a project (no retrofit.toml walkable), 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 --down

    Registry-less mode keys off the working directory, not off the target, so -C must name a directory with no retrofit.toml walkable above it (a sibling temp dir works; a subdirectory of the project does not). The -f target itself may live anywhere, including inside the project. Dropping -f does 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=1 makes schema-context verbs refuse with explicit-schema-required unless --schema or -f was passed. It is a CI hardening knob: set it in pipelines that should never silently fall through to default_schema.

Inspect resolution

retrofit validate enumerates the resolved scope mappings the chain will use:

retrofit validate
resolved 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 .sql files; 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 / --swap pick the bias, --clean / --accept-dirty pick 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 a pg_dump --schema-only is at (Exact / Ambiguous / None); the resolver behind migrate --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 refuses expect-mismatch when it is at some other valid cut. --expect takes the same references as migrate --from (opid prefix, or the anchors last / seal with an optional ^ / ~N), except root, which no dump is identified at and which refuses expect-unidentifiable. A dump of an empty database, one without the schema, refuses from-dump-schema-absent and points at retrofit migrate --from root, which creates it. A deployment check is one command:

    retrofit identify db.sql --expect last
  • retrofit op revise and op revise -i: edit the unsealed tail.

  • retrofit op unseal and op 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 truncate peels Seals with --drop-seals).

Run any verb with --help for its grammar and flags, or --help-all for the extended vocabulary, semantics, and caveats.

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