feat(npm): npm_virtual_upstream_rules schema 1/3 (S31 plan: 3/19)
Stacked MRs
| Part | MR | Scope | Reviewable LOC | Target |
|---|---|---|---|---|
| 1/3 | this MR | schema, generated companions, and the cascade guard | 728 | main |
| 2/3 | !2100 (merged) | the catalog and DDL suite, and the sidecar's first two sections | 949 | dmeshcharakou/s31-npm-virtual-step-3a |
| 3/3 | !2101 (merged) | the absences and the rows they admit | 1,208 | dmeshcharakou/s31-npm-virtual-step-3b |
Merge 1/3, then 2/3, then 3/3. The order is forced, not a preference. Part
2/3 asserts a table part 1/3 creates, and it also owns the fixtures part 3/3
consumes (npmVirtualRule, seedNPMVirtualRule, seedNPMVirtualRuleTree,
npmVirtualRulesInsert and the constraint-name constants), so part 3/3 does not
compile without it. Nothing flows the other way.
Part 1/3 additionally cannot be split from its cascade-guard companion: the
moment its migration lands, TestRepositoryStore_DeleteFKAssumption_SchemaGuard
fails until wantCascadeTargets names the new table, so separating them would
leave main red between merges.
Each part was compiled with -tags=integration and run on its own branch, not
only at the tip: 0, 19 and 39 TestNPMVirtualRules* functions respectively, all
passing, against PostgreSQL 16 with the full migration chain applied.
Total 2,885 reviewable LOC, split as evenly as the atomic units allow (728 / 949 / 1,208). Part 3/3 carries the overshoot and its "Diff size" section says why shrinking it would cost more than the imbalance does.
What this does
Creates npm_virtual_upstream_rules, the allow/deny filter rules of one npm
virtual upstream association: six columns, three CHECKs, a composite primary key
of (id, namespace_id), and HASH(namespace_id) partitioning into 64
partitions, matching the column and index tables in
the S31 spec.
Both foreign keys are composite and carry namespace_id, so a rule cannot be
attached to another tenant's association. The upstream key is ON DELETE CASCADE into npm_virtual_repository_upstreams; the namespaces key is
NO ACTION.
The Up section runs under -- +goose NO TRANSACTION with IF NOT EXISTS on all
65 CREATEs and the one CREATE INDEX, so an interrupted Up is replayable, and
the Down drops the 64 partitions and the parent with no DETACH.
The cascade guard rides with this MR, deliberately
The new ON DELETE CASCADE key widens the repositories delete closure to
three levels. TestRepositoryStore_DeleteFKAssumption_SchemaGuard derives that
closure from the catalog as a transitive fixpoint over CASCADE edges, so the new
table enters it and the guard's ElementsMatch fails from the moment the
migration lands until wantCascadeTargets names it. That is why the guard fix
is in this MR and not a later one: split apart, they leave main red. Steps 1
and 2 each shipped their guard fix in the commit that added their schema, so the
pairing is the established shape rather than a one-off.
wantBlocking needs no change, because the new namespaces key is NO ACTION
and namespaces is not in the closure, so mapRepositoryDeleteError's blanket
23503 mapping is unaffected. Step 2 is not a precedent for that half: the commit
that added its schema (e739e728b) added a wantBlocking entry, for the
association's own NO ACTION key to repositories. This table has no such key,
which is why the list is untouched here.
repositories.md gains the reading its two-level list did not state: the set is
a transitive closure, so a table enters it without naming repositories in any
key.
Generated files
structure.sql and the three go-jet binding files are regenerated, not
hand-written. knownHeadVersion moves to this migration's version_id; the
embedded-SQL digest is recomputed at runtime rather than pinned, so no checksum
constant changes.
Testing
Verified against PostgreSQL 16 with the full migration chain applied.
- The cascade guard fails with
extra elements in list B: npm_virtual_upstream_ruleswithout the added entry and passes with it, so the guard is doing its job rather than being satisfied vacuously. pg_format5.9 leaves the migration byte-identical, solint:sql-formatholds.squawk2.62.0 reports 0 issues.- Up, Down and re-Up all complete, and an interrupted Up replays.
No conformance suite applies: this MR adds no Maven, npm or OCI protocol behavior.
No e2e scenario is added or affected. The table is unreachable from any request path in this MR. Nothing outside the regenerated go-jet bindings references it, so there is no client-visible behavior for docs/testing/ to describe yet. The read path that makes it reachable is Step 4.
Diff size
728 reviewable LOC, past the 500
docs/dev/development-model.md
asks a split or a justification for. This MR is the split: the step was
divided into three, mirroring Step 2. By file group here: migration SQL 716,
repositories.md 9, repositories_integration_test.go 1, checksum constant 2.
Excluded as generated: structure.sql 1,560 and the go-jet bindings 115.
Review round 1
fbb2a444f answers the branch review. One behaviour change and four comment or
doc corrections:
- The Down now bounds its lock wait. Its parent
DROPtakesACCESS EXCLUSIVEonnamespacesand on all 65 relations of thenpm_virtual_repository_upstreamstree, and that conflicts withAccessShare, so it blocks reads. The file declined to bound it because "the rollback entrypoint leaves no session to issue aSETin", which is wrong:20260814150911_add_repositories_remotes_indexsetslock_timeoutinside its own-- +goose NO TRANSACTIONsection. The Down now setslock_timeout = '5s'and resets it. ThePGOPTIONSparagraph is dropped rather than kept beside theSET, because theSETruns after the connection is established and so overrides a value supplied that way. - The Up header carries the readiness consequence both siblings state, the
statement_timeouthalf of the squawk directive, andmigrations.upTimeout. - The ten bare "the spec" citations become
docs/specs/S31-npm-virtual.md. - The admitted-row-shape list gains the allow/deny pair on one
(target_field, pattern), which a unique key over the value columns would still admit and where the deny decides. 2026989f0attaches the 63-character measurement to the spelled-out index name in the identifier-naming comment; it was reading as a claim about the 28-character abbreviated form.repositories.mdsaidnpm_virtual_repository_upstreams"cascades fromrepositories". It does not; that edge isNO ACTION. The chain runs throughnpm_virtual_repositories.
Verified for this round: pg_format 5.9 leaves the migration byte-identical and
squawk 2.62.0 reports 0 issues. Up, Down and re-Up complete against PostgreSQL
16, 17 and 18 with the full 96-migration chain applied; after the Down no
relation named npm_virtual_upstream_rules remains, the re-Up restores 64
partitions and the index, and lock_timeout reads 0 on a fresh session, so the
RESET does not leak. With a concurrent reader holding AccessShare on
namespaces, the parent DROP now fails at 5s with
canceling statement due to lock timeout; unbounded it was still queued at 12s.
The one finding round 1 left open now has its own MR. The shipped index leads on
namespace_id, so the batched rules read must carry its own namespace_id
equality or it scans all 64 partitions, and docs/specs/S31-npm-virtual.md and
the plan both wrote the predicate without it. A step MR does not edit the plan,
so the correction went to !2105 (merged), which fixes the spec enumeration, the plan's
quote of it, the plan's squawk LIMIT argument and the plan's Step 4 entry.
Issue #885 (closed), the artifact Step 4 is actually written from, was corrected the same
way. Neither is a dependency of this MR: they can merge in any order.
Review round 2
985606f answers a second branch review. Comment-only, so structure.sql, the
go-jet bindings and the checksum constant are untouched; pg_format 5.9 leaves
the migration byte-identical and squawk 2.62.0 reports 0 issues.
The ADR-007 divergences are now called out where they appear.
docs/specs/S31-npm-virtual.md names four things that are not in ADR-007 and
says "Each is called out where it appears." Two of them land in this file, the
CHECK constraints and the ON DELETE actions on both keys, and neither was
called out. The file's only ADR-007 mention sat in the header and read as a
claim of conformance for the CHECKs, so a reader would have taken them for ADR
text. The step 2 sibling does this at four sites; this file now does it at two.
The amendment itself is handbook !20938, which covers all four S31 divergences.
Four claims were wrong or too broad.
- "the allow adds nothing", for an allow and a deny naming the same
(target_field, pattern). Perdocs/specs/S13-virtual-remote-foundation.md, an upstream is eligible when the path matches at least one allow rule or the upstream has no allow rules. Adding the allow moves the association out of that second arm, so every path the pattern does not match stops being eligible. The deny decides only for the path it matches. rule_type's squawk justification calledtarget_fielda closed set of two values, where it has three, and said neither column can grow, which the CHECK comment 25 lines below contradicts by naming a future added value as the ordinary case. Closed, not fixed, is the property the smallint width needs.- The Down's bound claimed goose runs every statement on one session. That is
the provider path (
migrations.Down/DownTo), which pins one*sql.Conn. The goose CLI thatmise run db:rollbackexecs runs the statements against the pool, where theSETcarries on idle-connection reuse instead. The sibling reasons about that path and reaches the opposite conclusion, so the blanket claim overturned it silently. RESET lock_timeoutis skipped exactly when the bound fires, since goose abandons aNO TRANSACTIONmigration at the first error. Recovery from a timed-out Down is a re-run of Down, not of Up.
Known and not fixed here: the two pg_locks measurements are sampled on
PostgreSQL 16.15 only, where the step 2 sibling sampled its equivalents on 16.15
and 17.11. The numbers are structural (1 + 65) and CI runs 16/17/18.
Related to #884 (closed)