feat(datastore): add the container virtual repository finder (S32 plan: 4a/20)
What changed and why
ContainerVirtualResolutionStore.UpstreamsByVirtualRepository takes the container_virtual_repositories.id. A request holds only the repositories.id, so a virtual read handler has no id to pass. ContainerVirtualRepositoryFinderStore.FindVirtualByRepositoryID maps one id to the other in one point read on unique_cvr_namespace_id_and_repository_id.
The store mirrors NpmVirtualRepositoryFinderStore row for row, with two differences. First, the container family spans docker and oci, and the shared parentSpec helper takes one format. The parent gate therefore follows containerRemoteParentPredicate with kind=1. That predicate binds no namespace, so the statement binds repositories.namespace_id itself, as findContainerRemoteRepositoryStmt does. Second, the error wrap carries no namespace or repository id, per the identifier-free error rule in docs/dev/database-query-patterns.md. The npm finder embeds both ids.
No route calls the finder yet. Steps 15 and 16 call it before Resolve.
Tests
| Criterion (plan Step 4a) | Test |
|---|---|
An active kind=1 oci or docker parent resolves to its child row's id |
resolves a kind=1 oci/docker repository and returns the id pair |
| The finder returns the child id, not the parent id | The same two subtests assert the ids differ and compare the whole struct |
The container_repositories sibling does not displace the binding |
resolves past the container_repositories sibling the create path writes |
A plain hosted container repository with no virtual child row returns ErrNotFound |
returns ErrNotFound for a hosted (kind=0) container repository |
Kind 0 and kind 2 parents with a virtual child row return ErrNotFound |
One subtest for each kind |
A parent of another format returns ErrNotFound |
returns ErrNotFound for an npm repository with a container virtual binding |
A soft-deleted parent, a missing child row, an unknown id, and another namespace return ErrNotFound |
One subtest for each |
| Any other database error keeps its identity | a transient DB failure is not misreported as ErrNotFound |
| A nil context, zero ids, and a zero-value store fail before a statement | container_virtual_repository_finder_guards_test.go |
The 20 container finder tests (5 top-level, 15 subtests) and the 20 npm finder tests pass locally against PostgreSQL 16 with max_locks_per_transaction=1024. golangci-lint run --build-tags=integration reports contextcheck and gosec on the integration file only, the same classes the npm and Maven finder suites carry.
Size
510 added lines: 132 source (container_virtual_repository_finder.go 131, query_names.go 1) and 378 test (guards 111, integration 267). The source is well under the 500-line ceiling. The total passes it because the test suites mirror the npm finder's suites case for case. Splitting the tests from the code would land a store with no tests.
e2e and conformance
No e2e scenario changes. No route reaches the finder until Steps 15 and 16 land, and those steps own the scenarios. No conformance run applies: the MR adds a datastore read and no protocol behavior.
Database Review Evidence
Queries
Note
Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral PostgreSQL 17.10 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.ContainerVirtualRepositoryFinderStore.FindVirtualByRepositoryID |
Limit | unique_cvr_namespace_id_and_repository_id, pk_repositories |
1 / 1 | 16.62 | 0.015ms | 6 / 0 | 1/64, 1/64 |
datastore.ContainerVirtualRepositoryFinderStore.FindVirtualByRepositoryID
datastore.ContainerVirtualRepositoryFinderStore.FindVirtualByRepositoryIDSummary: The plan matches the method's intent. A nested loop reads unique_cvr_namespace_id_and_repository_id with both key columns in the Index Cond, then reads pk_repositories and filters the parent gate on that one row. Each table prunes to 1 of 64 partitions. No anomalies.
Seed shape: namespaces=1, repositories=5000, container_virtual_repositories=5000
Rendered SQL:
SELECT container_virtual_repositories.id AS "container_virtual_repositories.id",
container_virtual_repositories.repository_id AS "container_virtual_repositories.repository_id"
FROM public.container_virtual_repositories
INNER JOIN public.repositories ON ((repositories.id = container_virtual_repositories.repository_id) AND (repositories.namespace_id = container_virtual_repositories.namespace_id))
WHERE (((container_virtual_repositories.namespace_id = $1::uuid) AND (repositories.namespace_id = $2::uuid)) AND (container_virtual_repositories.repository_id = $3::uuid)) AND (((repositories.format IN ($4, $5)) AND (repositories.kind = $6)) AND (repositories.soft_deleted_at IS NULL))
LIMIT $7;Bound args: [d11ea89c-633e-7d75-8927-00242cd5167b, d11ea89c-633e-7d75-8927-00242cd5167b, 88294727-7c0a-79c4-9261-4b22181edad8, 0, 3, 1, 1]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.56..16.62 rows=1 width=32) (actual time=0.014..0.015 rows=1 loops=1)
Buffers: shared hit=6
-> Nested Loop (cost=0.56..16.62 rows=1 width=32) (actual time=0.014..0.014 rows=1 loops=1)
Buffers: shared hit=6
-> Index Scan using container_virtual_repositories_namespace_id_repository_id_idx51 on container_virtual_repositories_p51 container_virtual_repositories (cost=0.28..8.30 rows=1 width=48) (actual time=0.007..0.007 rows=1 loops=1)
Index Cond: ((namespace_id = 'd11ea89c-633e-7d75-8927-00242cd5167b'::uuid) AND (repository_id = '88294727-7c0a-79c4-9261-4b22181edad8'::uuid))
Buffers: shared hit=3
-> Index Scan using repositories_p51_pkey on repositories_p51 repositories (cost=0.28..8.31 rows=1 width=32) (actual time=0.007..0.007 rows=1 loops=1)
Index Cond: ((id = '88294727-7c0a-79c4-9261-4b22181edad8'::uuid) AND (namespace_id = 'd11ea89c-633e-7d75-8927-00242cd5167b'::uuid))
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::integer[])) AND (kind = 1))
Buffers: shared hit=3
Planning:
Buffers: shared hit=716
Planning Time: 1.264 ms
Execution Time: 0.027 msTimings: planning 1.264ms, execution 0.027ms, total 1.291ms.
Related to #291 (closed)