Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Schema Diff

ferrule diff <conn-a> <conn-b> compares the table-and-column inventory of two databases and prints the differences. Like git diff for schemas — useful when migrations have been applied out-of-order, when staging and prod have drifted, or when porting a schema between backends.

What it compares

For each table that exists on both sides:

  • Columns present only in A (- lines).
  • Columns present only in B (+ lines).
  • Columns whose data_type differs across the two (~ lines).

For tables that exist on only one side:

  • Listed under Tables only in A or Tables only in B.

The comparison is keyed on table and column names. Type comparison uses the data_type string from each backend’s information_schema.columns (or its equivalent), so be aware: a VARCHAR(255) on one side and TEXT on the other will show as a type change, even though they often behave identically.

Cross-backend comparison

The intentional design call: ferrule does not normalize data types across backends. Diffing Postgres → MySQL will produce a lot of ~ lines because their type vocabularies are different. Diffing Postgres → Postgres or MySQL → MySQL is where this command shines.

If you need cross-backend type compatibility, post-process the JSON output (see --format json below) and apply your own type-mapping rules.

Basic usage

# Two named connections from .ferrule.toml
ferrule diff staging prod

# Two raw URLs
ferrule diff \
  "postgres://app:pwd@staging-db/myapp" \
  "postgres://app:pwd@prod-db/myapp"

# Mix: name on the left, URL on the right
ferrule diff staging "postgres://app:pwd@prod-db/myapp"

Diffing a single table

--table <name> restricts the diff to one table on each side. Useful when the inventory is large and you only care about one subject.

ferrule diff staging prod --table users

If the table is missing on either side, the command exits with a clear message rather than an empty diff.

Output formats

The default is human-readable text, mirroring diff output:

Tables only in A:
  - audit_log

Tables only in B:
  + feature_flags

Table users:
  - middle_name VARCHAR(255)
  + nickname TEXT
  ~ created_at: timestamp -> timestamp with time zone

JSON output (--format json) is structured — a top-level object with tables_only_in_a, tables_only_in_b, and table_diffs arrays. Pipe through jq for arbitrary post-processing:

ferrule diff staging prod --format json \
  | jq '.table_diffs[] | select(.table == "users")'

CSV output (--format csv) flattens the diff into rows: one per column-level change, with columns table, change_type, column, a_type, b_type. Easy to import into a spreadsheet for review.

Per-side passwords

The -p / --password flag from query doesn’t apply here — diff takes two connections, so each side gets its own flag:

  • --password-a <pwd> — overrides the credential stack for connection A.
  • --password-b <pwd> — same for B.

If a password isn’t passed on the CLI and isn’t in the credential stack, ferrule falls through to the standard interactive prompt twice (once per side). For non-interactive contexts, use FERRULE_<NAME>_PASSWORD env vars or the OS keyring.

Exit codes

ferrule diff follows GNU diff convention:

  • 0 — schemas are identical (no differences in tables or columns).
  • 1 — differences were found. Suitable for CI gates: a non-zero exit makes the job fail when staging and prod drift.
  • 2 — usage error (bad arguments, unknown connection name).
  • 3 — connection error (TLS, auth, network — couldn’t reach a side).
  • 4 — query / introspection error.

Exit 1 is reserved across ferrule for “command succeeded with a notable result” — diff today, future --fail-on-empty and check / validate commands tomorrow. Real errors live at codes 2..=4, so a diff-found result is unambiguously distinguishable from a malformed invocation.

Wrapping into CI

A common pattern: run ferrule diff in CI against the production database and the migration target, fail the job on any drift.

#!/bin/sh
set -e

if ! ferrule diff prod migration-target --format json > diff.json; then
  echo "Schemas drifted:"
  jq . diff.json
  exit 1
fi
echo "Schemas match — safe to deploy."

The JSON output is stable across runs, so the job’s diagnostic stays attached to the same artifact.

Limits

  • No constraint, index, or trigger comparison. Only column inventory and types. PRIMARY KEY / FOREIGN KEY differences are invisible here. A future Wave is planned to extend this.
  • No row-data comparison. This is a structure diff only. For row-level diffs, dump both tables and use diff / git diff / custom tooling.
  • Type strings are compared verbatim. varchar(255) and character varying(255) will show as different, even though they are the same type — the underlying data_type column reports them differently across backends.