test(datastore): mint UUIDv7 ids in every fixture seed
What this does
Fixture seeds minted ids that production never mints. Two commits, one per family.
Commit 1, the SQL seeds. They built ids with gen_random_uuid(), which
mints a version-4 UUID. Production never does: datastore.newID returns
uuid.Must(uuid.NewV7()), and ADR-007 annotates every one of these id
columns "UUIDv7, application-generated". This replaces the call with a
version-7-shaped equivalent:
overlay(gen_random_uuid()::text placing '7' from 15 for 1)::uuidCharacter 15 of the canonical form is the version nibble. The variant bits are
untouched, so they keep the value gen_random_uuid already sets.
Commit 2, the Go-side and conformance seeds. uuid.New() returns a
version-4 UUID, and 474 call sites across 121 files fed its result to fixtures
that run against a live database. They become uuid.Must(uuid.NewV7()). Separately, the two
conformance provisioners derive their namespace id from an MD5 digest of the
slug, which carries whatever version nibble the digest produces, and they now
substitute a literal 7 at the version position.
Why not uuidv7()
PostgreSQL 18 has a built-in uuidv7(), but test:integration runs the matrix
across PG 16, 17 and 18, so the expression has to work on all three.
Measured
Over 20000 draws on PG 17: every value reads version 7, all 20000 are distinct,
each is a well-formed 36-character UUID, and the variant bits read 0b10.
Why random ids are enough here
The values are not time-ordered. id appears in these fixtures only as a
keyset tiebreaker (ORDER BY <col> DESC, id DESC), and those tests assert the
plan is an index scan with no materialized sort, not which rows come back. The
two seeds that do need a predictable id direction already mint theirs in Go
and are untouched.
Scope, commit 1
107 call sites across 45 files, all in SQL position:
| Area | Sites |
|---|---|
Go integration tests under internal/ |
77 |
.claude/skills/run-artifact-registry/driver.sh |
18 |
scripts/conformance/ (provision.sh, npm-e2e.sh, maven-provision/main.go) |
7 |
cmd/artifact-registry/wire_oci_boot_integration_test.go |
5 |
Only SQL positions change. Four places name gen_random_uuid() in prose
rather than calling it, and they keep the bare call, because rewriting them
would make the sentences say something untrue:
- two comments explaining that a seed mints its id in Go instead, so the id tiebreaker has a predictable direction;
- two assertion messages describing a hypothetical server-side
DEFAULT.
The remaining mentions in the tree are comments, two of them in immutable
migrations, with one exception this commit missed: the run recipe's SKILL.md
kept three gen_random_uuid() calls in id position, in the hand-run npm
remote file chain that driver.sh has no statement for. Commit 4 converts
them. Nothing could have caught it: no CI job runs the skill and the shell
hooks skip .claude/skills.
Scope, commit 2
The rule the rewrite follows, and the one to re-check when adding a fixture: no
integration-tagged file, and no test-support package a tagged file imports,
mints a v4 UUID for a value that reaches an id column.
The second half is not decoration. internal/storage/testsuites carries no
build tag, so the first half alone leaves its seeds behind, and the whole
PostgreSQL blob-store and session conformance suite then fails on the namespace
it seeds.
Three integration-tagged files keep uuid.New(), and they are the complete
exception list. The tag match has to be anchored, or the two files that name
the tag inside a comment (internal/datastore/repositories_test.go and
internal/format/maven/reconciler_unit_test.go, both untagged unit tests) come
back with them:
grep -rl '^//go:build integration' --include='*.go' cmd internal scripts \
| xargs grep -l 'uuid.New()'npm_remote_file_write_integration_test.go,npm_remote_write_integration_test.goandnpm_remote_metadata_write_integration_test.gouse it only for nowhere-existing foreign-key probes, which reach anamespace_idor a parent-id column and never anid, so no CHECK reads them.
The uuid.New() sites elsewhere in the tree are sqlmock and pure unit tests
that never open a database.
The conformance derivation
scripts/conformance/provision.sh and deterministicUUID in
scripts/conformance/maven-provision/main.go must stay byte-for-byte
identical, or the two harnesses seed different namespaces. Both now substitute
a literal 7 at character 15 and drop the digest nibble it replaces, so the
golden in TestDeterministicUUID moves from
2d0c98f2-d117-a1b6-06ab-283f970fc601 to
2d0c98f2-d117-71b6-06ab-283f970fc601. Confirmed by running both derivations
against slug conformance.
The variant nibble is left as the digest produced it, which is what this derivation has always stored. The CHECK reads the version nibble only.
The one fixture that is not a substitution
TestBulkContainerWorkerIntegration_DeleteAllImages_SkipsAStoredZeroID seeds
the all-zero id on purpose, and the dependent CHECK is precisely what makes
that id unstorable. The behavior underneath it is still real: the scope read's
conditional cursor arm and the marker's argument guard are a second line of
defense, and dropping the test would retire their only coverage silently.
On this branch the zero id goes in through seedImageWithID, because no
constraint refuses it yet, and
internal/managementapi/bulk_container_worker_integration_test.go is identical
to the merge base. Commit 3 reverted the seedZeroIDImage helper an earlier
revision of this branch carried: it dropped
check_container_images_id_uuid_version around the insert, and that constraint
does not exist here, so the DROP failed with 42704 on PostgreSQL 16, 17 and
18 alike.
The helper belongs to !2106 (merged), which is where the CHECK exists. !2106 (merged) does not
carry it yet: it branches from commit 2, one commit before the revert, so it
inherits the helper from this branch rather than owning it, and loses it when
this merge request merges. It has to gain the helper in its own diff before
that happens, or
TestBulkContainerWorkerIntegration_DeleteAllImages_SkipsAStoredZeroID goes
red under the CHECK it adds.
Comment caps
Commit 2 compresses the blocks it had to correct in
maven-provision/main.go and its test, relocating what they said to an
assertion message and a body comment, which the caps table names as sites with
budget. The three blocks it also compressed in
bulk_container_worker_integration_test.go went away with commit 3, which
returns that file to the merge base.
One block takes the third outcome in
docs/dev/go-style.md, moving its claim to docs/dev/
behind a one-line pointer. The namespaceSeedingStore block in
internal/storage/pg_namespace_fixture_integration_test.go said the fixtures
"draw namespace ids from uuid.New()", which this merge request falsifies for
both halves of that sentence, and correcting the one token charges all 22 lines
against a 2-line cap. The block quantifies over internal/storage,
internal/storage/testsuites and the in-memory stub, so no single declaration
can state it and keep it true: the trigger, why a per-call-site seed cannot
work, and which three methods it wraps now live in
docs/dev/go-testing.md under ### Minting fixture ids, stated without naming a UUID version. The doc comment is a two-line
pointer at the cap.
An earlier revision restored the file to the merge base instead, the fourth outcome. That outcome's trigger did not hold: the edit corrected text this branch itself falsified, which is the case the fifth outcome refuses to leave standing.
Diff size
Commit 2 is past the 500 reviewable LOC development-model.md asks to split or justify. Splitting does not help: it is one mechanical substitution that has to land atomically, because a partial conversion leaves the dependent CHECK red on whatever it misses, and the misses are exactly what two earlier narrower passes produced.
Counted as call sites, with the changed-line count beside it, because a handful of lines carry two calls:
| File group | Call sites | Lines | Shape |
|---|---|---|---|
Integration tests under internal/ and cmd/ |
371 | 341 | one-line uuid.New() to uuid.Must(uuid.NewV7()) |
Test-support packages (testsuites, cryptotest, remotetest, testutil) |
148 | 133 | same |
scripts/conformance/ |
n/a | n/a | the version nibble and its golden |
Commit 2 is 519 call sites over 474 lines in 121 files. Everything outside the last row is the same substitution repeated.
Review follow-ups
Commits 4 and 5 close the findings from a branch review. Neither touches the substitution.
| Finding | Fix |
|---|---|
SKILL.md kept three gen_random_uuid() calls in id position, and db-review-prep's stub table maps uuid to the same |
both converted; the stub table now separates an id column from an ordinary uuid one |
provision.sh was no longer idempotent, because the derived namespace id changed and the child inserts used it directly |
the id is read back from the persisted row with \gset, as maven-provision's seed() already did |
Two comments claimed namespaces.id already carries the version CHECK |
both now say when it starts holding |
TestDeterministicUUID's message printed printf '%%s', because testify returns a single msgAndArgs string verbatim |
written %s |
One line reached 163 columns, over lll's 160, where no CI job or hook can see it |
wrapped, with nil kept on the directive's line |
| The version-7-shaped-but-not-time-ordered property was recorded only here | ### Minting fixture ids in docs/dev/go-testing.md |
The idempotency fix is verified on PostgreSQL 16 against both statements: on a fresh database the old and new blocks behave identically, and on a database seeded at the old derived id the old block exits 3 on a foreign-key violation while the new one adopts the persisted row.
Still open, and not fixable from this branch: commit 1's body carries the same
false completeness claim its Scope, commit 1 section did, and its
"ADR-007 annotates every one of these id columns" is off by one, since
upstream ADR-007 reads "UUIDv7, globally unique across Artifact Registry
deployments" for namespaces.id. Both need a reword of a non-HEAD commit.
Verification
The full test:integration package set was run locally with the dependent
CHECK constraint applied on top of this branch, against a PostgreSQL 16 tuned
with the .pg-service-options GUCs and the constraints confirmed present
(2341 rows in pg_constraint, parents plus partitions).
test:integration does not cover internal/datastore/migrations; that package
runs in the separate test:integration:migrations job, so this local run did
not exercise it. The row-shape and schema suites the CHECK adjusts live there
and are adjusted in !2106 (merged)'s own diff.
Result: zero check_*_id_uuid_version violations and 25 packages green. The
one remaining failure,
TestListNpmVersionIDsForDeleteAll_HostedKeepsTheAcceptanceBound, is a
local-only clock-skew artifact of the colima VM running ahead of the macOS
host; it passed in CI on the dependent merge request's red pipeline too, so it
is not related to this change.
driver.sh smoke is re-run on the dependent merge request rather than here,
because that is where the constraint exists and the run recipe's seeds are
actually exercised against it.
Stack
Merge in this order:
| # | MR | What |
|---|---|---|
| 1 | this one | fixture seeds mint UUIDv7 |
| 2 | !2106 (merged) | the CHECK holding id to UUIDv7 |
!2106 (merged) targets this branch rather than main, so the order cannot invert. Two
things are owed inside the window between the two merges. !2106 (merged) has to gain the
seedZeroIDImage helper in its own diff, per The one fixture that is not a
substitution above. And while the window is open nothing enforces the rule in
Scope, commit 2: a fixture added in that period that mints a v4 id for an
id column re-breaks !2106 (merged), and it surfaces as constraint violations in
whichever pipeline runs next rather than in the merge request that caused it.
The rule is written down in
docs/dev/go-testing.md under
### Minting fixture ids as of commit 5, so it is at least greppable.
Related to #983 (closed)