feat(datastore): add the maven virtual upstream list and rules reads (S30 plan: 4/25)
Adds the two datastore reads a Maven virtual resolution starts from. UpstreamsByVirtualRepository returns a virtual repository's upstream list in position order, joining repositories for kind, format, and name, maven_remote_repositories for the validity hours, the revalidate flag, health status, and URL, and maven_repositories for the hosted binding. It selects no credential column. RulesByUpstreams returns the rules for a set of associations in one statement, grouped by association. Both reads fail closed past their bound rather than serving a shortened result: the list read with ErrMavenVirtualUpstreamListTooMany above the 20-position cap the schema already enforces, the rules read with ErrMavenVirtualUpstreamRulesTooMany past its ceiling, because a dropped rule may be a deny. The plan books a clip-and-warn on the list read; it is dropped here because the position CHECK makes it unreachable. A soft-deleted upstream is returned flagged, not filtered; the caller drops it. The contract sidecar maven_virtual_resolution_contract.md states the projection and the two absence shapes the left joins report.
One defect found while writing it: the go-jet default alias for the rules read's grouping column is 65 bytes, past PostgreSQL's 63-byte identifier cap, so the column came back truncated, the row mapper matched no field, and every rule grouped under the zero UUID with no error. That is "no rules", so deny rules would have been ignored. The alias is now explicit and a test walks both row structs to keep every alias under the cap.
Tests cover the plan's acceptance criteria plus guards for defects this diff hit: a 63-byte cap on every alias tag in the package and an explicit alias tag on both resolution row structs, a transient failure surfacing as an error rather than an empty result, and the remote-position shape (every Maven repository carries a maven_repositories row whatever its kind). Review added the list read at ADR-004's cap and its indifference to a soft-deleted virtual parent, the constructor's nil-client panic and the client it keeps, the keys, joins, projection and discriminator guards on both rendered statements, and the statement count an empty rules batch issues.
No e2e catalog change and no docs/user change: nothing wires these reads yet, so no client-visible behavior moves.
Hand-written diff is about 1,570 lines and stays in one MR: the two reads are 251 lines of Go plus a 111-line contract, the rest is tests, and the list read and rules read are separate commits if a split is wanted.
Database Review Evidence
Queries
Note
Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral
PostgreSQL 17 container (matching GL_PG_CURR_VERSION from
.gitlab-ci-other-versions.yml), with synthesized seed data rolled
back per query and the container torn down at the end of the run.
Numbers reflect moderate cardinality and do not capture
production-scale effects. See
Database review evidence
for seed sizing, methodology, and the anomalies the skill flags.
Expand each row's details for the seed shape, rendered SQL, bound args,
and raw plan.
| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|---|---|---|---|---|---|---|---|
datastore.UpstreamsByVirtualRepository |
Limit | unique_mvru_ns_id_repository_id_position |
1 / 6 | 33.24 | 1.654ms | 40 / 11 | 1/64, 1/64, 1/64, 1/64 |
datastore.RulesByUpstreams |
Limit | index_mvur_on_ns_id_repository_upstream_id |
1 / 60 | 16.88 | 0.026ms | 3 / 0 | 1/64 |
datastore.UpstreamsByVirtualRepository
Summary: Plan matches the method's intent. The driving index scan is the
positional uniqueness index, so it supplies the ORDER BY position ordering
with no Sort node above it, and each of the three joins is an index probe on
its own (namespace_id, ...) index, pruning all four tables to one of 64
partitions. Both child joins reach their (namespace_id, repository_id)
unique index. No anomalies.
A note for reviewers, because it is easy to reproduce by accident: seeding
this read with a single namespace that owns every row of its partition makes
the planner pick a Hash Right Join over a Seq Scan on the
maven_repositories partition instead. That is a cost tie-break, not a
missing index. At one namespace per partition the namespace_id filter
removes nothing, so the scan is priced at 109.54 for 5,003 rows and the whole
plan at 316.65, against 342.38 for the index-probe plan — 7.7% apart. Forcing
the index path there (enable_seqscan = off) executes faster anyway (0.166ms
against 0.896ms). The seed below is the representative one: the target's
maven_repositories partition holds 8,003 rows from 6 namespaces, of which 3
are the target's, and the planner picks the index unprompted.
Seed shape: namespaces=201, repositories=650007, maven_repositories=320003, maven_remote_repositories=320003, maven_virtual_repositories=10001, maven_virtual_repository_upstreams=200006
Target namespace: one virtual repository with six upstream positions (3
hosted, 3 remote). 200 filler namespaces with random ids carry the bulk (3,200
upstream repositories and 50 virtual repositories x 20 positions each), so
every hash partition holds rows from several namespaces. Target partitions:
maven_virtual_repository_upstreams_p63 5,006 rows / 6 namespaces / 6 the
target's; maven_repositories_p63 8,003 / 6 / 3;
maven_remote_repositories_p63 8,003 / 6 / 3; repositories_p63 16,257 / 6 / 7.
Rendered SQL:
SELECT maven_virtual_repository_upstreams.id AS "maven_virtual_repository_upstreams.id",
maven_virtual_repository_upstreams.upstream_repository_id AS "maven_virtual_repository_upstreams.upstream_repository_id",
maven_virtual_repository_upstreams.position AS "maven_virtual_repository_upstreams.position",
repositories.name AS "repositories.name",
repositories.kind AS "repositories.kind",
repositories.format AS "repositories.format",
repositories.soft_deleted_at AS "repositories.soft_deleted_at",
maven_repositories.id AS "maven_repositories.id",
maven_remote_repositories.id AS "maven_remote_repositories.id",
maven_remote_repositories.repository_id AS "maven_remote_repositories.repository_id",
maven_remote_repositories.url AS "maven_remote_repositories.url",
maven_remote_repositories.cache_validity_hours AS "maven_remote_repositories.cache_validity_hours",
maven_remote_repositories.metadata_cache_validity_hours AS "maven_remote_repositories.metadata_cache_validity_hours",
maven_remote_repositories.snapshot_metadata_always_revalidate AS "maven_remote_repositories.snapshot_metadata_always_revalidate",
maven_remote_repositories.last_health_status AS "maven_remote_repositories.last_health_status"
FROM public.maven_virtual_repository_upstreams
INNER JOIN public.repositories ON ((repositories.namespace_id = maven_virtual_repository_upstreams.namespace_id) AND (repositories.id = maven_virtual_repository_upstreams.upstream_repository_id))
LEFT JOIN public.maven_remote_repositories ON ((maven_remote_repositories.namespace_id = $1::uuid) AND (maven_remote_repositories.repository_id = maven_virtual_repository_upstreams.upstream_repository_id))
LEFT JOIN public.maven_repositories ON ((maven_repositories.namespace_id = $2::uuid) AND (maven_repositories.repository_id = maven_virtual_repository_upstreams.upstream_repository_id))
WHERE ((maven_virtual_repository_upstreams.namespace_id = $3::uuid) AND (maven_virtual_repository_upstreams.maven_virtual_repository_id = $4::uuid)) AND (repositories.namespace_id = $5::uuid)
ORDER BY maven_virtual_repository_upstreams.position ASC
LIMIT $6;Bound args: [52ed1f92-7e53-7cf6-a79e-30747fa5bab4, 52ed1f92-7e53-7cf6-a79e-30747fa5bab4, 52ed1f92-7e53-7cf6-a79e-30747fa5bab4, a6500b9b-063e-7769-8ff9-99ae9498e100, 52ed1f92-7e53-7cf6-a79e-30747fa5bab4, 21]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Limit (cost=1.13..33.24 rows=1 width=166) (actual time=1.178..1.610 rows=6 loops=1)
Buffers: shared hit=40 read=11
-> Nested Loop Left Join (cost=1.13..33.24 rows=1 width=166) (actual time=1.178..1.608 rows=6 loops=1)
Buffers: shared hit=40 read=11
-> Nested Loop Left Join (cost=0.85..24.93 rows=1 width=150) (actual time=1.169..1.593 rows=6 loops=1)
Buffers: shared hit=25 read=11
-> Nested Loop (cost=0.57..16.62 rows=1 width=69) (actual time=0.710..1.123 rows=6 loops=1)
Buffers: shared hit=12 read=9
-> Index Scan using maven_virtual_repository_ups_namespace_id_maven_virtual_r_key63 on maven_virtual_repository_upstreams_p63 maven_virtual_repository_upstreams (cost=0.28..8.30 rows=1 width=52) (actual time=0.384..0.385 rows=6 loops=1)
Index Cond: ((namespace_id = '52ed1f92-7e53-7cf6-a79e-30747fa5bab4'::uuid) AND (maven_virtual_repository_id = 'a6500b9b-063e-7769-8ff9-99ae9498e100'::uuid))
Buffers: shared hit=1 read=2
-> Index Scan using repositories_p63_pkey on repositories_p63 repositories (cost=0.29..8.30 rows=1 width=65) (actual time=0.122..0.122 rows=1 loops=6)
Index Cond: ((id = maven_virtual_repository_upstreams.upstream_repository_id) AND (namespace_id = '52ed1f92-7e53-7cf6-a79e-30747fa5bab4'::uuid))
Buffers: shared hit=11 read=7
-> Index Scan using maven_remote_repositories_p63_namespace_id_repository_id_idx on maven_remote_repositories_p63 maven_remote_repositories (cost=0.28..8.30 rows=1 width=81) (actual time=0.078..0.078 rows=0 loops=6)
Index Cond: ((namespace_id = '52ed1f92-7e53-7cf6-a79e-30747fa5bab4'::uuid) AND (repository_id = maven_virtual_repository_upstreams.upstream_repository_id))
Buffers: shared hit=13 read=2
-> Index Scan using maven_repositories_p63_namespace_id_repository_id_idx on maven_repositories_p63 maven_repositories (cost=0.28..8.30 rows=1 width=32) (actual time=0.002..0.002 rows=0 loops=6)
Index Cond: ((namespace_id = '52ed1f92-7e53-7cf6-a79e-30747fa5bab4'::uuid) AND (repository_id = maven_virtual_repository_upstreams.upstream_repository_id))
Buffers: shared hit=15
Planning:
Buffers: shared hit=68 read=12
Planning Time: 1.294 ms
Execution Time: 1.654 msTimings: planning 1.294ms, execution 1.654ms, total 2.948ms.
datastore.RulesByUpstreams
Summary: Plan matches the method's intent: a single Index Scan on
index_mvur_on_ns_id_repository_upstream_id, with both the namespace equality
and the association list inside the Index Cond, pruning to one of 64
partitions. The estimate is 1 row against 60 actual, a 60x divergence that
comes from the per-value selectivity of the six-element IN list rather than
from the plan shape; the Index Cond is fully bound either way and the LIMIT
is the method's constant ceiling. No index or predicate change is indicated.
Seed shape: namespaces=201, repositories=4207, maven_virtual_repositories=201, maven_virtual_repository_upstreams=4006, maven_virtual_upstream_rules=200060
Target namespace: one virtual repository, six positions, 10 rules each. 200
filler namespaces with random ids: one virtual repository, 20 positions, 50
rules each. Target partition maven_virtual_upstream_rules_p05 holds 3,060
rows from 4 namespaces, of which 60 are the target's.
Rendered SQL (six-element association list, matching the six-position upstream list the paired read returns):
SELECT maven_virtual_upstream_rules.maven_virtual_repository_upstream_id AS "rule_upstream.id",
maven_virtual_upstream_rules.rule_type AS "maven_virtual_upstream_rules.rule_type",
maven_virtual_upstream_rules.target_field AS "maven_virtual_upstream_rules.target_field",
maven_virtual_upstream_rules.pattern AS "maven_virtual_upstream_rules.pattern"
FROM public.maven_virtual_upstream_rules
WHERE (maven_virtual_upstream_rules.namespace_id = $1::uuid) AND (maven_virtual_upstream_rules.maven_virtual_repository_upstream_id IN ($2::uuid, $3::uuid, $4::uuid, $5::uuid, $6::uuid, $7::uuid))
LIMIT $8;Bound args: [10b87903-6dbe-7f0b-81a0-aad4ce498c02, 4debcb35-60bb-7af9-aba8-776a524d48ba, ef804ac6-c2c7-742a-a953-61ca9811f343, 45cf6afb-a8d5-71ab-b8dc-cce684fa3c3c, 07d2840f-1a02-77fa-85c5-b40bd9ccd734, 73a51992-fa69-7d15-8abe-dd086bc84976, 71600a03-74c9-74f0-9e11-1e28ef2e086a, 20001]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Limit (cost=0.28..16.88 rows=1 width=38) (actual time=0.010..0.019 rows=60 loops=1)
Buffers: shared hit=3
-> Index Scan using maven_virtual_upstream_rules__namespace_id_maven_virtual_r_idx5 on maven_virtual_upstream_rules_p05 maven_virtual_upstream_rules (cost=0.28..16.88 rows=1 width=38) (actual time=0.009..0.014 rows=60 loops=1)
Index Cond: ((namespace_id = '10b87903-6dbe-7f0b-81a0-aad4ce498c02'::uuid) AND (maven_virtual_repository_upstream_id = ANY ('{4debcb35-60bb-7af9-aba8-776a524d48ba,ef804ac6-c2c7-742a-a953-61ca9811f343,45cf6afb-a8d5-71ab-b8dc-cce684fa3c3c,07d2840f-1a02-77fa-85c5-b40bd9ccd734,73a51992-fa69-7d15-8abe-dd086bc84976,71600a03-74c9-74f0-9e11-1e28ef2e086a}'::uuid[])))
Buffers: shared hit=3
Planning:
Buffers: shared hit=12
Planning Time: 0.233 ms
Execution Time: 0.026 msTimings: planning 0.233ms, execution 0.026ms, total 0.259ms.
Query notes:
datastore.RulesByUpstreams: plan rows 1 against 60 actual, a 60x divergence. It comes from the per-value selectivity the planner assigns each element of the six-elementINlist, not from the plan shape: the Index Cond bindsnamespace_idand the whole list, one partition is scanned, and theLIMITis the method's own constant ceiling. No change indicated.
Related to #289 (closed)