feat(datastore): batched container remote upstream lookup (S32 plan: 6/19)
What
Adds the batched remote cache read for a virtual container repository and the Phase 1 adapter that answers one remote position from it.
ContainerVirtualRemoteLookupStore.ByPositions answers every remote position
in one statement per reference shape — tag, manifest by digest, blob by
digest — instead of one statement per position. Each statement is the IN-list
twin of the read ContainerRemoteCacheStore already answers per repository.
oci.VirtualRemoteUpstream maps one position's verdict onto
remote.LookupResult.
LookupResult.ETag carries the row's upstream_etag. That field is what makes
the conditional HEAD reachable: conditionalETag reads it off the Phase 1
result, and remote.Prober maps a 304 to ProbeFresh only on a non-empty
If-None-Match. An adapter that left it empty would silently disable the
spec's stale-tag revalidation for every container position.
No route reaches either entry point yet. Nothing on main calls ByPositions
or NewVirtualRemoteUpstream, so this MR changes no observable behavior and
rolls back cleanly in both directions — an older binary simply emits three
fewer name label values.
Why the predicates are enumerated
A batched rewrite is where a per-repository read's predicates get dropped, and
nothing fails when one goes missing: the SQL compiles and the wrong answer is
plausible. internal/datastore/container_virtual_remote_lookup_predicates.md
names each predicate the twins carry, per statement, alongside what dropping it
would cost. Every excluded row shape has an integration subtest asserting a
miss, and each runs per reference shape rather than once, because identical
SQL across the three tables is not a reason to assert one.
Dropped-subtest diff
Guardrail 6's mirror clause applies twice here: this suite mirrors Step 5's
hosted half, and the oci adapter mirrors internal/format/npm's. Both diffs
were run, and they found three dropped subtests, all now restored:
- A live content row answering when a tombstoned row shares its key, on both
digest routes.
unique_container_remote_manifests_ns_id_image_id_digestandunique_container_remote_blobs_ns_id_image_id_digestare partial onsoft_deleted_at IS NULL(migrations/structure.sql:54878,:56427), so a tombstone releases its slot and a re-cached row sits beside it. An earlier comment in this branch's suite claimed those indexes carry noWHEREclause and skipped the case on that basis. That claim was wrong; the case is required and is now covered. - The by-digest routes not crossing tables. Manifests and blobs both key on
(namespace, image, digest), so a builder reaching for the wrong one answers plausibly. - A miss-carrying adapter in the oci path-mismatch table. That table varied the
asked-for path and built a hit adapter every time, so it never asserted that
the guard precedes the verdict for a miss. A Miss is the zero
LookupResult, so a verdict returned ahead of the guard reads as a plausible miss on the asked-for path rather than as a fault.
Two findings left for follow-up
Neither is closed here. The first has had its test half fixed, which the paragraph below states.
The two freshness reads spell one window differently.
freshWithinHoursExpr builds its interval from a Go time.Duration, and jet
splits whole days out first, so a 24-hour window — the cache_validity_hours
default — renders INTERVAL '1 DAY'. freshWithinColumnWindowExpr multiplies
INTERVAL '1 HOUR' by the column, so it always renders exact hours. Postgres
makes timestamptz - interval '1 day' DST-aware and - interval '24 hours'
not, so under a session timezone that observes DST the two edges sit an hour
apart on a shift day. Measured under America/New_York at
2026-11-01 12:00:00-05: the day edge falls 25 real hours back and the hours
edge 24, so a row 24.5 hours old reads fresh by the per-repository read and
stale by this one. The "two reads agree about staleness" claim therefore holds
everywhere except that hour, and the remaining exposure is production's rather
than the suite's. cvrlIntStaleStamp is 30 hours, not 25, which keeps every
fixture clear of the gap at either edge, so the agreement test holds under any
session timezone. At 25 hours it did not: both reads call that row stale, but
with zero margin against the day edge, and the skew between the fixture's
application-side stamp and the database's NOW() then decides the verdict.
Closing the gap for production means rendering exact hours in
freshWithinHoursExpr, which the per-repository container read and both npm
remote reads share, so it belongs in its own change. Tracked in
#1001.
The name label cardinality budget is nearly spent.
internal/metrics/cardinality.go sets labelName: 500. query_names.go now
declares 476 and internal/storage/queries.go 12, so 488 are spoken for
including this MR's 3. Four open MRs — !2030 (merged), !2035 (merged), !2123 (merged), !2040 (merged) — add 17
more, which crosses 500. AuditCardinality sees only observed values, so no
test fails when it does. The plan assigns the hand-raise to Step 4 and this MR
correctly leaves the file alone, but the arithmetic already crosses from work in
flight.
Size
3,192 reviewable LOC, past the 500 ceiling. Split by file group:
| Group | LOC | Files |
|---|---|---|
| Production Go | 472 | the datastore read 356, the oci adapter 109, query_names.go 7 |
| In-package doc | 236 | container_virtual_remote_lookup_predicates.md |
| Tests | 2,484 | datastore integration 1,378, datastore unit 414, datastore explain 312, oci unit 241, oci internal 139 |
Measured at d914bce94.
Splitting was considered and rejected. The production halves would split by
package along S31's precedent for the identical step, but the 2,484 test LOC
follow whichever half they exercise: the datastore side would still carry
~2,600 LOC and the oci side ~470. That trades one over-ceiling MR for one
over-ceiling MR plus a stack, a rewritten history, and a step marker duplicated
across two titles. The production code is 472 LOC, under the ceiling on its
own, and the test bulk is what the plan's Step 6 Tests bullet asked for —
statement counts at 2 and 20 positions, freshness agreement with the
per-repository reader, and an EXPLAIN suite. The plan's own size section
(## Step sizing) states that every step of this plan trips the gate and
carries this justification.
Testing
- Unit and integration suites for both new files, green locally against a lock-tuned Postgres.
- The
EXPLAINsuite asserts one partition per table at 2 and at 20 positions. 20 is ADR-004's upstream cap. TestContainerVirtualRemoteLookupStore_StatementCountIsIndependentOfPositionCountpins the statement count against the query counter.- Freshness agreement with
ContainerRemoteCacheStore.Lookupacross four window-and-age combinations. - The ETag claim is asserted through
conditionalETag's exported equivalent rather than on the field, because a populated field is not the property that matters. golangci-lint run --build-tags=integrationreports nothing on the new files beyondcontextcheck, which fires on every sibling integration suite and never reaches CI.
e2e scenarios
docs/testing/ is untouched. Step 18 of the plan owns the hermetic end-to-end
proxy harness and the container scenario catalogs, so no scenario is added or
affected here.
Notes for the reviewer
- The plan's Status-table row for this step is recorded in the S32 batched
docs(plans)MR, not here — guardrail 4 gives the plan file a single writer. container_virtual_remote_lookup_predicates.mdis a fifth file, beyond the plan's four-fileFiles:list.internal/datastorealready carriesnpm_virtual_remote_lookup_join_order.md, andinternal/format/npmcarriesvirtual_remote_upstream.md, so an in-package note for a batched read has precedent — despite AGENTS.md putting project documentation underdocs/.ByPositionsdoes not check that a position belongs to the virtual repository. That is the producing read's gate, documented under## Positions are trusted, and nothing enforces ADR-004's 20-upstream cap yet because no container virtual write path exists.- The access path is deliberately unpinned. The
EXPLAINsuite asserts partition count and table set, not scan method, for the reasoncontainer_remote_cache_explain_integration_test.go's header gives: at one row per table every join order costs the same, so anIndex Condassertion would pin a fixture artifact rather than the plan production wants.container_virtual_remote_lookup_predicates.mdrecords that under## Partition pruning, so the deferral is stated in the tree and not only here. The npm sibling made the opposite call for the sameIN-list rewrite.npm_virtual_remote_lookup_join_order.mdrecords a measurement at two tenant sizes and at one, two and 20 positions, andnpm_virtual_remote_lookup_join_order_integration_test.goguards the drive across PostgreSQL 16, 17 and 18. Container defers that fixture, and what it defers is real: no index on the tag table or either digest table leads with the columns this predicate binds directly —container_remote_image_idis the second column of all three unique indexes and arrives only through the join — so a leaf-driven plan scans the tenant's whole partition, and the 20-wideINlist is the input that could move the planner there. Building the container twin of that fixture is the close, and this MR does not.
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.ContainerVirtualRemoteLookupStore.blobByPositions |
Nested Loop | container_remote_blobs_p58_namespace_id_container_remote_im_idx |
3 / 3 | 28.72 | 0.032ms | 15 / 0 | 1/64, 1/64, 1/64, 1/64 |
datastore.ContainerVirtualRemoteLookupStore.manifestByPositions |
Nested Loop | container_remote_manifests_p5_namespace_id_container_remot_idx8 |
3 / 3 | 28.72 | 0.033ms | 15 / 0 | 1/64, 1/64, 1/64, 1/64 |
datastore.ContainerVirtualRemoteLookupStore.tagByPositions |
Nested Loop | container_remote_tags_p58_namespace_id_container_remote_ima_idx, container_remote_manifests_p58_pkey |
1 / 3 | 49.46 | 0.264ms | 28 / 0 | 1/64, 1/64, 1/64, 1/64, 1/64 |
datastore.ContainerVirtualRemoteLookupStore.blobByPositions
Summary: The plan matches the method's intent. The planner drives from
container_remote_images, then probes the digest unique index once per
position, so the three-position IN list costs three index lookups rather
than a partition scan. Every scanned table prunes to partition p58, and no
anomalies appear.
Seed shape: namespaces=1, blob_storage_blobs=1, blob_storage_attachments=1, repositories=3, container_remote_repositories=3, container_remote_images=3, container_remote_blobs=5003, container_remote_manifests=5003, container_remote_tags=5006
Rendered SQL:
SELECT container_remote_images.container_remote_repository_id AS "container_remote_images.container_remote_repository_id",
container_remote_blobs.blob_sha256 AS "container_remote_blobs.blob_sha256"
FROM public.container_remote_blobs
INNER JOIN public.container_remote_images ON ((container_remote_images.id = container_remote_blobs.container_remote_image_id) AND (container_remote_images.namespace_id = container_remote_blobs.namespace_id))
INNER JOIN public.container_remote_repositories ON ((container_remote_repositories.id = container_remote_images.container_remote_repository_id) AND (container_remote_repositories.namespace_id = container_remote_images.namespace_id))
INNER JOIN public.repositories ON ((repositories.id = container_remote_repositories.repository_id) AND (repositories.namespace_id = container_remote_repositories.namespace_id))
WHERE (((container_remote_blobs.namespace_id = $1::uuid) AND (container_remote_blobs.digest = $2::bytea)) AND (container_remote_blobs.soft_deleted_at IS NULL)) AND (((((((container_remote_images.namespace_id = $3::uuid) AND (container_remote_images.container_remote_repository_id IN ($4::uuid, $5::uuid, $6::uuid))) AND (container_remote_images.name = $7::text)) AND (container_remote_images.soft_deleted_at IS NULL)) AND (container_remote_repositories.namespace_id = $8::uuid)) AND (container_remote_repositories.id IN ($9::uuid, $10::uuid, $11::uuid))) AND ((repositories.namespace_id = $12::uuid) AND (((repositories.format IN ($13, $14)) AND (repositories.kind = $15)) AND (repositories.soft_deleted_at IS NULL))));Bound args: [aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, \x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, library/nginx, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 0, 3, 2]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Nested Loop (cost=0.41..28.72 rows=3 width=49) (actual time=0.024..0.032 rows=3 loops=1)
Join Filter: (container_remote_repositories.repository_id = repositories.id)
Rows Removed by Join Filter: 3
Buffers: shared hit=15
-> Nested Loop (cost=0.41..27.54 rows=3 width=81) (actual time=0.019..0.026 rows=3 loops=1)
Join Filter: (container_remote_repositories.id = container_remote_images.container_remote_repository_id)
Rows Removed by Join Filter: 3
Buffers: shared hit=14
-> Nested Loop (cost=0.41..26.38 rows=3 width=65) (actual time=0.014..0.021 rows=3 loops=1)
Buffers: shared hit=13
-> Seq Scan on container_remote_images_p58 container_remote_images (cost=0.00..1.06 rows=3 width=48) (actual time=0.004..0.005 rows=3 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (name = 'library/nginx'::text) AND (container_remote_repository_id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Buffers: shared hit=1
-> Index Scan using container_remote_blobs_p58_namespace_id_container_remote_im_idx on container_remote_blobs_p58 container_remote_blobs (cost=0.41..8.43 rows=1 width=65) (actual time=0.005..0.005 rows=1 loops=3)
Index Cond: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (container_remote_image_id = container_remote_images.id) AND (digest = '\x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'::bytea))
Buffers: shared hit=12
-> Materialize (cost=0.00..1.06 rows=3 width=48) (actual time=0.001..0.002 rows=2 loops=3)
Buffers: shared hit=1
-> Seq Scan on container_remote_repositories_p58 container_remote_repositories (cost=0.00..1.05 rows=3 width=48) (actual time=0.002..0.002 rows=3 loops=1)
Filter: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Buffers: shared hit=1
-> Materialize (cost=0.00..1.07 rows=3 width=32) (actual time=0.001..0.002 rows=2 loops=3)
Buffers: shared hit=1
-> Seq Scan on repositories_p58 repositories (cost=0.00..1.05 rows=3 width=32) (actual time=0.002..0.003 rows=3 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::integer[])) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (kind = 2))
Buffers: shared hit=1
Planning:
Buffers: shared hit=307
Planning Time: 1.185 ms
Execution Time: 0.051 msTimings: planning 1.185ms, execution 0.051ms, total 1.236ms.
datastore.ContainerVirtualRemoteLookupStore.manifestByPositions
Summary: The plan matches the method's intent, and it is the
container_remote_manifests twin of the blob plan: an image-driven Nested
Loop with one digest index probe per position. Every scanned table prunes to
partition p58. No anomalies appear.
Seed shape: namespaces=1, blob_storage_blobs=1, blob_storage_attachments=1, repositories=3, container_remote_repositories=3, container_remote_images=3, container_remote_manifests=5003, container_remote_blobs=5003, container_remote_tags=5006
Rendered SQL:
SELECT container_remote_images.container_remote_repository_id AS "container_remote_images.container_remote_repository_id",
container_remote_manifests.blob_sha256 AS "container_remote_manifests.blob_sha256"
FROM public.container_remote_manifests
INNER JOIN public.container_remote_images ON ((container_remote_images.id = container_remote_manifests.container_remote_image_id) AND (container_remote_images.namespace_id = container_remote_manifests.namespace_id))
INNER JOIN public.container_remote_repositories ON ((container_remote_repositories.id = container_remote_images.container_remote_repository_id) AND (container_remote_repositories.namespace_id = container_remote_images.namespace_id))
INNER JOIN public.repositories ON ((repositories.id = container_remote_repositories.repository_id) AND (repositories.namespace_id = container_remote_repositories.namespace_id))
WHERE (((container_remote_manifests.namespace_id = $1::uuid) AND (container_remote_manifests.digest = $2::bytea)) AND (container_remote_manifests.soft_deleted_at IS NULL)) AND (((((((container_remote_images.namespace_id = $3::uuid) AND (container_remote_images.container_remote_repository_id IN ($4::uuid, $5::uuid, $6::uuid))) AND (container_remote_images.name = $7::text)) AND (container_remote_images.soft_deleted_at IS NULL)) AND (container_remote_repositories.namespace_id = $8::uuid)) AND (container_remote_repositories.id IN ($9::uuid, $10::uuid, $11::uuid))) AND ((repositories.namespace_id = $12::uuid) AND (((repositories.format IN ($13, $14)) AND (repositories.kind = $15)) AND (repositories.soft_deleted_at IS NULL))));Bound args: [aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, \x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, library/nginx, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 0, 3, 2]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Nested Loop (cost=0.41..28.72 rows=3 width=49) (actual time=0.023..0.033 rows=3 loops=1)
Join Filter: (container_remote_repositories.repository_id = repositories.id)
Rows Removed by Join Filter: 3
Buffers: shared hit=15
-> Nested Loop (cost=0.41..27.54 rows=3 width=81) (actual time=0.018..0.027 rows=3 loops=1)
Join Filter: (container_remote_repositories.id = container_remote_images.container_remote_repository_id)
Rows Removed by Join Filter: 3
Buffers: shared hit=14
-> Nested Loop (cost=0.41..26.38 rows=3 width=65) (actual time=0.013..0.020 rows=3 loops=1)
Buffers: shared hit=13
-> Seq Scan on container_remote_images_p58 container_remote_images (cost=0.00..1.06 rows=3 width=48) (actual time=0.004..0.005 rows=3 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (name = 'library/nginx'::text) AND (container_remote_repository_id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Buffers: shared hit=1
-> Index Scan using container_remote_manifests_p5_namespace_id_container_remot_idx8 on container_remote_manifests_p58 container_remote_manifests (cost=0.41..8.43 rows=1 width=65) (actual time=0.004..0.005 rows=1 loops=3)
Index Cond: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (container_remote_image_id = container_remote_images.id) AND (digest = '\x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'::bytea))
Buffers: shared hit=12
-> Materialize (cost=0.00..1.06 rows=3 width=48) (actual time=0.002..0.002 rows=2 loops=3)
Buffers: shared hit=1
-> Seq Scan on container_remote_repositories_p58 container_remote_repositories (cost=0.00..1.05 rows=3 width=48) (actual time=0.003..0.003 rows=3 loops=1)
Filter: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Buffers: shared hit=1
-> Materialize (cost=0.00..1.07 rows=3 width=32) (actual time=0.001..0.002 rows=2 loops=3)
Buffers: shared hit=1
-> Seq Scan on repositories_p58 repositories (cost=0.00..1.05 rows=3 width=32) (actual time=0.002..0.002 rows=3 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::integer[])) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (kind = 2))
Buffers: shared hit=1
Planning:
Buffers: shared hit=249
Planning Time: 0.977 ms
Execution Time: 0.052 msTimings: planning 0.977ms, execution 0.052ms, total 1.029ms.
datastore.ContainerVirtualRemoteLookupStore.tagByPositions
Summary: The plan matches the method's intent. The planner drives from
container_remote_images, probes container_remote_tags on the
(namespace_id, container_remote_image_id, name) unique index once per
position, then resolves each tag's manifest by primary key, so the in-SQL
freshness expression evaluates over three rows. Every scanned table prunes to
partition p58. The root estimate of 1 row against 3 actual is a 3x
divergence, which is the join-selectivity floor and not a stats problem.
Seed shape: namespaces=1, blob_storage_blobs=1, blob_storage_attachments=1, repositories=3, container_remote_repositories=3, container_remote_images=3, container_remote_manifests=5003, container_remote_blobs=5003, container_remote_tags=5006
Rendered SQL:
SELECT container_remote_images.container_remote_repository_id AS "container_remote_images.container_remote_repository_id",
container_remote_manifests.blob_sha256 AS "container_remote_manifests.blob_sha256",
container_remote_tags.upstream_etag AS "container_remote_tags.upstream_etag",
((container_remote_repositories.cache_validity_hours = $1) OR (container_remote_tags.upstream_checked_at > (NOW() - (INTERVAL '1 HOUR' * container_remote_repositories.cache_validity_hours)))) AS "fresh"
FROM public.container_remote_tags
INNER JOIN public.container_remote_manifests ON ((((container_remote_manifests.namespace_id = $2::uuid) AND (container_remote_manifests.id = container_remote_tags.container_remote_manifest_id)) AND (container_remote_manifests.container_remote_image_id = container_remote_tags.container_remote_image_id)) AND (container_remote_manifests.soft_deleted_at IS NULL))
INNER JOIN public.container_remote_images ON ((container_remote_images.id = container_remote_tags.container_remote_image_id) AND (container_remote_images.namespace_id = container_remote_tags.namespace_id))
INNER JOIN public.container_remote_repositories ON ((container_remote_repositories.id = container_remote_images.container_remote_repository_id) AND (container_remote_repositories.namespace_id = container_remote_images.namespace_id))
INNER JOIN public.repositories ON ((repositories.id = container_remote_repositories.repository_id) AND (repositories.namespace_id = container_remote_repositories.namespace_id))
WHERE ((container_remote_tags.namespace_id = $3::uuid) AND (container_remote_tags.name = $4::text)) AND (((((((container_remote_images.namespace_id = $5::uuid) AND (container_remote_images.container_remote_repository_id IN ($6::uuid, $7::uuid, $8::uuid))) AND (container_remote_images.name = $9::text)) AND (container_remote_images.soft_deleted_at IS NULL)) AND (container_remote_repositories.namespace_id = $10::uuid)) AND (container_remote_repositories.id IN ($11::uuid, $12::uuid, $13::uuid))) AND ((repositories.namespace_id = $14::uuid) AND (((repositories.format IN ($15, $16)) AND (repositories.kind = $17)) AND (repositories.soft_deleted_at IS NULL))));Bound args: [0, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, latest, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, library/nginx, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 11111111-1111-4111-8111-111111111111, 22222222-2222-4222-8222-222222222222, 33333333-3333-4333-8333-333333333333, aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa, 0, 3, 2]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Nested Loop (cost=0.69..49.46 rows=1 width=82) (actual time=0.247..0.264 rows=3 loops=1)
Buffers: shared hit=28
-> Nested Loop (cost=0.69..48.37 rows=1 width=123) (actual time=0.026..0.042 rows=3 loops=1)
Buffers: shared hit=25
-> Nested Loop (cost=0.69..47.30 rows=1 width=105) (actual time=0.023..0.037 rows=3 loops=1)
Buffers: shared hit=22
-> Nested Loop (cost=0.41..22.36 rows=3 width=120) (actual time=0.015..0.024 rows=3 loops=1)
Buffers: shared hit=13
-> Seq Scan on container_remote_images_p58 container_remote_images (cost=0.00..1.06 rows=3 width=48) (actual time=0.005..0.006 rows=3 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (name = 'library/nginx'::text) AND (container_remote_repository_id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Buffers: shared hit=1
-> Index Scan using container_remote_tags_p58_namespace_id_container_remote_ima_idx on container_remote_tags_p58 container_remote_tags (cost=0.41..8.43 rows=1 width=88) (actual time=0.005..0.005 rows=1 loops=3)
Index Cond: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (container_remote_image_id = container_remote_images.id) AND (name = 'latest'::text))
Buffers: shared hit=12
-> Index Scan using container_remote_manifests_p58_pkey on container_remote_manifests_p58 container_remote_manifests (cost=0.28..8.30 rows=1 width=65) (actual time=0.004..0.004 rows=1 loops=3)
Index Cond: ((id = container_remote_tags.container_remote_manifest_id) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid))
Filter: ((soft_deleted_at IS NULL) AND (container_remote_tags.container_remote_image_id = container_remote_image_id))
Buffers: shared hit=9
-> Seq Scan on container_remote_repositories_p58 container_remote_repositories (cost=0.00..1.06 rows=1 width=50) (actual time=0.001..0.001 rows=1 loops=3)
Filter: ((namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (id = container_remote_images.container_remote_repository_id) AND (id = ANY ('{11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222,33333333-3333-4333-8333-333333333333}'::uuid[])))
Rows Removed by Filter: 1
Buffers: shared hit=3
-> Seq Scan on repositories_p58 repositories (cost=0.00..1.06 rows=1 width=32) (actual time=0.001..0.001 rows=1 loops=3)
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::integer[])) AND (namespace_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'::uuid) AND (kind = 2))
Rows Removed by Filter: 1
Buffers: shared hit=3
Planning:
Buffers: shared hit=1608 read=1
Planning Time: 3.634 ms
Execution Time: 0.292 msTimings: planning 3.634ms, execution 0.292ms, total 3.926ms.
Query notes:
- All three statements ran at three positions, the shape the resolver sends
today.
## Notes for the reviewerrecords that no index on the tag table or either digest table leads withcontainer_remote_image_id, so a leaf-driven plan would scan the tenant's whole partition. At three positions the planner drove fromcontainer_remote_imagesin every case and never chose that path. Whether a 20-wideINlist moves it is what the deferred container join-order fixture measures; this evidence does not settle it. - The rendered
SELECTs carry noLIMIT. Each returns at most one row per position, becauseunique_container_remote_tags_ns_id_image_id_nameand the twounique_..._ns_id_image_id_digestindexes make the bound predicate unique per image, andunique_container_remote_images_ns_id_repo_id_namemakes the image unique per position. The result therefore grows with the caller's position list and not with the table.## Positions are trustedincontainer_virtual_remote_lookup_predicates.mdrecords that nothing caps that list yet, and that ADR-004's 20-upstream cap is a write-time rule with no container virtual write path to enforce it.
Related to #291