BIGSERIAL / SERIAL cannot be declared, and neither can its modern replacement GENERATED … AS IDENTITY. introspect refuses both, honestly and with a good reason (#119/#124 made the serial case reported rather than silently wrong, which is how I found this cleanly) — but the result is that no auto-incrementing integer key can be declared at all.
What sqlb survey says about it
This is the whole point of the survey command, so here is what it found on the shared core/ schema of the multi-app adoption — 27 tables, 14 modules:
## Phase B — per-table isolation
| verdict | tables |
|---|---:|
| clean — imports with nothing dropped | 25 |
| partial — imports, constructs dropped | 2 |
| refused — registry error | 0 |
### Partial
- **activity_log** (8 cols)
- `id`: column draws its default from a sequence (a serial), which the DSL cannot declare
`bigint`
- **audit_log** (7 cols)
- `id`: column draws its default from a sequence (a serial), which the DSL cannot declare
`bigint`
### By module
| module | tables | clean | partial | refused | gate |
|---|---:|---:|---:|---:|---|
| activity | 1 | 0 | 1 | 0 | **blocked** |
| audit | 1 | 0 | 1 | 0 | **blocked** |
| … the other 12 | | | | | green |
**2 of 14 modules blocked.**
25 of 27 tables are clean. The only construct blocking anything in this entire schema is the serial, and it blocks exactly the two modules that use it. Every other gap I have reported against this codebase is now closed; this is what is left.
Why the workaround is not as cheap as it looks
The obvious substitution is UUIDv7, and for an append-only log it is genuinely defensible — time-ordered, no sequence contention. But it is not free, and the cost is not in the schema:
- It is an API change, not a storage change. The reader interface is
GetByID(ctx context.Context, scope tenancy.Scope, id int64) (Entry, error). Going to UUID changes int64 to string in a public core interface and in every caller — for two cross-cutting modules that several apps consume.
- The id is load-bearing for ordering, not just identity. These tables order by
occurred_at DESC, id DESC, where the id is the tiebreak that makes the order total — without it two rows written in the same millisecond can swap places between two pages of the same cursor walk. UUIDv7 preserves this (lexicographic order is chronological); a UUIDv4 would silently destroy it. That is a sharp edge for anyone taking the obvious workaround without noticing which of the two UUID constructors they reached for.
- It fails the test the composite-key work established. The v0.8.0 notes put it well: a schema change forced by the declaration language must be defensible if sqlb vanished tomorrow.
BIGSERIAL on an audit log is not a legacy accident — it is the smallest, densest monotonically increasing key Postgres offers, on the two tables in the schema that are pure append-only and are read in insertion order. Widening it to 16 bytes per row, on the two highest-volume tables in the system, to satisfy a declaration language, is the same shape of argument that PrimaryKeyColumns and SmallInt were added to retire.
Suggested shape
The same treatment the other five constructs got in v0.8.0:
schema.Serial("id").PrimaryKey() // serial, int32
schema.BigSerial("id").PrimaryKey() // bigserial, int64
and/or the identity spelling, which is what Postgres itself now recommends over serial:
schema.BigInt("id").PrimaryKey().Identity() // GENERATED BY DEFAULT AS IDENTITY
schema.BigInt("id").PrimaryKey().Identity(schema.Always)
The sequence being a separate object is the real difficulty — Diff has to render CREATE SEQUENCE / OWNED BY, or emit the serial shorthand and let Postgres create it. The identity form may be the cheaper one to implement for exactly that reason: it has no separate object to name, which is why Postgres introduced it.
Either would take this schema from 25/27 to 27/27 and from 2 blocked modules to 0.
Found by sqlb survey on the subject-mono of docs/review-adoption-multi-app.md. Related: #119, #124 (which made the serial case report correctly rather than import wrong), #109, #114 (the same "declarable so it can be gated" argument).
BIGSERIAL/SERIALcannot be declared, and neither can its modern replacementGENERATED … AS IDENTITY.introspectrefuses both, honestly and with a good reason (#119/#124 made the serial case reported rather than silently wrong, which is how I found this cleanly) — but the result is that no auto-incrementing integer key can be declared at all.What
sqlb surveysays about itThis is the whole point of the survey command, so here is what it found on the shared
core/schema of the multi-app adoption — 27 tables, 14 modules:25 of 27 tables are clean. The only construct blocking anything in this entire schema is the serial, and it blocks exactly the two modules that use it. Every other gap I have reported against this codebase is now closed; this is what is left.
Why the workaround is not as cheap as it looks
The obvious substitution is
UUIDv7, and for an append-only log it is genuinely defensible — time-ordered, no sequence contention. But it is not free, and the cost is not in the schema:GetByID(ctx context.Context, scope tenancy.Scope, id int64) (Entry, error). Going to UUID changesint64tostringin a public core interface and in every caller — for two cross-cutting modules that several apps consume.occurred_at DESC, id DESC, where the id is the tiebreak that makes the order total — without it two rows written in the same millisecond can swap places between two pages of the same cursor walk. UUIDv7 preserves this (lexicographic order is chronological); a UUIDv4 would silently destroy it. That is a sharp edge for anyone taking the obvious workaround without noticing which of the two UUID constructors they reached for.BIGSERIALon an audit log is not a legacy accident — it is the smallest, densest monotonically increasing key Postgres offers, on the two tables in the schema that are pure append-only and are read in insertion order. Widening it to 16 bytes per row, on the two highest-volume tables in the system, to satisfy a declaration language, is the same shape of argument thatPrimaryKeyColumnsandSmallIntwere added to retire.Suggested shape
The same treatment the other five constructs got in v0.8.0:
and/or the identity spelling, which is what Postgres itself now recommends over serial:
The sequence being a separate object is the real difficulty —
Diffhas to renderCREATE SEQUENCE/OWNED BY, or emit theserialshorthand and let Postgres create it. The identity form may be the cheaper one to implement for exactly that reason: it has no separate object to name, which is why Postgres introduced it.Either would take this schema from 25/27 to 27/27 and from 2 blocked modules to 0.
Found by
sqlb surveyon the subject-mono ofdocs/review-adoption-multi-app.md. Related: #119, #124 (which made the serial case report correctly rather than import wrong), #109, #114 (the same "declarable so it can be gated" argument).