What satus 0.3.11 checks before a write, and how to keep order totals correct in Postgres
satus 0.3.11 checks foreign keys, types and NOT NULL before a write. It does not check totals or CHECK constraints. Postgres enforces CHECKs on insert.
A seeded order whose total_cents is not subtotal_cents + tax_cents passes every check @passkeybridge/satus@0.3.11 runs before a write. Foreign keys resolve, types match, NOT NULL holds. The arithmetic is not checked, and the README lists that under "Honest limits (v0.x)": "cross-column arithmetic is not reconciled". This post shows which checks the shipped CLI runs, what Postgres can and cannot enforce on its own, and how to move the arithmetic into the schema so that the database computes it instead of the model. Every transcript below ran on PostgreSQL 17.11 with psql 17.11, against @passkeybridge/satus@0.3.11, with no provider key in the environment.
The schema
Two tables: orders and their line items. The order carries its own subtotal, tax and total, and a shipping date that has to come after the order date.
create table orders (
id integer generated always as identity primary key,
subtotal_cents integer not null,
tax_cents integer not null,
total_cents integer not null,
placed_at timestamptz not null,
shipped_at timestamptz,
constraint total_adds_up check (total_cents = subtotal_cents + tax_cents),
constraint ships_after_placed check (shipped_at is null or shipped_at >= placed_at)
);
create table order_items (
id integer generated always as identity primary key,
order_id integer not null references orders(id),
quantity integer not null,
unit_price_cents integer not null,
line_total_cents integer not null
);
There are three sums in this schema. total_adds_up is the only one Postgres knows about. line_total_cents = quantity * unit_price_cents is not declared anywhere, and subtotal_cents matching the sum of an order's line items cannot be declared as a CHECK at all:
$ psql $DATABASE_URL -c "alter table orders add constraint subtotal_matches_items check (subtotal_cents = (select sum(line_total_cents) from order_items where order_id = orders.id))"
ERROR: cannot use subquery in check constraint
A CHECK constraint can only reference columns of the row being inserted or updated. A rule that spans rows has to be enforced by a trigger or by application code, or it is not enforced. The introspection query in introspect.ts reads primary keys, foreign keys and single-column unique constraints from pg_constraint, and does not read triggers, so satus does not see a rule that a trigger enforces.
What the dry run checks
The dry run reads the catalog, simulates rows, and validates them. It needs no key and writes nothing:
$ satus generate --dry-run --rows 25
satus generate
schema: public
profile: saas
provider: openai
model: gpt-4o-mini
rows: 25 per table
tables: orders -> order_items
orders 25 rows ~$0.0054
order_items 25 rows ~$0.0045
estimated cost: $0.0099
simulating + validating...
orders . (dry-run)
order_items . (dry-run)
✓ no validation findings across 2 tables
The coverage list at the top of validate.ts lists these checks: NOT NULL, type mismatches, integer range, string length, UUID shape, foreign keys present in the parent pool, and single-column UNIQUE within a batch. The same comment lists CHECK constraints as out of scope for v0.3.x. To confirm this, add a constraint no row can satisfy and run the dry run again:
$ psql $DATABASE_URL -c "alter table order_items add constraint impossible check (quantity > 0 and quantity < 0)"
ALTER TABLE
$ satus generate --dry-run --rows 25
satus generate
schema: public
profile: saas
provider: openai
model: gpt-4o-mini
rows: 25 per table
tables: orders -> order_items
orders 25 rows ~$0.0054
order_items 25 rows ~$0.0045
estimated cost: $0.0099
simulating + validating...
orders . (dry-run)
order_items . (dry-run)
✓ no validation findings across 2 tables
A clean dry run means the checks listed above found no problems. It does not check total_adds_up, ships_after_placed, or a constraint that rejects every row. Postgres still enforces those on insert during a real run, and the run is one transaction, so a violation rolls the whole run back. The environment these transcripts come from has no provider key, so this post contains no output from a real run.
Move the arithmetic into the schema
The rows satus asks a model for exclude generated columns, identity columns, defaulted columns and foreign keys; the filter is in schema.ts. The model is not asked for a column that the database computes. Two of the three sums can be generated columns:
create table orders (
id integer generated always as identity primary key,
subtotal_cents integer not null default 0,
tax_cents integer not null default 0,
total_cents integer generated always as (subtotal_cents + tax_cents) stored,
placed_at timestamptz not null,
shipped_at timestamptz,
constraint ships_after_placed check (shipped_at is null or shipped_at >= placed_at)
);
create table order_items (
id integer generated always as identity primary key,
order_id integer not null references orders(id),
quantity integer not null,
unit_price_cents integer not null,
line_total_cents integer generated always as (quantity * unit_price_cents) stored
);
subtotal_cents and tax_cents now default to 0, which also removes them from the model's row schema. The dry run against this version is clean:
$ satus generate --dry-run --rows 25
satus generate
schema: public
profile: saas
provider: openai
model: gpt-4o-mini
rows: 25 per table
tables: orders -> order_items
orders 25 rows ~$0.0054
order_items 25 rows ~$0.0045
estimated cost: $0.0099
simulating + validating...
orders . (dry-run)
order_items . (dry-run)
✓ no validation findings across 2 tables
The per-table estimates did not change. The estimate in runner.ts is a heuristic over every column in the table, generated or not, so here it also counts the columns the model is not asked for.
A generated column also cannot be set to a value by a later UPDATE:
$ psql $DATABASE_URL -c "update orders set total_cents = 1 where id = 1"
ERROR: column "total_cents" can only be updated to DEFAULT
DETAIL: Column "total_cents" is a generated column.
This is a schema change, and it is only right if the application never needs to store a total that differs from the arithmetic, for example a historical order priced under an old tax rate. Where that is true, the generated column enforces the total in production as well as in seeded data.
The cross-row sum, after the seed
The subtotal is the sum of other rows, which a generated column cannot express. It can be computed after the seed, in the same script that runs the seed. The rows below were inserted with plain SQL and generate_series, in place of a real satus generate run:
insert into orders (placed_at)
select timestamptz '2026-10-01 12:00+00' + g * interval '1 hour'
from generate_series(1, 3) g;
insert into order_items (order_id, quantity, unit_price_cents)
select o.id, i, 250 * o.id
from orders o, generate_series(1, 2) i;
$ psql $DATABASE_URL -c "select id, subtotal_cents, tax_cents, total_cents from orders order by id"
id | subtotal_cents | tax_cents | total_cents
----+----------------+-----------+-------------
1 | 0 | 0 | 0
2 | 0 | 0 | 0
3 | 0 | 0 | 0
(3 rows)
One UPDATE sets each order's subtotal and tax from its line items. The 8% tax rate is an example value:
update orders o
set subtotal_cents = s.sum_cents,
tax_cents = round(s.sum_cents * 0.08)
from (
select order_id, sum(line_total_cents) as sum_cents
from order_items
group by order_id
) s
where s.order_id = o.id;
$ psql $DATABASE_URL -f rollup.sql
UPDATE 3
$ psql $DATABASE_URL -c "select id, subtotal_cents, tax_cents, total_cents from orders order by id"
id | subtotal_cents | tax_cents | total_cents
----+----------------+-----------+-------------
1 | 750 | 60 | 810
2 | 1500 | 120 | 1620
3 | 2250 | 180 | 2430
(3 rows)
Postgres recomputed total_cents from the new subtotal and tax. The query below returns zero rows when every subtotal matches its items, and can run in CI after the rollup:
select o.id, o.subtotal_cents, sum(i.line_total_cents) as items_cents
from orders o
join order_items i on i.order_id = o.id
group by o.id, o.subtotal_cents
having o.subtotal_cents <> sum(i.line_total_cents);
$ psql $DATABASE_URL -f drift.sql
id | subtotal_cents | items_cents
----+----------------+-------------
(0 rows)
Postgres enforces the shipping-date CHECK on insert
ships_after_placed is a single-row rule, so Postgres rejects a violation on insert whatever wrote the row:
$ psql $DATABASE_URL -c "insert into orders (placed_at, shipped_at) values ('2026-10-02 12:00+00', '2026-10-01 12:00+00')"
ERROR: new row for relation "orders" violates check constraint "ships_after_placed"
DETAIL: Failing row contains (4, 0, 0, 0, 2026-10-02 08:00:00-04, 2026-10-01 08:00:00-04).
The timestamps in the error are shown in the session time zone, America/New_York here. In a real satus run, the same rejection rolls back the whole transaction. The dry run does not read CHECK constraints, so it does not predict that rejection.
What satus would need to change
Cross-column consistency is item 1 of Phase 2 in the roadmap, and CHECK introspection is marked as planned in the validate.ts comment. Neither is in 0.3.11. Until one ships, compute the arithmetic in the schema with generated columns where possible, and in a rollup script where it spans rows.
Run the dry run against your own schema first. It needs no key and writes nothing, and the quickstart has the install line.
Sources
- Every transcript above: PostgreSQL 17.11 (Debian 17.11-0+deb13u1),
psql17.11,@passkeybridge/satus@0.3.11on Node 20.19.2, run 2026-10-09 with no provider key set. The rows in the rollup section were inserted with plain SQL, not by a realsatus generaterun. - Validator coverage and the planned
CHECKintrospection: the header comment ofpackages/cli/src/generate/validate.ts. - Which columns the model is asked for:
packages/cli/src/generate/schema.ts. - Single-row
CHECKconstraints and generated columns: the PostgreSQL 17 docs on constraints and generated columns. - The v0.x limits: the README. Phase 2 scope: the roadmap.