feat(npm): npm_remote_versions cache-fill writes (S15 Step 6, part 2/5)

📦 What this MR does

Part 2 of 5 of S15 Step 6 (datastore remote write — cache-fill): the npm_remote_versions write path.

  • UpsertNpmRemoteVersion — the idempotent lazy upsert, deliberately opposite to the hosted InsertNpmVersion's conflict-is-error: a remote version row is a cache anchor, so concurrent fills or a re-request legitimately hit the same (package, version) and get the existing row id back. Same partial-unique-index semantics as part 1: a tombstone never conflicts, re-caching inserts fresh.

  • ErrParentNpmRemotePackageMissing — the constraint-name-matched parent sentinel (shared with part 3's metadata store, which FKs the same parent).

  • BumpLastDownloadedAt — the buffered retention stamp, same contract as the package-level bumper.

  • Conflict action, both stores (review round): UpsertNpmRemoteVersion and part 1's UpsertNpmRemotePackage now use ON CONFLICT ... DO UPDATE with a self-assignment instead of DO NOTHING plus a read-back. Both conflict actions wait out an uncommitted conflicting inserter; the difference is the outcome. DO NOTHING returns no row, and the read-back that resolved the id filtered soft_deleted_at IS NULL, so a soft delete landing between the conflict and the read-back turned a row that exists into ErrNotFound — the sentinel a genuine cache miss returns. DO UPDATE returns the row on both paths, so neither upsert can report a miss for a row that exists. This is why the diff touches npm_remote_packages.go, which part 1 already merged.

  • AddNpmRemoteVersionSizeBytes — the per-version storage counter from ADR-007's storage-accounting amendment: accumulates signed deltas (a fill adds the new blob size, a repoint adds new minus old), clamps at zero via GREATEST, and is soft-delete gated. Applied off the request path through the format layer's buffered-update wrapper, wired in Steps 9/12.

⚙️ Why stacked

Step 6's full diff is ~3.4k reviewable LoC, so it ships as 5 stacked MRs to keep each within the review size limit (≈600 ideal), each part targeting the previous (part 1 → main), reviewed and merged bottom-up. This part sits on part 1 (FK to npm_remote_packages; reuses its test scaffolding).

  • Plan: docs/plans/2026-07-15-npm-remote.md — Step 6
  • Spec: docs/specs/S15-npm-remote.md — Name resolution, Data model

🔬 e2e scenarios

No scenario added or affected: datastore write layer with no reachable request path until the proxy handlers (Steps 10–12) wire the cache fill end-to-end.

Related to #343 (closed)

📚 Stacked MRs (review/merge bottom-up)

Edited by David Fernandez

Merge request reports

Loading
Loading