Adopting An Existing Database
Most teams don’t start withcratestack migrate diff. They start with a Postgres database
that already has tables — hand-created, inherited from a prior tool, or managed by a
previous internal migration system — and no cratestack_migrations history at all. Pointing
cratestack migrate diff at that database with no baseline produces a full CREATE TABLE
for everything it finds, because diff has no way to know the tables already exist.
cratestack migrate baseline (issue #205,
design doc migrate-baseline.md)
solves exactly that: it introspects the live database, reports how it differs from .cstack, and
writes a snapshot from what it actually found — so the very next migrate diff treats the
existing tables as already accounted for and proposes only the incremental change.
This page walks that command end to end against a real, dockerized Postgres 18 database, using the
cratestack-cli binary built from cratestack/cratestack main (merged via
PR #397). Every command and every output block
below is copied verbatim from an actual run — see Verification at the end for the
exact setup.
Prerequisites
- A live Postgres database reachable from wherever you run
cratestack. Baselining is Postgres-only for v1 — there is no--backend sqlite/bothoption (design doc §6). cratestack-cliinstalled — see Installing the CLI.- A
.cstackschema describing the shape you want the database to end up matching. It does not need to match the live database exactly; baselining is designed for the case where it doesn’t. - No
migrations/postgres/schema.snapshot.jsonyet for this project.migrate baselinerefuses to run if one already exists — see Refusing a second baseline below.
The scenario
Say you’ve inherited a Postgres database with acustomers and an orders table, created outside
CrateStack, with rows already in it:
.cstack schema that describes the same shape:
Running migrate baseline
0. Two things happened:
-
migrations/postgres/schema.snapshot.jsonwas written from the introspected database, not fromschema.cstack. For a clean baseline like this one they’re identical, but see the drift scenario below for a case where they aren’t. The snapshot is CrateStack’s post-#397format_version: 2— it stores theProjectionsIR directly rather than a full reconstructed schema, which is what makes writing it straight from a live-introspected shape possible at all. -
A synthetic row was inserted into
cratestack_migrationsin the target database:This row doesn’t run any DDL — the tables already exist — but it means a latercratestack_sqlx::apply_pending()run against this same database won’t try to replay a generated “create everything” migration against tables baseline already accounted for. See Migrations for what that table and that runner do more generally.
Refusing a second baseline
Running the exact same command again, against the same--out-dir, refuses outright:
1, no writes, no database round-trip. This is deliberate: baselining an
already-cratestack-managed backend is almost certainly a mistake, and silently overwriting a real
migration history is a worse failure mode than requiring an explicit
rm migrations/postgres/schema.snapshot.json first.
Confirming the follow-up migrate diff
This is the acceptance bar for the whole feature: after a clean baseline, migrate diff against the
unchanged schema reports nothing pending.
ALTER TABLE ADD COLUMN — not a CREATE TABLE customers (...). This is exactly the
regression this feature exists to fix: before baselining, migrate diff had no record that
customers already existed, so every diff would have proposed recreating it from scratch. The
project’s own test suite pins this exact behavior as
clean_baseline_then_added_field_produces_alter_table_not_create_table.
Interpreting the drift report
Real databases rarely match the schema byte-for-byte on day one. Say the inherited database instead looks like this — an extra column.cstack doesn’t know about, and a missing unique index:
.cstack schema as before now reports drift, but still succeeds:
0. Read the report like this:
- Grouped by table (
customers), one line per drifted column, index, or constraint. - Each line is tagged with a
Destructiveness-derived severity:safe(a missing index — cheap to add back),lossy(an undeclared column — dropping it would destroy data if you ever reconcile toward the schema), orblocking(not triggered here, reserved for changes the diff engine can’t express safely at all). - Baselining reports drift, it does not resolve it. The snapshot is written from the
live shape as introspected —
legacy_crm_idand all — not fromschema.cstack. That’s deliberate: the drift becomes visible as a pending change the next time you runmigrate diff, rather than silently disappearing.
--strict on the same drifted database flips the default:
1, and — unlike the default mode — nothing is written: no snapshot, no
cratestack_migrations row. --strict is for a different job than adoption: proving in CI that a
database already matches the schema exactly, rather than adopting one that doesn’t.
Reconciling drift afterward
Once a drifted database has been baselined (in the default, non-strict mode), the drift it reported is now sitting in the snapshot as “pending” frommigrate diff’s point of view — and because
legacy_crm_id’s removal is a DROP COLUMN, it’s destructive:
--allow-destructive is the explicit opt-in:
DROP COLUMN — versus keeping legacy_crm_id and adding it
to schema.cstack instead — is exactly the kind of call baselining deliberately leaves to a human,
per its explicit “report drift, don’t reconcile it automatically” design.
Flags reference
Verification
Every command and output block above was run against a realpostgres:18 container (not a mock),
using cratestack-cli built from cratestack/cratestack@main (commit 1dc8344, which includes
PR #397):
ALTER TABLE
after a schema change) and the drift path (--strict fails closed with zero writes; default mode
reports and still writes; the follow-up migrate diff requires --allow-destructive for the
resulting DROP COLUMN) match the design doc’s §8 test plan
and the project’s own tests_baseline integration suite, which this walkthrough independently reproduced by hand.
Source of truth: issue #206 (this page),
parent epic #202, originating issue
#135.
Read Next
- Migrations — the forward-only migration runner and
cratestack_migrationstable that baselining’s synthetic row plugs into. - Schema diff (CLI) —
cratestack diff, the sibling command that checks wire-contract-breaking changes rather than DB schema changes. - Installing the CLI — get
cratestack-cliwithout a Rust toolchain. - Composite keys —
@@id([...])/@@unique([...]), relevant to how baselining projects primary keys and unique indexes.