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

Summary: 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 ms

Timings: planning 1.264ms, execution 0.027ms, total 1.291ms.

Related to #291 (closed)

🤖 Generated with Claude Code

Edited by Radamanthus Batnag

Merge request reports

Loading
Loading