schema-diff
Compares schemas across nodes. By default, runs table-diff on every table.
Alternatively, --ddl-only compares only object presence (tables, views, functions, indexes).
A third mode, --compare=structure, checks the actual definition of each common
table instead of its data or its mere presence.
Usage
./ace schema-diff [flags] [cluster_name] <schema_name>
Arguments
[cluster_name]— Optional; overridesdefault_cluster.<schema_name>— Schema name.
Flags
| Flag | Alias | Description | Default |
|---|---|---|---|
--dbname |
-d |
Database name | |
--nodes |
-n |
Nodes to include (comma or all) |
all |
--compare <data\|structure> |
What to compare: data (default, per-table data diff) or structure (compares table definitions instead of data; see below). |
||
--skip-tables |
-T |
Comma list of tables to exclude. Applies to --compare=structure too. |
|
--skip-file |
-s |
File with list of tables to exclude. Applies to --compare=structure too. |
|
--block-size <int> |
-b |
Rows per block when diffing tables. Default 100000. |
|
--concurrency-factor <float> |
-c |
CPU ratio for concurrency (0.0–4.0). Default 0.5. |
|
--compare-unit-size <int> |
-u |
Recursive split size for mismatched blocks. Default 10000. |
|
--output <json\|html> |
-o |
Per-table diff report format. Default json. With --compare=structure, only json is accepted, and it only takes effect when given by hand (see below). |
|
--override-block-size |
-B |
Allow block sizes outside ace.yaml guardrails. |
|
--ddl-only |
-L |
Compare object sets only (no per-table diff) | false |
--quiet |
-q |
Suppress output | false |
--debug |
-v |
Debug logging | false |
--schedule |
-S |
Run the schema diff repeatedly on a timer (requires --every). Not compatible with --ddl-only or --compare=structure. |
|
--every <duration> |
-e |
Go duration string (for example, 24h). Used with --schedule. |
Example
./ace schema-diff --dbname=mydatabase my-cluster public
When --ddl-only is not set, every qualifying table invokes table-diff using the same block size, concurrency factor, compare-unit size, output format, and override settings supplied here, so you get consistent behaviour between standalone and schema-driven comparisons.
Scheduling runs
Use --schedule --every=<duration> to keep a schema comparison running on a loop. This mode is only supported when ACE can run per-table diffs (omit --ddl-only and --compare=structure):
./ace schema-diff --schedule --every=24h --dbname=mydatabase my-cluster public
ACE performs the first comparison immediately, then waits for the given interval before repeating. Stop the process to end the loop.
Structure mode (--compare=structure)
./ace schema-diff --compare=structure --dbname=mydatabase my-cluster public
This mode does not read table data. It reads the real table definition on each node — columns, replica identity key, constraints, partition bounds, and every domain, range, composite, and enum type a column uses — and compares them directly. It is a fast check. Run it before the data diff (which is much slower), or use it alone to check that the schema DDL is still the same on every node.
What is compared: column type, NOT NULL, identity, generated, storage
options, default, and collation; the replica identity key and its operator
classes; PRIMARY KEY, UNIQUE, CHECK, FOREIGN KEY, and EXCLUDE
constraints; partition bound and partition key; and the full definition of
every domain, range, composite, and enum type used by a compared column, not
just its name. It also compares the two databases' collation settings,
because a mismatch there changes how the same text sorts and compares on
each node.
What is not compared: non-constraint indexes, triggers, rules, sequences, views, materialized views, storage parameters, column order, comments, and ACLs. A skipped view is named in a log line, not left out silently.
Types are matched by name and definition, never by OID. Two nodes created by
separate initdb runs give different OIDs to the same user-defined type, so
matching by OID would either miss a real difference or report one that does
not exist.
Findings and exit code. Each difference found gets one of five ranks. The process exits with the code of the single worst rank found in the whole run:
| Exit code | Rank | Meaning |
|---|---|---|
0 |
(none) | The schemas are identical, or both agree that the schema is empty. |
16 |
cosmetic |
Does not change the shape of the data. This mode does not produce this rank today. |
32 |
equivalent-differing |
The same values fit on both sides, but are stored or handled differently — for example, a different collation on the same type. |
48 |
narrowed |
One side accepts a strict subset of the values the other side accepts — for example, int4 vs int8, or a stricter CHECK. The report names the narrow side. |
64 |
incompatible |
Neither side's set of values contains the other's, or the object exists on only one node. |
A table missing on some nodes is reported first, on its own, before the
per-table findings. It also counts toward the 64 exit code.
Getting a JSON report. Pass --output=json on the command line to get a
structured report (schema name, node names, missing tables, and each finding)
instead of the plain-text report shown above. The flag must be given by hand:
running the command with no --output at all still prints text, even though
json is --output's own default value for every other schema-diff mode. If
this mode did not check for that, every run would print JSON by default, even
one that never named --output.
Not yet supported with this mode:
--schedule— rejected with a clear error. Use a loop around the command instead, or use--compare=data, which does support scheduling.--output=html— rejected when given by hand, since this mode has no per-table diff files to turn into HTML, only a list of findings.