feat(npm): metadata-file write methods (npm hosted step 12, part 2/5)
This change is split into 5 stacked MRs to keep each within review size. Each part targets the previous one (part 1 targets main); review and merge bottom-up, part 1 first.
Part 2 of 5 of docs/plans/2026-05-11-npm-hosted.md — Step 12: packument cache rebuilder (in-process).
Adds the simple npm_metadata_files write paths used by the rebuilder: ForceExpireNpmMetadata (sets expires_at = NOW() to invalidate all kinds) and UpsertNpmMetadataFile (insert-or-update against a caller-supplied attachment), sharing the private upsertNpmMetadataFileStmt builder. Step 5's scope listed these methods, but they did not ship then; the rebuild service is their consumer, so they land here.
- Plan: docs/plans/2026-05-11-npm-hosted.md
- Spec: S11-npm-hosted
Related to #130 (closed)
Stacked MRs (review/merge bottom-up)
- feat(npm): NpmPackageByID resolver (npm hosted ... (!712 - merged) • David Fernandez • 19.2
- feat(npm): metadata-file write methods (npm hos... (!713 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.2
👈 - feat(npm): transactional cache upsert (npm host... (!714 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.2
- feat(npm): packument cache rebuilder core (npm ... (!715 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.2
- feat(npm): wire rebuild enqueue + tests (npm ho... (!716 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.2
Database evidence
Static index analysis (not EXPLAIN-verified). Docker/Colima is unavailable in this environment, so there is no live database and no executed plan. Each query's WHERE / conflict-target columns are matched against the declared indexes, primary keys, and partition keys in internal/datastore/migrations/structure.sql and ADR 007. No migration ships on this branch (npm_metadata_files exists in a pre-existing migration), so migration timing analysis is n/a.
Relevant schema facts:
npm_metadata_filesisPARTITION BY HASH (namespace_id)with composite primary key(id, namespace_id).unique_npm_metadata_files_ns_id_pkg_id_kindis a unique btree index on(namespace_id, npm_package_id, kind).
NpmMetadataFileStore.ForceExpireNpmMetadata (UPDATE)
UPDATE npm_metadata_files SET expires_at = NOW() WHERE namespace_id = $1 AND npm_package_id = $2. The (namespace_id, npm_package_id) predicate is the two-column prefix of unique_npm_metadata_files_ns_id_pkg_id_kind (namespace_id, npm_package_id, kind), so the index covers the lookup (it intentionally matches all three kind rows for the package by leaving kind unconstrained). namespace_id is present, so the planner prunes to the single namespace hash partition. No anomalies under static analysis.
NpmMetadataFileStore.UpsertNpmMetadataFile via upsertNpmMetadataFileStmt (INSERT ... ON CONFLICT DO UPDATE)
INSERT INTO npm_metadata_files (...) VALUES (...) ON CONFLICT (namespace_id, npm_package_id, kind) DO UPDATE SET blob_storage_attachment_id = ..., blob_sha256 = ..., expires_at = .... The conflict target (namespace_id, npm_package_id, kind) matches unique_npm_metadata_files_ns_id_pkg_id_kind column-for-column, which is the index PostgreSQL requires to arbitrate the conflict. namespace_id is the leading conflict-target column and the partition key, so the conflict probe prunes to the single namespace hash partition. No anomalies under static analysis.