Adopting An Existing Database

Most teams don’t start with cratestack 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/both option (design doc §6).
  • cratestack-cli installed — see Installing the CLI.
  • A .cstack schema 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.json yet for this project. migrate baseline refuses to run if one already exists — see Refusing a second baseline below.

The scenario

Say you’ve inherited a Postgres database with a customers and an orders table, created outside CrateStack, with rows already in it:
And a .cstack schema that describes the same shape:

Running migrate baseline

Against the matching database above, this is a clean baseline — the live shape and the schema agree exactly:
Exit code 0. Two things happened:
  1. migrations/postgres/schema.snapshot.json was written from the introspected database, not from schema.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-#397 format_version: 2 — it stores the Projections IR directly rather than a full reconstructed schema, which is what makes writing it straight from a live-introspected shape possible at all.
  2. A synthetic row was inserted into cratestack_migrations in the target database:
    This row doesn’t run any DDL — the tables already exist — but it means a later cratestack_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:
Exit code 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.
No migration directory is written. Now add a field to the schema:
An incremental 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:
Baselining against the same .cstack schema as before now reports drift, but still succeeds:
Exit code 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), or blocking (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_id and all — not from schema.cstack. That’s deliberate: the drift becomes visible as a pending change the next time you run migrate diff, rather than silently disappearing.
Running the same command with --strict on the same drifted database flips the default:
Exit code 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” from migrate 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:
Whether you actually want to apply that 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 real postgres:18 container (not a mock), using cratestack-cli built from cratestack/cratestack@main (commit 1dc8344, which includes PR #397):
Both the clean-baseline path (baseline → refuse-on-repeat → no-op diff → incremental 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.
  1. Migrations — the forward-only migration runner and cratestack_migrations table that baselining’s synthetic row plugs into.
  2. Schema diff (CLI)cratestack diff, the sibling command that checks wire-contract-breaking changes rather than DB schema changes.
  3. Installing the CLI — get cratestack-cli without a Rust toolchain.
  4. Composite keys@@id([...])/@@unique([...]), relevant to how baselining projects primary keys and unique indexes.