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)::uuid

Character 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.go and npm_remote_metadata_write_integration_test.go use it only for nowhere-existing foreign-key probes, which reach a namespace_id or a parent-id column and never an id, 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)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading