feat(db-tool): hectic-inheritance: add hectic.immutable + diff coverage

Add a hectic.immutable parent table. Tables inheriting it get auto-attached
BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement triggers that block DML
unless the session sets hectic.migration_mode='on' (intended use: SET LOCAL
inside a migration transaction). Same exemptions as the rest of the bundle
apply (hectic schema, partitions, temp tables, GUC-excluded schemas).

database diff now appends an --- IMMUTABLE TABLE DATA --- section to its
output, with per-table unified row diffs of every table inheriting
hectic.immutable, surfacing drift in 'frozen' reference data alongside schema
drift. Subcommand exits non-zero when either schema or data differs.

Test postgres-init-hectic-inheritance extended to 10 cases covering
immutable triggers, DML blocked outside migration_mode, SET LOCAL allowing
DML inside a transaction, GUC not leaking past COMMIT, and TRUNCATE under
migration_mode.
This commit is contained in:
2026-04-30 16:10:38 +00:00
parent 31d2994997
commit 2eaa568f5b
4 changed files with 266 additions and 5 deletions

View File

@@ -112,7 +112,7 @@ To use `db-tool` in a Nix development shell, add the following to your `flake.ni
## hectic Inheritance Bundle
`pkgs.hectic.hectic-inheritance` ships a SQL artifact that bootstraps a `hectic`
schema with two parent tables and DDL event triggers:
schema with three parent tables and DDL event triggers:
- `hectic.created_at(created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())` — every
user table must `INHERITS (hectic.created_at)`. The event trigger
@@ -122,6 +122,20 @@ schema with two parent tables and DDL event triggers:
Any table that inherits from it automatically gets a `BEFORE UPDATE FOR EACH
ROW` trigger calling `hectic.set_updated_at()` attached by
`hectic_attach_updated_at_trigger`.
- `hectic.immutable()` — pure marker. Tables inheriting it are blocked from
`INSERT`/`UPDATE`/`DELETE`/`TRUNCATE` outside migration mode by triggers
attached by `hectic_attach_immutable_triggers`. Useful for reference data
that must only change via migrations. To allow DML inside a migration, wrap
it in a transaction:
```sql
BEGIN;
SET LOCAL hectic.migration_mode = 'on';
INSERT INTO public.frozen (id, label) VALUES (1, 'x');
COMMIT;
```
`SET LOCAL` is required so the permission cannot leak past `COMMIT`.
Always-exempt schemas: `hectic`, `information_schema`, anything matching
`pg_*`. Declarative partitions (`relispartition = true`) and temporary tables
@@ -134,6 +148,16 @@ Per-database opt-out for additional schemas via the
ALTER DATABASE mydb SET hectic.inheritance_extra_excluded_schemas = 'legacy,etl';
```
### `db-tool diff` and immutable tables
`database diff` already includes immutable tables in its schema-level
comparison (via `pg_dump --schema-only`). On top of that, when a `hectic`
schema is present in either side, it appends an
`--- IMMUTABLE TABLE DATA ---` section to the diff with a per-table unified
diff of the rows of every table inheriting `hectic.immutable`. Drift in
"frozen" reference data therefore surfaces in the same pager view as schema
drift, and the subcommand exits non-zero when either differs.
### Apply via `postgres-init`
Applied automatically. Set `PG_HECTIC_INHERITANCE=0` to opt out.