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 hostedInsertNpmVersion'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):
UpsertNpmRemoteVersionand part 1'sUpsertNpmRemotePackagenow useON CONFLICT ... DO UPDATEwith a self-assignment instead ofDO NOTHINGplus a read-back. Both conflict actions wait out an uncommitted conflicting inserter; the difference is the outcome.DO NOTHINGreturns no row, and the read-back that resolved the id filteredsoft_deleted_at IS NULL, so a soft delete landing between the conflict and the read-back turned a row that exists intoErrNotFound— the sentinel a genuine cache miss returns.DO UPDATEreturns the row on both paths, so neither upsert can report a miss for a row that exists. This is why the diff touchesnpm_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 viaGREATEST, 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).
🔗 References
- 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)
- feat(npm): npm_remote_packages cache-fill write... (!1184 - merged) • David Fernandez • 19.3
- feat(npm): npm_remote_versions cache-fill write... (!1185 - merged) • David Fernandez • 19.3
👈 - feat(npm): remote metadata cache-fill upsert (S... (!1186 - merged) • David Fernandez • 19.3
- feat(npm): remote tarball cache-fill upsert (S1... (!1187 - merged) • David Fernandez • 19.3
- feat(npm): remote 304 revalidation bumps (S15 S... (!1188 - merged) • David Fernandez • 19.3