feat(db): guarantee public.uuidv7() exists after nself start - #428
Merged
Merged
Conversation
Requested by ummeco/ummat (inbox msg-2026-09-14-uuidv7-extension, high).
Their migration 20260511104612 runs `CREATE EXTENSION IF NOT EXISTS pg_uuidv7`
and SOFT-FAILS by design: the base image does not ship it, so the migration warns
and is still recorded as applied. Its own header then tells later DDL to use
`id UUID PRIMARY KEY DEFAULT public.uuidv7()`. Two later migrations did exactly
that and died with "function public.uuidv7() does not exist". A soft-failing
install that later DDL trusts is worse than a hard failure.
The ask was to add pg_uuidv7 to the base PG16 image. Not doing that, for two
concrete reasons:
- It would mean nSelf building, publishing and maintaining its own Postgres
image instead of using pgvector/pgvector:pg16. That is a distribution change
(Docker Hub is one of the eight bus-factor accounts) and an owner decision,
not a code fix.
- It would not work at all on the embedded pglite runtime, which cannot load a
C extension. That path would still lack the function.
Instead, guarantee the POST-CONDITION the consumers actually need: after
InitializeDatabase, public.uuidv7() exists. ensureUUIDv7 tries CREATE EXTENSION
first, because a C implementation is better when present, tolerates its absence,
and then applies a guarded SQL definition that no-ops if anything already
provides the function. An image that does ship pg_uuidv7 keeps the C version.
The SQL body is the standard community implementation, taken from ummat's own
verified fallback rather than written fresh: overlay a v4 UUID's first 6 bytes
with the big-endian millisecond timestamp, then set bits 52 and 53 to turn the
version nibble from 0100 into 0111. gen_random_uuid() has already set the RFC
9562 variant bits and the overlay does not touch them.
Verified live against postgres:16-alpine, which has no pg_uuidv7, so these
exercise the fallback path and not a C implementation:
--- PASS: TestEnsureUUIDv7_Integration_FallbackDefinesAConformingFunction
--- PASS: TestEnsureUUIDv7_Integration_IsIdempotent
--- PASS: TestEnsureUUIDv7_Integration_DoesNotReplaceAnExistingDefinition
ok github.com/nself-org/cli/internal/database 6.379s
The first pins what consumers depend on rather than merely that a function
exists: version nibble 7, variant bits in 8/9/a/b, the leading 48 bits decoding
to within 60s of now, strict ordering across milliseconds, and 200 distinct
values in one batch. A wrong implementation still returns a uuid, so
"it exists" would not have been a test.
CI caught this, correctly:
--- FAIL: TestFileSizeBudgetNotExceeded
file_size_test.go:120: 1 files exceed the 300-line cap but the budget is 0.
Adding ensureUUIDv7 took internal/database/init.go from 275 to 360 lines, past
the 300-line cap the repo enforces. Raising the budget was not an option: the cap
is the rule, and a gate that gets relaxed whenever it fires is not a gate.
Split instead, which is the better shape anyway — the uuidv7 bootstrap is its own
concern, not part of the create-database/schema/extension sequence.
init.go 360 -> 280
uuidv7.go 89 (new)
Verified after the split:
go test ./internal/repoqa/ -run TestFileSizeBudget ok
INTEGRATION=1 go test -tags integration -run TestEnsureUUIDv7 ok (6.604s)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by ummeco/ummat — inbox
msg-2026-09-14-uuidv7-extension, priority high.The trap being closed
Their migration
20260511104612runsCREATE EXTENSION IF NOT EXISTS pg_uuidv7and soft-fails by design: the base image does not ship it, so the migration raises a WARNING and is still recorded as applied. Its own header then instructs later DDL to useid UUID PRIMARY KEY DEFAULT public.uuidv7().Two later migrations did exactly that and died:
A soft-failing install that downstream DDL is told to trust is worse than a hard failure — it fails later, somewhere else, in someone else's migration.
Why not "add pg_uuidv7 to the base image", which is what was asked
Two concrete reasons, both of which would leave the request only half-served:
pgvector/pgvector:pg16. That is a distribution change — Docker Hub is one of the eight bus-factor accounts — and an owner decision, not a code fix.nself start --embedded-pgusers would still hit the exact same error.What this does instead
Guarantee the post-condition the consumers actually depend on: after
InitializeDatabase,public.uuidv7()exists.ensureUUIDv7:CREATE EXTENSION IF NOT EXISTS pg_uuidv7— a C implementation is better when it is there — and tolerates its absence;So an image that does ship
pg_uuidv7keeps the C version, and everything else gets a working one. If the owner later decides to ship a custom image, this becomes dead code rather than a conflict.The SQL body is the standard community implementation, taken from ummat's already-verified fallback rather than written fresh: overlay a v4 UUID's first 6 bytes with the big-endian millisecond timestamp, then set bits 52 and 53 to turn the version nibble from
0100into0111.gen_random_uuid()has already set the RFC 9562 variant bits and the overlay does not touch them.Verified live
Against
postgres:16-alpine, which has nopg_uuidv7— so these exercise the fallback, not a C implementation. The first test asserts that precondition explicitly."The function exists" would not have been a test — a wrong implementation still defines a function and still returns a uuid. So these pin what callers actually rely on:
78/9/a/bgen_random_uuid()'s variant bitsuuid_generate_v4— keyset-pagination cursor indexesnself startruns this every bootpg_uuidv7would silently lose its C implementation