feat(maven): maven remote versions schema (S14 Step 3)
What
Adds the maven_remote_versions table — the version-level cache table for
kind=2 (remote) Maven repositories, child of Step 2's
maven_remote_packages — as Step 3 of the S14 Maven remote vertical slice.
One goose migration, the regenerated jet types and structure.sql dump, an
integration test suite asserting the schema shape and every constraint's
accept and reject paths, and an S14 spec amendment recording the one index
the migration adds beyond ADR-007.
No Go production code and no behavior change: nothing reads or writes this table until Step 8 (remote cache store Lookup) and Step 9 (cache-fill writes).
Schema
Follows the npm_remote_versions boilerplate and Step 1's
DETACH-then-DROP Down shape:
PARTITION BY HASH (namespace_id)× 64, children in thepartitionsschema.- PK
(id, namespace_id); app-generated UUIDv7, no sequence or default. - Composite FK
(maven_remote_package_id, namespace_id)→maven_remote_packages(id, namespace_id)withON DELETE NO ACTIONper ADR-007's artifact-table rule;namespace_idFK tonamespaces(id), alsoNO ACTION. CHECK (char_length(version) >= 1 AND char_length(version) <= 255)— the same minimum-length bound Step 2 recorded for the coordinate columns.size_bytes bigint NOT NULL DEFAULT 0andcreated_at timestamptz NOT NULL DEFAULT NOW();last_downloaded_atandsoft_deleted_atnullable.
Six indexes:
| Index | Columns | Purpose |
|---|---|---|
| Unique | (namespace_id, maven_remote_package_id, version) WHERE soft_deleted_at IS NULL |
Cached-version lookup; admits re-caching after soft delete |
| Index | (namespace_id, maven_remote_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL |
Size-ordered version listing (S17) |
| Index | (namespace_id, maven_remote_package_id) |
Full, non-partial FK coverage — the partial indexes exclude soft-deleted rows, so the RI check on a parent delete cannot use them |
| Index | (namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL |
Trash listing (S20) |
| Index | (namespace_id, created_at DESC) |
Chronological provenance scans, unconditional |
| Index | (namespace_id, maven_remote_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL |
Cache-retention sweep evaluation |
The first five come straight from ADR-007 and the merged S14 data model. The
sixth is the only shape this MR adds beyond them, and it is byte-identical to
hosted index_maven_versions_on_ns_id_pkg_id_last_downloaded_at, predicate
and NULLS FIRST included. NULLS FIRST is load-bearing: it groups
never-downloaded versions with the oldest rows, so one range scan returns
both, where NULLS LAST would sort them past every dated row and hide them
from the sweep.
Tests
maven_remote_versions_schema_integration_test.go (integration tag; raw SQL
is permitted in the migrations package to assert constraints). 18 test
functions; every acceptance clause has a named asserting test:
| Acceptance clause | Test |
|---|---|
| Applies cleanly, 64 partitions | _TableAndPartitionsExistPostUp |
| Reverts and replays cleanly | package-level TestMigrations_UpDownUp, plus _DownLockBudget and _DownReversesEveryUpObject via the shared mavenRemoteCoreMigrationTokens extension |
| In order after Step 2, no intermediate FK error | _FKRejectsAbsentPackage, _FKRejectsAbsentNamespace; every constraint test seeds through Steps 1 and 2 |
| Partial unique admits re-caching after soft delete | _PartialUniqueAllowsRecachingAfterSoftDelete |
| Partial unique rejects duplicate live coordinate | _PartialUniqueRejectsDuplicateLive (SQLSTATE 23505), _PartialUniqueScopedPerPackage |
size_bytes defaults 0, created_at defaults NOW() |
_ColumnDefaults |
| All six indexes, spec columns and predicates | _PartialUniqueIndexShape, _NonUniqueIndexShapes |
| Partitioning, PK, column set | _PartitionsByHashOfNamespaceID, _PrimaryKey, _Columns, _PartitionRoutingByHashOfNamespaceID |
version length CHECK at and over limit |
_VersionLengthCHECK (SQLSTATE 23514) |
version CHECK rejects empty |
_VersionNonEmptyCHECK (SQLSTATE 23514) |
Namespace FK NO ACTION |
_NamespaceFKIsNoAction |
| Parent delete blocked by child | _PackageDeleteBlockedByLiveChild, _PackageDeleteBlockedBySoftDeletedChild |
Constraint-rejection assertions pin the exact SQLSTATE (23514, 23503,
23505) rather than "an error occurred", as in Steps 1 and 2.
_ColumnDefaults asserts created_at.After(time.Now().Add(-time.Minute))
rather than just non-NULL, so a fixed-past-timestamp default would fail.
_NonUniqueIndexShapes pins the non-unique count at five, making any added
or dropped index fail the suite alongside the unique-shape test.
The suite was diffed subtest-by-subtest against the npm remote versions
equivalent and Step 2's packages suite for dropped coverage. Every mirrored
subtest class has a counterpart; Step 2's _FKCoverageIndexNonPartial is
folded into _NonUniqueIndexShapes with all three of its assertions
preserved, and _ColumnDefaults is net-new for this table's two defaults.
MR size
~1,431 lines of reviewable code (522-line migration + 906-line test + 3 lines across two existing test files), over the 500 LOC ceiling in the development model, which asks for a split or a justification here.
Justification: as in Steps 1 and 2, roughly 450 of the migration's 522 lines
are the mechanical 64-partition CREATE DDL and its DETACH/DROP
reverse — 128 near-identical statements, reviewable as a block once the first
is read. The generated jet types and the structure.sql dump are excluded as
generated artifacts. The novel surface is the table body and its six indexes
(~72 lines) plus the test suite. Splitting the partition DDL from the table it
partitions would produce a non-applying migration, so the step is already at
its minimum reviewable size.
End-to-end scenario catalogs
No scenario added or invalidated. docs/testing/ holds no Maven catalog yet —
authoring the first one is a separate docs concern — and this step ships no
request path, so there is no observable behavior to cover. Step 18's hermetic
proxy harness is the automated coverage for the S14 read paths.
Conformance
Not applicable: this step implements no Maven protocol behavior. Conformance runs against the read paths landing in Steps 11 and 14 through 16.
Notes for reviewers
- The migration declares
-- +goose NO TRANSACTIONfor the same reason as Steps 1 and 2: a single transaction would hold all 128AccessExclusiveLocks until commit; the per-statement layout releases them as it goes. The accepted cost — a partway-interruptedDownis not replayable — is documented in the migration header and tracked in #448. - The final commit corrects the two scan indexes. They were first written
as
(namespace_id, soft_deleted_at DESC, id)and(namespace_id, created_at DESC, id), adding a trailingidkeyset tiebreaker. That makes both a mixed(sort_col DESC, id ASC)shape, and20260730170000_add_artifact_read_keyset_indexes.sqlrecords the rule:ROW(sort_col, id)stays a single index range only when both keys share a direction, so a mixed index forces the expanded OR form and a post-scan filter. The tiebreaker therefore did not deliver the stable cursor it was added for. Both indexes now match hostedmaven_versionsexactly. - The cursor need is real but deferred. A paginated namespace-wide trash
list does want a tiebreaker, because the cache soft-delete cascade stamps
one
NOW()across a package and ties are the common case. S20 owns that query and should add the uniform(namespace_id, soft_deleted_at, id) WHERE soft_deleted_at IS NOT NULLindex with EXPLAIN evidence — the wayindex_maven_versions_on_ns_id_pkg_id_idarrived forListLiveVersionsand the S17 Phase 3 indexes arrived in20260730170000. Nothing queries this table yet, so there is no query to regress in the meantime. - The spec amendment restates the divergence preamble from two to five. Only the retention-sweep index row is a new schema divergence; the minimum-length bound and the non-partial FK-coverage index were already in the merged spec's tables but uncounted by its preamble, so that part of the edit is a docs-accuracy correction rather than new scope.
Database Review Evidence
Migrations
Note
Timings are from CI (db:migrate matrix, goose verbose) against an
empty database, in apply / rollback order per PG version.
Production-scale validation via Database Lab is not yet available. See
Database review evidence
for the matrix rationale and how to read the numbers.
| Migration | PG 16 | PG 17 | PG 18 |
|---|---|---|---|
20260731130000_create_maven_remote_versions.sql |
OK (1.72s / 6.27s) | OK (940.31ms / 6.94s) | OK (996.52ms / 6.91s) |
Migration notes:
- Up/down asymmetry: rollback is 3.6-7.4x slower than apply across all PG
versions (6.27s-6.94s down vs 940ms-1.72s up). This is expected for the
64-partition
DETACH-then-DROPDown shape and is consistent with Step 1'smaven_remote_repositoriesmigration (4.36s-7.17s down) and Step 2'smaven_remote_packages(6.48s-6.73s down). The Up direction — the boot-relevant phase — peaks at 1.72s, well within the 5-minute boot budget. - Apply is roughly 1.8x Step 2's on PG 17 and 18 (940ms and 997ms against
515ms and 522ms). This table builds six parent-level indexes where
maven_remote_packagesbuilds two, and each recurses into all 64 partitions, so the extra time is index builds rather than the table body. - PG 16 apply (1.72s) is ~1.8x the PG 17/18 applies (~940ms-997ms). Steps 1 and 2 show the same ratio in their own jobs, so this reads as PG 16 runner variance rather than anything migration-specific.
Related to #286 (closed)