feat(npm): metadata-file datastore read query (S11 Step 4c)
📦 What
Step 4c of the npm local plan — the content-addressed metadata-file read: NpmMetadataFileByPackageAndKind (packument cache-hit path).
Re-sliced from the original file+metadata Step 4b (!476 (merged)) by storage concern to keep each MR under 500 LOC. Now that !476 (merged) (Step 4b, the file read) has merged, this MR is rebased onto main — so the diff here is just the metadata store plus its fixtures, on top of 4b's shared content-addressed fixture layer (now in main).
| Store file | Read method |
|---|---|
npm_metadata_files.go |
NpmMetadataFileByPackageAndKind |
NpmMetadataFileByPackageAndKind returns the single fresh npm_metadata_files row for (namespace, package, kind), baking the expires_at > NOW() freshness predicate into the query per ADR 007. A missing row and an expired row are therefore indistinguishable — both yield ErrNotFound, the cache-miss signal the packument/dist-tags GET handlers (S11 Steps 10, 11) fall through to an inline rebuild on. Leads with the namespace_id partition key (ADR 022); wraps qrm.ErrNoRows as datastore.ErrNotFound.
✅ Testing
internal/datastore/npm_read_integration_test.go(integration, testcontainers PostgreSQL): the metadata-file suite — success, kind selection across the three kinds, expiry filtering,ErrNotFoundfor missing / wrong-kind / cross-namespace.internal/datastore/npm_read_test.go(unit): argument-guard rejections (nil ctx, zero namespace, non-positivenpmPackageID, out-of-rangekind) + constructor nil-client panic.- Full npm integration suite + unit + pre-commit (gofmt, goimports, golangci-lint, go-test, gitlint) green.
MR size: ~360 LOC.
🔍 Reviewer note
The expired-row subtest pins the expires_at > NOW() predicate directly (require.ErrorIs(t, err, ErrNotFound) + assert.Nil): it fails if the freshness filter is ever dropped from the query. This is the fix from the original Step 4b review (Duo + AppSec Finding 4), carried over here with the metadata read.
AppSec Finding 3 (range check on kind) is addressed in this MR (d144363d): a typed sentinel (errNpmMetadataFileInvalidKind) rejects kind outside 0..2 before any DB access, so an invalid kind returns a deterministic, attributable error instead of a generic wrapped DB error. AppSec Finding 2 (positivity guard on npmPackageID) is now addressed in this MR (commit 444b872c), matching the finder-guard convention 10io applied to NpmFilesByVersion in !476 (merged); the exported kind constants remain deferred to work_items/176.
Related to #122 (closed)
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.
Scoped to this MR's own delta against main: the file read
NpmFilesByVersion landed with !476 (merged) (now merged) and is evidenced there.
| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|---|---|---|---|---|---|---|---|
datastore.NpmMetadataFileByPackageAndKind |
Limit | unique_npm_metadata_files_ns_id_pkg_id_kind |
1 / 1 | 8.31 | 0.014ms | 6 / 0 | 1 |
datastore.NpmMetadataFileByPackageAndKind
Summary: Plan matches the method's intent. Limit over an Index Scan on the unique index unique_npm_metadata_files_ns_id_pkg_id_kind, with the namespace_id literal pruning to one of 64 partitions (npm_metadata_files_p49). The (namespace_id, npm_package_id, kind) tuple is unique, so the index returns at most one row; expires_at > NOW() is applied as a cheap Filter on that single row (correct — a freshness miss is the intended ErrNotFound cache-miss signal). Actual rows match the estimate (1 / 1) over 5000 seeded packages, execution at 0.023ms with no buffer reads. No anomalies.
Seed shape: namespaces=1, repositories=1, npm_repositories=1, blob_storage_blobs=1, blob_storage_attachments=1, npm_packages=5000, npm_metadata_files=5000
Rendered SQL:
SELECT npm_metadata_files.namespace_id AS "npm_metadata_files.namespace_id",
npm_metadata_files.id AS "npm_metadata_files.id",
npm_metadata_files.npm_package_id AS "npm_metadata_files.npm_package_id",
npm_metadata_files.blob_storage_attachment_id AS "npm_metadata_files.blob_storage_attachment_id",
npm_metadata_files.expires_at AS "npm_metadata_files.expires_at",
npm_metadata_files.kind AS "npm_metadata_files.kind",
npm_metadata_files.blob_sha256 AS "npm_metadata_files.blob_sha256"
FROM public.npm_metadata_files
WHERE (((npm_metadata_files.namespace_id = $1::uuid) AND (npm_metadata_files.npm_package_id = $2)) AND (npm_metadata_files.kind = $3)) AND (npm_metadata_files.expires_at > NOW())
LIMIT $4;Bound args: [b28b6161-1f78-4238-9c8b-77bf90c5e4ad, 2501, 0, 1]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Limit (cost=0.28..8.31 rows=1 width=83) (actual time=0.014..0.014 rows=1 loops=1)
Buffers: shared hit=6
-> Index Scan using npm_metadata_files_p49_namespace_id_npm_package_id_kind_idx on npm_metadata_files_p49 npm_metadata_files (cost=0.28..8.31 rows=1 width=83) (actual time=0.013..0.013 rows=1 loops=1)
Index Cond: ((namespace_id = 'b28b6161-1f78-4238-9c8b-77bf90c5e4ad'::uuid) AND (npm_package_id = '2501'::bigint) AND (kind = '0'::bigint))
Filter: (expires_at > now())
Buffers: shared hit=6
Planning:
Buffers: shared hit=360 read=1
Planning Time: 0.584 ms
Execution Time: 0.023 msTimings: planning 0.584ms, execution 0.023ms, total 0.607ms.