feat(managementapi): serialize remote settings on detail and list, and complete the npm remote store
The Maven and container remote repository stores are being built to npm's precedent, but npm itself recorded two gaps: UpdateNpmRemoteRepositoryInput cannot patch the metadata cache window (its own doc comment says so), and the management list has no page-bounded batch details read. npm also lacks the health reset the spec requires on a URL change, so the stored verdict describes a host the repository no longer points at. This MR closes all three so the formats present one store contract to the management handler steps, and the settings PATCH consumes the patch field and the reset. The list serialization that consumes the batch read now rides this branch too, because its MR merged here rather than into main: feat(managementapi): serialize remote settings ... (!1625 - merged) • Hayley Swimelar • 19.4.
Plan: 2026-08-13-s17-phase6-virtual-remote-repositories.md, Steps 8 and 12. Spec: S17-rest-management-api.md, the "Non-hosted repository CRUD" and "Remote repository settings" sections.
What (non-obvious parts)
npmRemoteReadStmtnow takes a caller-supplied repository predicate, so the singular finders and the new batch read share one spelling of the projection, partition key, and parent gate. The singular rendered SQL is unchanged.- The batch read's error wrap omits the namespace id, unlike its three older siblings: docs/dev/database-query-patterns.md rules that new store methods do not copy the id-bearing wraps.
- The health reset rides the same UPDATE as the URL write and fires only when the normalized URL actually changes. A respelled URL resets nothing. Unit tests pin the SET-list order.
- The EXPLAIN pin for the batch statement (single-partition pruning on both partitioned tables) lives in
npm_remote_explain_integration_test.go. - Step 12:
detail.gosplits a details miss two ways. A remote row that vanished under the read is a404, and a details row missing under a live parent is a500, because a page has no per-row not-found to give. - Step 12:
fillFamilySettingsgroups a page by format family and runs one batch read per family present, so a mixed page costs one read per family rather than one per row. - Step 12:
mavenBatchDetailsis the only hand-written reader wrapper of the three. The maven store's map carries pointers, so a present-but-nil entry is normalized to the missing-row error rather than dereferenced. - Step 12: the response
settingsis a closedoneOfper format family, so a response payload satisfies exactly one branch. Requests use a permissive union instead, the direction split the spec fixes.
Diff size
4512 lines across 26 files. Two steps ride this branch: Step 8's store work in internal/datastore/, and Step 12's serialization across internal/managementapi/, cmd/artifact-registry/, and api/openapi/v1.yaml. The merged plan fixes each as one MR, and they became one diff when Step 12's MR merged here rather than into main, so splitting them again would mean reverting that merge. Production delta by group: store 202+/48-, management API 574+/24-, wiring 187+/22-, and the OpenAPI contract 221+/7-, which is 1184+/101- of the total. The rest is test suites the project's parity rules require (extend without dropping subtests, cover every enumerated value positively).
Test plan
go test ./internal/datastore/ -count=1go test -tags=integration ./internal/datastore/ -run 'TestNpmRemote' -count=1(testcontainers PostgreSQL)go test ./internal/managementapi/ ./cmd/artifact-registry/ -count=1go test -tags=integration ./internal/managementapi/ -run 'TestRemoteSettingsIntegration' -count=1(testcontainers PostgreSQL)
The suites pin: metadata-window patch isolation, CHECK-refusal classification with the aborted-transaction residue, the health-reset matrix (re-point, respell, non-URL patch), batch projection parity with the singular read, per-row parent gates, cross-namespace omission, duplicate collapse, the all-miss outcome, the caller-handle contract (tx visibility and pool isolation), the id-set key predicate, and the EXPLAIN partition pin. Step 12's suites pin: the per-format settings serialization matrix, the closed response oneOf contract, the detail route's 404/500 miss split, one batch read per family on a mixed page, and each family's batch-error passthrough as a 500 that names the datastore failure.
No docs/testing/ e2e scenario is added: the catalogs cover protocol-client journeys, and this is read-side management-API serialization, covered by the settings read integration suite.
The test commit carries this spec-coverage table.
Spec coverage
Spec: docs/specs/S17-rest-management-api.md Plan: docs/plans/2026-08-13-s17-phase6-virtual-remote-repositories.md, Step 8
Scope: Step 8, the internal/datastore npm remote repository store. Step 12's
serialization coverage rides its own MR's table. Acceptance
criteria are numbered by position; the Phase 6 block opens at #68.
Acceptance criteria
| # | Criterion (this step's store slice) | Tests |
|---|---|---|
| AC-74 | List serializes settings for remote rows with a page-bounded batch fetch, and cleartext credentials appear in no response |
TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs (row-for-row parity with the singular read via cmp.Diff, per-row parent gate over every format and kind value, cross-namespace omission, duplicate collapse, outage-is-an-error), TestFindNpmRemoteDetailsBatchStmt (the token column is never selected; has_credentials computed in SQL), TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs_ArgumentGuards (guards + empty set without a round trip), TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs_PrunesToOnePartition. The serialization itself is Step 12's, which rides this branch and carries its own coverage table |
| AC-75 | PATCH merges settings by field: a field absent from the body is unchanged |
Metadata-window half (this step): TestUpdateNpmRemoteRepositoryStmt/a metadata window alone writes only that column, TestUpdateNpmRemoteRepositoryInput_IsZero, TestNpmRemoteRepositoryStore_UpdateNpmRemoteRepository_MetadataWindow (patch-alone reads back every other column; nil keeps the stored value across a url change). Cache-window and credential halves: pre-existing TestNpmRemoteRepositoryStore_UpdateNpmRemoteRepository, unchanged and passing |
| AC-76 | PATCH changing url without credentials clears them in the same transaction and emits the audit event |
Pre-existing TestNpmRemoteRepositoryStore_UpdateNpmRemoteRepository and TestNpmRemoteRepositoryStore_CredentialWriteAudit, unchanged and passing (the reverse dropped-subtest rule); credentials_cleared serialization is Step 34's |
| AC-77 | PATCH changing url resets last_health_status to unknown and last_health_checked_at to null in the same transaction |
TestUpdateNpmRemoteRepositoryStmt (both url-change cases pin the reset in the same SET list as the url write), TestNpmRemoteRepositoryStore_UpdateNpmRemoteRepository_HealthReset (reset on re-point; respelled url and every url-less patch shape reset neither column). The failure-counter reset after commit is distributed state the handler owns (Step 34) |
| AC-83 | Invalid url returns 400 on update |
Store slice: the remote.ParseUpstreamBase reject passthrough, pre-existing TestNpmRemoteRepositoryStore_UpdateNpmRemoteRepository_ArgumentGuards/invalid url. The 422 mapping is Step 23's. The update-side 400 is Step 34's, where url becomes writable |
| AC-84 | npm public-registry create pins cache_validity_hours to 0 |
Pre-existing create suite (TestNpmRemoteCacheValidityHours, TestNpmRemoteRepositoryStore_CreateNpmRemoteRepository), untouched |
| AC-1 to AC-73, AC-78 to AC-82, AC-85 to AC-123 | Phase 1-5 surface, eviction, audit actors, virtual repositories, the connection test, contract documents, and every route-level criterion | Not Step 8. The handler, route, and contract surface on this branch is Step 12's, under its own MR's coverage table |
Behavior obligations from the spec prose
| # | Obligation | Tests |
|---|---|---|
| B-1 | Settings patch by field (Non-hosted repository CRUD): the metadata window patches without touching other fields | ..._MetadataWindow/patches the metadata window without touching other columns (url, token, cache window, and both health columns read back unchanged), TestUpdateNpmRemoteRepositoryStmt/a metadata window alone writes only that column |
| B-2 | The URL-change health reset is one transaction with the url write | TestUpdateNpmRemoteRepositoryStmt pins both columns in the same SET list as the url write (one statement, so one transaction by construction); ..._HealthReset/a url change resets the health verdict in the same transaction pins the committed state |
| B-3 | The list's fetch is page-bounded, one child-table read per format family, each a unique-index lookup set | One named statement (findNpmRemoteDetailsBatchStmt) resolves the whole id set: the store method and the EXPLAIN pin bind that one construction, and the empty-input guard proves no degenerate round trip. A per-row fallback cannot satisfy the parity suite's map return |
| B-4 | metadata_cache_validity_hours: default 24, at least 1, at most 32767 (Remote repository settings) |
Default: pre-existing create suite. Floor: the DB CHECK owns it; ..._MetadataWindow/maps a metadata-window CHECK refusal... pins the boundary value 0 classifying to ErrNpmRemoteRepositoryCheckViolation (0 is legal for the cache window one column over, so a conflated bound fails here). Cap: the *int16 input carries the smallint bound structurally; the boundary 400 is Step 23's |
| B-5 | last_health_status and last_health_checked_at are read-only fields |
Structural in the store: UpdateNpmRemoteRepositoryInput carries no health fields, so no patch can write them except the URL-change reset; the reads project stored values (parity suite). The 422 on a read-only field in a body is Step 23's |
| B-6 | A respelled url is not a change (normalization owned by remote.ParseUpstreamBase) |
Pre-existing respell subtests (unchanged), plus ..._HealthReset/a respelled url does not reset the health verdict |
Plan obligations (Step 8)
| # | Obligation | Tests |
|---|---|---|
| P-1 | MetadataCacheValidityHours on UpdateNpmRemoteRepositoryInput, closing the documented gap |
TestUpdateNpmRemoteRepositoryStmt (metadata-alone and every-column cases), TestUpdateNpmRemoteRepositoryInput_IsZero, ..._MetadataWindow |
| P-2 | The in-transaction health reset on URL change, the shape Steps 5 and 7 mirror | TestUpdateNpmRemoteRepositoryStmt url-change cases, ..._HealthReset |
| P-3 | The page-bounded batch FindRemoteDetailsByRepositoryIDs |
The batch suites in AC-74's row |
| P-4 | Acceptance: the update patches the metadata window without touching other fields | ..._MetadataWindow/patches the metadata window without touching other columns |
| P-5 | Acceptance: the batch read matches the single read's projection | TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs/matches the singular read row for row (cmp.Diff against the singular read per id, no ignores) |
| P-6 | The dropped-subtest rule in reverse: no existing subtest lost | No subtest removed or renamed away; three unit stmt cases extended append-only (health tail on wantSet/wantArgs, metadata field on the every-column case). Every pre-existing unit and integration subtest passes against the skeleton, verified with -skip over the new coverage |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-1 | Create, Update: an out-of-range cache window returns 400 |
Store slice: the CHECK refusal classifies to ErrNpmRemoteRepositoryCheckViolation and leaves the transaction aborted, so a caller must roll back before rendering a body — ..._MetadataWindow/maps a metadata-window CHECK refusal and leaves the transaction aborted (SQLSTATE 25P02 pinned on the next statement), plus the pre-existing create-path metadata and cache CHECK tests. The 400 mapping is Step 23's |
| E-2 | Malformed store calls: nil context or handle, zero namespace | TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs_ArgumentGuards (recordingDB proves no round trip), mirroring the pre-existing guard tables |
| E-3 | Every route-level Error Cases row (400, 404, 409, 413, 422, 503, 405) |
Handler steps. This store has no request surface |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | Upstream credentials: write-only, surfaced only as has_credentials, cleared on URL change, audited, absent from logs and error bodies |
TestFindNpmRemoteDetailsBatchStmt (the token column is never a selected column; the presence flag is computed in SQL), the parity suite's three token states with assertHasCredentialsParity, the CHECK-refusal subtest's failing-row hygiene (requireNoPgErrorInChain, token absent from the message). Clear-on-URL-change and the audit events: pre-existing suites, unchanged and passing |
| S-2 | Tenant isolation: every query scoped by the resolved namespace_id |
TestNpmRemoteRepositoryStore_FindRemoteDetailsByRepositoryIDs/does not return another namespace's rows, TestFindNpmRemoteDetailsBatchStmt (partition key in the predicate), the EXPLAIN pin (single partition on both tables) |
| S-3 | Injection: all queries use the Jet builder with bound parameters | TestFindNpmRemoteDetailsBatchStmt and TestUpdateNpmRemoteRepositoryStmt assert $n placeholders and bound args throughout |
| S-4 | SSRF and URL validation | Owned by remote.ParseUpstreamBase (S13); the store's reject passthrough is pinned by the pre-existing argument-guard tests |
| S-5 | Authentication, authorization, existence hiding, echoed input, audit actors | Not Step 8's store surface. Step 12's request surface rides this branch, under its own MR's coverage table |
Context for LLM agents
Rejected alternatives:
- A third delegation layer under an unchanged
npmRemoteReadStmt: deeper for no gain. - A store-enforced cardinality cap on the batch read: the caller bounds the set to one list page, and that caller is now in this diff rather than promised by a later step. A duplicate bound invites drift.
- An id-bearing error wrap: ruled out by
docs/dev/database-query-patterns.md.
Non-goals:
- Handler wiring and settings serialization were out of scope for Step 8. They are in this diff now, because Step 12 merged into this branch instead of into main.
- The consecutive-failure counter reset is a post-commit distributed-state write owned by the handler.
- No npm prober exists yet, so the stale-in-flight-probe window the spec accepts is unreachable today.
- The pre-existing
contextcheckfindings on the shared seed helpers are a package-wide class, deliberately left to avoid colliding with sibling branches.
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.
Two conventions in this section. The three 100-id statements carry a
100-element uuid[] inline in both the rendered SQL and the plan; the
middle of each run is elided with a marked comment, and nothing else in
either text is altered. And the container and Maven rows measure
statements this MR does not change: the list path it adds is their first
caller at a full page, so they are here as pre-existing statements newly
exercised by this MR's list path, not as changed ones.
| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|---|---|---|---|---|---|---|---|
NpmRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (20-id page) |
Nested Loop | npm_remote_repositories_p07_namespace_id_repository_id_idx, repositories_p07_namespace_id_id_format_idx |
20 / 20 | 219.63 | 0.058ms | 64 / 0 | 1/64, 1/64 |
NpmRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page) |
Hash Join | npm_remote_repositories_p07_namespace_id_repository_id_idx (bitmap); parent side Seq Scan |
100 / 100 | 325.63 | 0.763ms | 92 / 0 | 1/64, 1/64 |
NpmRemoteRepositoryStore.UpdateNpmRemoteRepository.MetadataWindowOnly |
Update | n/a at 50 rows; npm_remote_repositories_p07_pkey at 5000 |
1 / 1 | 1.75 | 0.078ms | 16 / 0 | 1/64 |
NpmRemoteRepositoryStore.UpdateNpmRemoteRepository.URLChange |
Update | n/a at 50 rows; npm_remote_repositories_p07_pkey at 5000 |
1 / 1 | 1.75 | 0.081ms | 16 / 0 | 1/64 |
ContainerRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page, pre-existing statement) |
Hash Join | container_remote_repositories_p_namespace_id_repository_id_idx7 (bitmap); parent side Seq Scan |
100 / 100 | 333.60 | 0.849ms | 93 / 0 | 1/64, 1/64 |
MavenRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page, pre-existing statement) |
Hash Join | maven_remote_repositories_p07_namespace_id_repository_id_idx (bitmap); parent side Seq Scan |
100 / 100 | 338.98 | 0.781ms | 92 / 0 | 1/64, 1/64 |
Query notes:
- All three 100-id batch reads plan a Seq Scan over
repositories_p07rather than probing the parent gate per row. The cause is the seed, not the statement: the skill's recipe puts all 5000 seededrepositoriesrows in the queried namespace, sonamespace_id = $n AND format = … AND kind = 2 AND soft_deleted_at IS NULLselects the whole partition and one sequential pass beats 100 index probes. Re-seeded so the queried namespace is a fifth of the partition — 5000 of 25000repositoriesrows, the rest spread over 40 neighbour tenants hashed into the same partition — every one of the three reverts to a Nested Loop with an Index Scan on both sides and gets four to five times faster in wall time (npm 0.801ms → 0.181ms, container 0.885ms → 0.303ms, Maven 0.824ms → 0.173ms). The 25000 total was read back on the npm control; the container and Maven controls use the identical seed block. Each<details>block carries both plans. No index or predicate change is proposed. - The FK-trigger figures in the two UPDATE footers supersede the ones
this section carried before. Across the four vacuum-baseline runs
published here,
fk_npm_remote_repositories_namespace_id_namespacesmeasured 0.298ms to 0.326ms andfk_npm_remote_repositories_repository_id_repositories0.770ms to 0.829ms, at 50 and 5000 seeded rows alike, so neither tracks partition size and no FK column is in either SET list. The earlier 7.918ms and 0.115ms readings did not reproduce in any run whose footer was captured. - Every plan here was taken after
VACUUM (FULL, ANALYZE)on the seeded tables and confirmed unchanged on a second pass. Without that reset the write-target plans move within a single container: run back to back,URLChangeat 50 rows planned a Bitmap Heap Scan onnpm_remote_repositories_p07_pkeyat cost 12.29 instead of the Seq Scan at 1.75, andMetadataWindowOnlyonce planned at cost 2.75 with 23 buffers instead of 1.75 and 16.EXPLAIN ANALYZEinside a rolled-back transaction leaves its dead tuples behind, so the write target's page count grows across a run and shifts the Seq Scan's cost. - The batch reads carry no
LIMIT, which the unbounded-SELECTrule exempts as a caller-cappedIN (...)on a key column: the id set is one list page,managementapi.parseLimitParamclamps that page withmin(limit, maxPageSize)atmaxPageSize = 100, and each(namespace_id, repository_id)pair is unique, so the result cannot exceed the input. The container and Maven stores additionally reject an oversized set in-store at 100; npm rests on the caller contract alone.
NpmRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (20-id page)
Summary: Plan matches the batched read's intent: the namespace literal prunes both partitioned tables to one of 64 partitions, the 20-id page drives an Index Scan over the partition child of unique_npm_remote_repositories_namespace_id_and_repository_id (20 / 20, exact estimate), and the parent gate is answered by an Index Only Scan on repositories_p07_namespace_id_id_format_idx — the child of index_repositories_on_namespace_id_id_format_remotes, whose kind = 2 AND soft_deleted_at IS NULL predicate absorbs those two arms, so the gate carries no Filter at all. The 20 heap fetches follow from the seed living inside the rolled-back EXPLAIN transaction, which leaves it outside the visibility map. No anomalies.
Seed shape: namespaces=1, repositories=5000, npm_remote_repositories=5000 (all rows in one namespace, so both partitioned tables hold all 5000 rows in a single partition; every even binding stores a token so has_credentials varies across the page)
Rendered SQL:
SELECT npm_remote_repositories.id AS "npm_remote_repositories.id",
npm_remote_repositories.repository_id AS "npm_remote_repositories.repository_id",
npm_remote_repositories.url AS "npm_remote_repositories.url",
((npm_remote_repositories.tmp_plaintext_auth_token IS NOT NULL) AND (npm_remote_repositories.tmp_plaintext_auth_token != $1::text)) AS "has_credentials",
npm_remote_repositories.cache_validity_hours AS "npm_remote_repositories.cache_validity_hours",
npm_remote_repositories.metadata_cache_validity_hours AS "npm_remote_repositories.metadata_cache_validity_hours",
npm_remote_repositories.last_health_status AS "npm_remote_repositories.last_health_status",
npm_remote_repositories.last_health_checked_at AS "npm_remote_repositories.last_health_checked_at"
FROM public.npm_remote_repositories
INNER JOIN public.repositories ON ((repositories.id = npm_remote_repositories.repository_id) AND (repositories.namespace_id = npm_remote_repositories.namespace_id))
WHERE ((npm_remote_repositories.namespace_id = $2::uuid) AND (npm_remote_repositories.repository_id IN ($3::uuid, $4::uuid, $5::uuid, $6::uuid, $7::uuid, $8::uuid, $9::uuid, $10::uuid, $11::uuid, $12::uuid, $13::uuid, $14::uuid, $15::uuid, $16::uuid, $17::uuid, $18::uuid, $19::uuid, $20::uuid, $21::uuid, $22::uuid))) AND ((((repositories.namespace_id = $23::uuid) AND (repositories.format = $24)) AND (repositories.kind = $25)) AND (repositories.soft_deleted_at IS NULL));Bound args: ['', 11111111-1111-4111-8111-111111111111, 00000000-0000-4000-8000-0000000007d1, 00000000-0000-4000-8000-0000000007d2, 00000000-0000-4000-8000-0000000007d3, 00000000-0000-4000-8000-0000000007d4, 00000000-0000-4000-8000-0000000007d5, 00000000-0000-4000-8000-0000000007d6, 00000000-0000-4000-8000-0000000007d7, 00000000-0000-4000-8000-0000000007d8, 00000000-0000-4000-8000-0000000007d9, 00000000-0000-4000-8000-0000000007da, 00000000-0000-4000-8000-0000000007db, 00000000-0000-4000-8000-0000000007dc, 00000000-0000-4000-8000-0000000007dd, 00000000-0000-4000-8000-0000000007de, 00000000-0000-4000-8000-0000000007df, 00000000-0000-4000-8000-0000000007e0, 00000000-0000-4000-8000-0000000007e1, 00000000-0000-4000-8000-0000000007e2, 00000000-0000-4000-8000-0000000007e3, 00000000-0000-4000-8000-0000000007e4, 11111111-1111-4111-8111-111111111111, 2, 2] (empty-string token comparator; namespace; 20 repository ids, the seeded rows g=2001..2020; namespace again for the parent gate; format=2 npm; kind=2 remote)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Nested Loop (cost=0.56..219.63 rows=20 width=75) (actual time=0.032..0.058 rows=20 loops=1)
Buffers: shared hit=64
-> Index Scan using npm_remote_repositories_p07_namespace_id_repository_id_idx on npm_remote_repositories_p07 npm_remote_repositories (cost=0.28..77.28 rows=20 width=112) (actual time=0.012..0.017 rows=20 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{00000000-0000-4000-8000-0000000007d1,00000000-0000-4000-8000-0000000007d2,00000000-0000-4000-8000-0000000007d3,00000000-0000-4000-8000-0000000007d4,00000000-0000-4000-8000-0000000007d5,00000000-0000-4000-8000-0000000007d6,00000000-0000-4000-8000-0000000007d7,00000000-0000-4000-8000-0000000007d8,00000000-0000-4000-8000-0000000007d9,00000000-0000-4000-8000-0000000007da,00000000-0000-4000-8000-0000000007db,00000000-0000-4000-8000-0000000007dc,00000000-0000-4000-8000-0000000007dd,00000000-0000-4000-8000-0000000007de,00000000-0000-4000-8000-0000000007df,00000000-0000-4000-8000-0000000007e0,00000000-0000-4000-8000-0000000007e1,00000000-0000-4000-8000-0000000007e2,00000000-0000-4000-8000-0000000007e3,00000000-0000-4000-8000-0000000007e4}'::uuid[])))
Buffers: shared hit=4
-> Index Only Scan using repositories_p07_namespace_id_id_format_idx on repositories_p07 repositories (cost=0.28..7.10 rows=1 width=32) (actual time=0.002..0.002 rows=1 loops=20)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (id = npm_remote_repositories.repository_id) AND (format = '2'::smallint))
Heap Fetches: 20
Buffers: shared hit=60
Planning:
Buffers: shared hit=393
Planning Time: 1.608 ms
Execution Time: 0.083 msTimings: planning 1.608ms, execution 0.083ms, total 1.691ms.
NpmRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page)
Summary: A full page — managementapi.maxPageSize is 100, so this is the largest set the list path can hand the store — flips the join. The child side stays on the unique index as a Bitmap Index Scan (100 / 100, exact estimate, 5 buffers), but the parent gate becomes a Seq Scan of repositories_p07 and the join a Hash Join. That is the seed, not the statement: every seeded repositories row is an npm remote in the queried namespace, so the gate has no selectivity and one 87-buffer pass beats 100 index probes. The multi-tenant control below puts the queried namespace at a fifth of a 25000-row partition and the plan reverts to a Nested Loop with an Index Scan on both sides at 0.181ms. Partition pruning holds in both: one of 64 on each table. No anomalies.
Seed shape: namespaces=1, repositories=5000, npm_remote_repositories=5000 (the 20-id page's seed, unchanged). Control: namespaces=41, repositories=25000, npm_remote_repositories=25000 (queried namespace 5000, 40 neighbour tenants hashed into repositories_p07 at 500 each)
Rendered SQL:
SELECT npm_remote_repositories.id AS "npm_remote_repositories.id",
npm_remote_repositories.repository_id AS "npm_remote_repositories.repository_id",
npm_remote_repositories.url AS "npm_remote_repositories.url",
((npm_remote_repositories.tmp_plaintext_auth_token IS NOT NULL) AND (npm_remote_repositories.tmp_plaintext_auth_token != $1::text)) AS "has_credentials",
npm_remote_repositories.cache_validity_hours AS "npm_remote_repositories.cache_validity_hours",
npm_remote_repositories.metadata_cache_validity_hours AS "npm_remote_repositories.metadata_cache_validity_hours",
npm_remote_repositories.last_health_status AS "npm_remote_repositories.last_health_status",
npm_remote_repositories.last_health_checked_at AS "npm_remote_repositories.last_health_checked_at"
FROM public.npm_remote_repositories
INNER JOIN public.repositories ON ((repositories.id = npm_remote_repositories.repository_id) AND (repositories.namespace_id = npm_remote_repositories.namespace_id))
WHERE ((npm_remote_repositories.namespace_id = $2::uuid) AND (npm_remote_repositories.repository_id IN ($3::uuid, $4::uuid, /* ... 97 more ... */ $102::uuid))) AND ((((repositories.namespace_id = $103::uuid) AND (repositories.format = $104)) AND (repositories.kind = $105)) AND (repositories.soft_deleted_at IS NULL));Bound args: ['', 11111111-1111-4111-8111-111111111111, <100 repository ids, the seeded rows g=2001..2100, 00000000-0000-4000-8000-0000000007d1 through 00000000-0000-4000-8000-000000000834>, 11111111-1111-4111-8111-111111111111, 2, 2] (empty-string token comparator; namespace; the full page of ids; namespace again for the parent gate; format=2 npm; kind=2 remote)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Hash Join (cost=137.74..325.63 rows=100 width=75) (actual time=0.349..0.763 rows=100 loops=1)
Hash Cond: (repositories.id = npm_remote_repositories.repository_id)
Buffers: shared hit=92
-> Seq Scan on repositories_p07 repositories (cost=0.00..174.50 rows=5000 width=32) (actual time=0.006..0.463 rows=5000 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (format = '2'::smallint) AND (kind = '2'::smallint))
Buffers: shared hit=87
-> Hash (cost=136.49..136.49 rows=100 width=112) (actual time=0.061..0.061 rows=100 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 23kB
Buffers: shared hit=5
-> Bitmap Heap Scan on npm_remote_repositories_p07 npm_remote_repositories (cost=44.37..136.49 rows=100 width=112) (actual time=0.025..0.035 rows=100 loops=1)
Recheck Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Heap Blocks: exact=3
Buffers: shared hit=5
-> Bitmap Index Scan on npm_remote_repositories_p07_namespace_id_repository_id_idx (cost=0.00..44.10 rows=100 width=0) (actual time=0.021..0.021 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=2
Planning:
Buffers: shared hit=393
Planning Time: 1.640 ms
Execution Time: 0.801 msPlan, multi-tenant control (same statement and bound args, partition shared with 40 other tenants):
Nested Loop (cost=0.70..460.26 rows=20 width=75) (actual time=0.030..0.145 rows=100 loops=1)
Buffers: shared hit=306
-> Index Scan using npm_remote_repositories_p07_namespace_id_repository_id_idx on npm_remote_repositories_p07 npm_remote_repositories (cost=0.41..297.76 rows=20 width=112) (actual time=0.020..0.037 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=6
-> Index Scan using repositories_p07_pkey on repositories_p07 repositories (cost=0.29..8.11 rows=1 width=32) (actual time=0.001..0.001 rows=1 loops=100)
Index Cond: ((id = npm_remote_repositories.repository_id) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid))
Filter: ((soft_deleted_at IS NULL) AND (format = '2'::smallint) AND (kind = '2'::smallint))
Buffers: shared hit=300
Planning:
Buffers: shared hit=240
Planning Time: 1.249 ms
Execution Time: 0.181 msThe control's rows=20 estimate against 100 actual is the planner's independence assumption: it multiplies the 100-value id list by the namespace's fifth of the partition, not knowing the ids are all that namespace's. Five times under, inside the ten-times flag threshold, and it does not change the plan choice.
Timings: planning 1.640ms, execution 0.801ms, total 2.441ms. Control: planning 1.249ms, execution 0.181ms, total 1.430ms.
NpmRemoteRepositoryStore.UpdateNpmRemoteRepository.MetadataWindowOnly
Summary: Plan matches the patch's intent: pruned to one partition, the row resolved by a Seq Scan that filters out the 49 siblings. The 50-row partition is one page, so the Seq Scan costs 1.75 against 8.30 for the index path; re-running the same statement against a 5000-row partition switches it to an Index Scan on npm_remote_repositories_p07_pkey with both key columns as Index Cond, so the predicate does reach the index once the table is worth indexing. Scan rows are 1 / 1 — the Update root reports 0 because there is no RETURNING — and the footer's FK triggers are an artifact of seeding and updating in one transaction: the same pair measured 0.326ms and 0.829ms at 5000 seeded rows, so neither tracks partition size, and no FK column is in this SET list. No anomalies.
Seed shape: namespaces=1, repositories=50, npm_remote_repositories=50 (1 target row plus 49 siblings; the unique (namespace_id, repository_id) index forces one parent repositories row per binding). Re-check: the same shape at 5000 rows each.
Rendered SQL:
UPDATE public.npm_remote_repositories
SET metadata_cache_validity_hours = $1
WHERE (npm_remote_repositories.namespace_id = $2::uuid) AND (npm_remote_repositories.id = $3::uuid);Bound args: [48, 11111111-1111-4111-8111-111111111111, 00000000-0000-4000-9000-000000000019] (new metadata window in hours; namespace; the target row's npm_remote_repositories.id, the id the locked read resolves)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Update on npm_remote_repositories (cost=0.00..1.75 rows=0 width=0) (actual time=0.077..0.078 rows=0 loops=1)
Update on npm_remote_repositories_p07 npm_remote_repositories_1
Buffers: shared hit=16
-> Seq Scan on npm_remote_repositories_p07 npm_remote_repositories_1 (cost=0.00..1.75 rows=1 width=12) (actual time=0.005..0.007 rows=1 loops=1)
Filter: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (id = '00000000-0000-4000-9000-000000000019'::uuid))
Rows Removed by Filter: 49
Buffers: shared hit=1
Planning:
Buffers: shared hit=85
Planning Time: 0.455 ms
Trigger for constraint fk_npm_remote_repositories_namespace_id_namespaces on npm_remote_repositories_p07: time=0.315 calls=1
Trigger for constraint fk_npm_remote_repositories_repository_id_repositories on npm_remote_repositories_p07: time=0.809 calls=1
Execution Time: 1.365 msPlan at 5000 seeded rows (same statement and bound args):
Update on npm_remote_repositories (cost=0.28..8.30 rows=0 width=0) (actual time=0.105..0.106 rows=0 loops=1)
Update on npm_remote_repositories_p07 npm_remote_repositories_1
Buffers: shared hit=26
-> Index Scan using npm_remote_repositories_p07_pkey on npm_remote_repositories_p07 npm_remote_repositories_1 (cost=0.28..8.30 rows=1 width=12) (actual time=0.008..0.009 rows=1 loops=1)
Index Cond: ((id = '00000000-0000-4000-9000-000000000019'::uuid) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid))
Buffers: shared hit=3
Planning:
Buffers: shared hit=85
Planning Time: 0.422 ms
Trigger for constraint fk_npm_remote_repositories_namespace_id_namespaces on npm_remote_repositories_p07: time=0.326 calls=1
Trigger for constraint fk_npm_remote_repositories_repository_id_repositories on npm_remote_repositories_p07: time=0.829 calls=1
Execution Time: 1.416 msTimings: planning 0.455ms, execution 1.365ms, total 1.820ms. At 5000 rows: planning 0.422ms, execution 1.416ms, total 1.838ms.
NpmRemoteRepositoryStore.UpdateNpmRemoteRepository.URLChange
Summary: Same key shape and plan as MetadataWindowOnly: pruned to one partition, a one-page Seq Scan at 50 rows and an Index Scan on npm_remote_repositories_p07_pkey at 5000. The SET list is the whole URL-change write — new url, credential clear, and both health-reset columns in one statement — so the re-point cannot commit without the reset. Execution is 1.365ms at 50 rows and 1.331ms at 5000, of which the two FK triggers are 1.119ms and 1.068ms; they dominate both figures and track neither the SET list's width nor the partition's size. No anomalies.
Seed shape: namespaces=1, repositories=50, npm_remote_repositories=50 (the target row's stored token, health verdict, and probe timestamp are what the credential clear and both health-reset arms overwrite). Re-check: the same shape at 5000 rows each.
Rendered SQL:
UPDATE public.npm_remote_repositories
SET url = $1::text,
tmp_plaintext_auth_token = NULL,
last_health_status = $2,
last_health_checked_at = NULL
WHERE (npm_remote_repositories.namespace_id = $3::uuid) AND (npm_remote_repositories.id = $4::uuid);Bound args: ['https://registry.example.test/', 0, 11111111-1111-4111-8111-111111111111, 00000000-0000-4000-9000-000000000019] (new normalized URL; remote.HealthUnknown; namespace; the target row's npm_remote_repositories.id)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Update on npm_remote_repositories (cost=0.00..1.75 rows=0 width=0) (actual time=0.080..0.081 rows=0 loops=1)
Update on npm_remote_repositories_p07 npm_remote_repositories_1
Buffers: shared hit=16
-> Seq Scan on npm_remote_repositories_p07 npm_remote_repositories_1 (cost=0.00..1.75 rows=1 width=84) (actual time=0.006..0.008 rows=1 loops=1)
Filter: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (id = '00000000-0000-4000-9000-000000000019'::uuid))
Rows Removed by Filter: 49
Buffers: shared hit=1
Planning:
Buffers: shared hit=85
Planning Time: 0.521 ms
Trigger for constraint fk_npm_remote_repositories_namespace_id_namespaces on npm_remote_repositories_p07: time=0.324 calls=1
Trigger for constraint fk_npm_remote_repositories_repository_id_repositories on npm_remote_repositories_p07: time=0.795 calls=1
Execution Time: 1.365 msPlan at 5000 seeded rows (same statement and bound args):
Update on npm_remote_repositories (cost=0.28..8.30 rows=0 width=0) (actual time=0.103..0.104 rows=0 loops=1)
Update on npm_remote_repositories_p07 npm_remote_repositories_1
Buffers: shared hit=26
-> Index Scan using npm_remote_repositories_p07_pkey on npm_remote_repositories_p07 npm_remote_repositories_1 (cost=0.28..8.30 rows=1 width=84) (actual time=0.009..0.009 rows=1 loops=1)
Index Cond: ((id = '00000000-0000-4000-9000-000000000019'::uuid) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid))
Buffers: shared hit=3
Planning:
Buffers: shared hit=94
Planning Time: 0.405 ms
Trigger for constraint fk_npm_remote_repositories_namespace_id_namespaces on npm_remote_repositories_p07: time=0.298 calls=1
Trigger for constraint fk_npm_remote_repositories_repository_id_repositories on npm_remote_repositories_p07: time=0.770 calls=1
Execution Time: 1.331 msTimings: planning 0.521ms, execution 1.365ms, total 1.886ms. At 5000 rows: planning 0.405ms, execution 1.331ms, total 1.736ms.
ContainerRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page, pre-existing statement)
Summary: The container family's batch read is unchanged by this MR; the list path it adds is what first drives it at a full page, so it is measured here alongside npm's. Same shape as npm at 100 ids and the same cause: a Bitmap Index Scan on the unique (namespace_id, repository_id) index feeding a Hash Join whose parent side is a Seq Scan of repositories_p07, because the canonical seed leaves the parent gate with no selectivity. The gate's format = ANY('{0,3}') arm covers both container formats and stays a Filter rather than an Index Cond in the single-tenant plan, and an Index Cond on the pkey in the control. Both tables prune to one of 64 partitions. Control at a fifth namespace share: Nested Loop, 0.303ms. No anomalies.
Seed shape: namespaces=1, repositories=5000, container_remote_repositories=5000 (format alternating docker and oci so the two-value gate is exercised on both; every even binding stores a username and password pair, auth_status=0 with auth_url NULL as the CHECK requires). Control: namespaces=41, repositories=25000, container_remote_repositories=25000
Rendered SQL:
SELECT container_remote_repositories.id AS "container_remote_repositories.id",
container_remote_repositories.repository_id AS "container_remote_repositories.repository_id",
container_remote_repositories.url AS "container_remote_repositories.url",
(((container_remote_repositories.tmp_plaintext_username IS NOT NULL) AND (container_remote_repositories.tmp_plaintext_username != $1::text)) OR ((container_remote_repositories.tmp_plaintext_password IS NOT NULL) AND (container_remote_repositories.tmp_plaintext_password != $2::text))) AS "has_credentials",
container_remote_repositories.cache_validity_hours AS "container_remote_repositories.cache_validity_hours",
container_remote_repositories.last_health_status AS "container_remote_repositories.last_health_status",
container_remote_repositories.last_health_checked_at AS "container_remote_repositories.last_health_checked_at"
FROM public.container_remote_repositories
INNER JOIN public.repositories ON ((repositories.id = container_remote_repositories.repository_id) AND (repositories.namespace_id = container_remote_repositories.namespace_id))
WHERE ((container_remote_repositories.namespace_id = $3::uuid) AND (container_remote_repositories.repository_id IN ($4::uuid, $5::uuid, /* ... 97 more ... */ $103::uuid))) AND ((repositories.namespace_id = $104::uuid) AND (((repositories.format IN ($105, $106)) AND (repositories.kind = $107)) AND (repositories.soft_deleted_at IS NULL)));Bound args: ['', '', 11111111-1111-4111-8111-111111111111, <100 repository ids, g=2001..2100>, 11111111-1111-4111-8111-111111111111, 0, 3, 2] (two empty-string credential comparators; namespace; the full page of ids; namespace again for the parent gate; format=0 docker and format=3 oci; kind=2 remote)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Hash Join (cost=145.46..333.60 rows=100 width=75) (actual time=0.390..0.849 rows=100 loops=1)
Hash Cond: (repositories.id = container_remote_repositories.repository_id)
Buffers: shared hit=93
-> Seq Scan on repositories_p07 repositories (cost=0.00..174.50 rows=5000 width=32) (actual time=0.007..0.536 rows=5000 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::smallint[])) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (kind = '2'::smallint))
Buffers: shared hit=87
-> Hash (cost=144.21..144.21 rows=100 width=132) (actual time=0.073..0.073 rows=100 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 23kB
Buffers: shared hit=6
-> Bitmap Heap Scan on container_remote_repositories_p07 container_remote_repositories (cost=44.37..144.21 rows=100 width=132) (actual time=0.033..0.045 rows=100 loops=1)
Recheck Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Heap Blocks: exact=4
Buffers: shared hit=6
-> Bitmap Index Scan on container_remote_repositories_p_namespace_id_repository_id_idx7 (cost=0.00..44.10 rows=100 width=0) (actual time=0.029..0.029 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=2
Planning:
Buffers: shared hit=403
Planning Time: 1.776 ms
Execution Time: 0.885 msPlan, multi-tenant control:
Nested Loop (cost=0.70..459.88 rows=20 width=75) (actual time=0.045..0.233 rows=100 loops=1)
Buffers: shared hit=307
-> Index Scan using container_remote_repositories_p_namespace_id_repository_id_idx7 on container_remote_repositories_p07 container_remote_repositories (cost=0.41..293.33 rows=20 width=132) (actual time=0.029..0.059 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=7
-> Index Scan using repositories_p07_pkey on repositories_p07 repositories (cost=0.29..8.31 rows=1 width=32) (actual time=0.001..0.001 rows=1 loops=100)
Index Cond: ((id = container_remote_repositories.repository_id) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid))
Filter: ((soft_deleted_at IS NULL) AND (format = ANY ('{0,3}'::smallint[])) AND (kind = '2'::smallint))
Buffers: shared hit=300
Planning:
Buffers: shared hit=419
Planning Time: 3.179 ms
Execution Time: 0.303 msTimings: planning 1.776ms, execution 0.885ms, total 2.661ms. Control: planning 3.179ms, execution 0.303ms, total 3.482ms.
MavenRemoteRepositoryStore.FindRemoteDetailsByRepositoryIDs (100-id page, pre-existing statement)
Summary: Unchanged by this MR and measured for the same reason as the container row: the list path is its first full-page caller. The plan is npm's at 100 ids — Bitmap Index Scan on the unique (namespace_id, repository_id) index into a Hash Join whose parent side seq-scans repositories_p07 — for the same zero-selectivity reason, and the control restores the Nested Loop at 0.173ms. Maven projects two more columns than npm (snapshot_metadata_always_revalidate and the metadata window), which shows only as the wider 86-byte row. Both tables prune to one of 64 partitions. No anomalies.
Seed shape: namespaces=1, repositories=5000, maven_remote_repositories=5000 (every even binding stores a username and password pair, the all-or-none CHECK satisfied on both arms). Control: namespaces=41, repositories=25000, maven_remote_repositories=25000
Rendered SQL:
SELECT 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.tmp_plaintext_username IS NOT NULL) AND (maven_remote_repositories.tmp_plaintext_username != $1::text)) AND (maven_remote_repositories.tmp_plaintext_password IS NOT NULL)) AND (maven_remote_repositories.tmp_plaintext_password != $2::text)) AS "has_credentials",
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",
maven_remote_repositories.last_health_checked_at AS "maven_remote_repositories.last_health_checked_at"
FROM public.maven_remote_repositories
INNER JOIN public.repositories ON ((repositories.id = maven_remote_repositories.repository_id) AND (repositories.namespace_id = maven_remote_repositories.namespace_id))
WHERE ((maven_remote_repositories.namespace_id = $3::uuid) AND (maven_remote_repositories.repository_id IN ($4::uuid, $5::uuid, /* ... 97 more ... */ $103::uuid))) AND ((((repositories.namespace_id = $104::uuid) AND (repositories.format = $105)) AND (repositories.kind = $106)) AND (repositories.soft_deleted_at IS NULL));Bound args: ['', '', 11111111-1111-4111-8111-111111111111, <100 repository ids, g=2001..2100>, 11111111-1111-4111-8111-111111111111, 1, 2] (two empty-string credential comparators; namespace; the full page of ids; namespace again for the parent gate; format=1 maven; kind=2 remote)
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Hash Join (cost=150.85..338.98 rows=100 width=86) (actual time=0.349..0.781 rows=100 loops=1)
Hash Cond: (repositories.id = maven_remote_repositories.repository_id)
Buffers: shared hit=92
-> Seq Scan on repositories_p07 repositories (cost=0.00..174.50 rows=5000 width=32) (actual time=0.007..0.478 rows=5000 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (format = '1'::smallint) AND (kind = '2'::smallint))
Buffers: shared hit=87
-> Hash (cost=149.60..149.60 rows=100 width=143) (actual time=0.064..0.064 rows=100 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 25kB
Buffers: shared hit=5
-> Bitmap Heap Scan on maven_remote_repositories_p07 maven_remote_repositories (cost=44.37..149.60 rows=100 width=143) (actual time=0.025..0.035 rows=100 loops=1)
Recheck Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Heap Blocks: exact=3
Buffers: shared hit=5
-> Bitmap Index Scan on maven_remote_repositories_p07_namespace_id_repository_id_idx (cost=0.00..44.10 rows=100 width=0) (actual time=0.021..0.021 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=2
Planning:
Buffers: shared hit=400
Planning Time: 1.686 ms
Execution Time: 0.824 msPlan, multi-tenant control:
Nested Loop (cost=0.70..451.48 rows=20 width=86) (actual time=0.029..0.140 rows=100 loops=1)
Buffers: shared hit=306
-> Index Scan using maven_remote_repositories_p07_namespace_id_repository_id_idx on maven_remote_repositories_p07 maven_remote_repositories (cost=0.41..288.93 rows=20 width=143) (actual time=0.019..0.033 rows=100 loops=1)
Index Cond: ((namespace_id = '11111111-1111-4111-8111-111111111111'::uuid) AND (repository_id = ANY ('{...100 repository ids elided...}'::uuid[])))
Buffers: shared hit=6
-> Index Scan using repositories_p07_pkey on repositories_p07 repositories (cost=0.29..8.11 rows=1 width=32) (actual time=0.001..0.001 rows=1 loops=100)
Index Cond: ((id = maven_remote_repositories.repository_id) AND (namespace_id = '11111111-1111-4111-8111-111111111111'::uuid))
Filter: ((soft_deleted_at IS NULL) AND (format = '1'::smallint) AND (kind = '2'::smallint))
Buffers: shared hit=300
Planning:
Buffers: shared hit=416
Planning Time: 1.718 ms
Execution Time: 0.173 msTimings: planning 1.686ms, execution 0.824ms, total 2.510ms. Control: planning 1.718ms, execution 0.173ms, total 1.891ms.
Related to #314