feat(maven): serialize version size via the shared size field

Summary

Step 5 of the Maven version-size accounting plan: the hosted Maven Version resource now serializes maven_versions.size_bytes through the shared size field the npm slice (#549 (closed)) already populated.

  • versionFromMavenModel maps Size: new(row.SizeBytes), mirroring versionFromNpmModel — both mappers now always serialize a present value, zero included, and no hosted response sends null.
  • mavenVersionColumns() gains SizeBytes so both version reads (list and find) project the stored counter.
  • The OpenAPI Version.size description and the S17 Artifact-resources row state "both Maven and npm hosted versions carry a value"; the type stays [integer, 'null'] for contract stability against the historical nullable shape.
  • The file-delete refold ships with the serialization: MavenFileDeleter.deleteFileTx now refolds maven_versions.size_bytes in the delete's own transaction via the walk's transaction-taking halves — LockVersionForSizeRefoldTx takes the version-keyed advisory lock and the maven_versions row FOR NO KEY UPDATE as the transaction's first statements, then the maven_files DELETE runs, then RefoldVersionSizeTx refolds from the surviving rows (the same walk as the upload path's recompute), so the value this MR puts on the wire never drifts after a file hard-delete — the npm arm already had this property, and without it a served size would go permanently wrong on any Maven file delete. The plan's "no Maven delete path" premises that deferred this site on #472 (closed)/S28 are corrected in its Delete-and-GC, Decrement-side, and Dependencies passages.

Mechanism-neutral for the maintenance mechanism: the contract describes only the serialized value, not how the counter is maintained (ADR-007 governs that).

Scope

  • Field-only serialization on the wire: size on the existing shared Version resource for hosted Maven. No new size_bytes field is added (the npm slice already shipped the shared field), no route changes, no error-case changes.
  • The file-delete refold is a datastore-internal fix that keeps the serialized field correct; no management-API behavior change (the delete route already answered 202; only its transaction now also maintains the counter).
  • Size-ordered version listing stays out of scope: the S17 sort vocabulary covers created_at/version only; the column and its size-DESC index exist, but the sort key has no tracker.

Spec and plan records in this MR

  • docs/specs/S17-rest-management-api.md: the Artifact-resources Version row now reads "the stored per-version bytes for both Maven and npm hosted versions" (the "hosted" qualifier scoping the claim against the remote twins, which declare buffered counters nothing writes yet), plus reconciliation of the stale passages the Step 1/2 landings left behind — the S22 dependency row's and Freshness section's "still absent" clauses, the Follow-ups bullet's three stale sentences (the hosted-Maven carve-out, the "null for Maven" wire claim, the size-sort sentence), the Resolutions "defer to S18/S22" pointer, the Phase 8 read-cost bullet's "no total per-version index" premise (recast to prescribe the per-version bound the index_maven_files_on_ns_id_ver_id index enables), and the Freshness sentence on the Maven column's write paths (upload post-commit recompute, plus the delete-transaction refold). Each contradicted a landed Step 1/2 artifact or the spec's own S22 correction.
  • docs/plans/2026-08-10-maven-version-size-accounting.md: Step 5's Files and Acceptance entries record the shared-field routing, the delete-refold wiring, and the reconciliation scope, per the plan's research-correction guardrail; the falsified "no Maven delete path" premises in Delete-and-GC, Decrement-side, and Dependencies are corrected with the landed deleter and its refold.

Rolling-deploy window

The refold changes a file delete's lock order without a schema or an API change, so old and new pods serve the delete route side by side during a rolling deploy with opposite orders:

  • old: the maven_files DELETE, then the advisory lock, then the maven_versions row at the writeback;
  • new: the advisory lock, then the maven_versions row FOR NO KEY UPDATE, then the maven_files DELETE.

Two concurrent DELETE /files/{id} calls for the same file id, one per binary, can close a lock cycle, and PostgreSQL aborts one side as 40P01 (one of the two would otherwise answer 404). The window is bounded by the rollout and is narrower than the upload-versus-delete cycle the old order carries, so it argues for finishing the rollout promptly rather than for staging it.

Testing

  • TestVersionDetailHandler_SerializesSizePerFormat (managementapi): "maven carries the stored size" (654321) and "maven zero is a present zero, not null" (0); npm cases unchanged (123456, 0).
  • TestVersionListHandler_SerializesStoredSize (managementapi, new): each list row's size equals its distinct stored counter, for both Maven and npm arms (the npm list arm previously had no size coverage).
  • Datastore: TestMavenVersionStore_RecomputeVersionSizeTx_FiresBeforeAnyDBWork pins the Tx sibling's guards, and TestMavenVersionStore_SizeRefoldHalves_FiresBeforeAnyDBWork extends the table to LockVersionForSizeRefoldTx and RefoldVersionSizeTx; TestMavenFileDeleter_DeleteFile_RefoldsVersionSize (integration, new) pins the delete-transaction refold to the surviving rows and its digest-deduplicated shared-digest arm, and TestMavenFileDeleter_DeleteFile_LocksVersionRowBeforeFileRow (integration, new) pins the parent-before-child lock order (holds the version row so the delete parks, then probes the file row FOR UPDATE NOWAIT); TestLockMavenVersionForRecomputeStmt_SQL pins the lock read's FOR NO KEY UPDATE clause; TestListMavenVersionsByPackageStmt_Projection pins maven_versions.size_bytes in the SELECT; the list and find integration suites stamp non-zero size_bytes and assert round-trip (a dropped column scans as zero and fails).
  • Lint: golangci-lint run ./internal/managementapi/ ./internal/datastore/ clean (pinned 2.12.2); --build-tags=integration ./internal/datastore/ gains no new findings beyond the suite's existing contextcheck pattern.

E2e scenario catalogs

No catalog scenario is added or affected. The management-API size display field this MR adds to the Maven version resource is already served by the existing scenario e2e.maven.discover.view-in-ui in docs/testing/e2e/maven.md ("with coordinates and sizes shown"), and the file-delete refold is a datastore-internal correctness fix with no user-flow delta. Stated per the testing-catalogs guardrail; the catalogs target user-level flows and there is no new user flow to cover.

Related to #550

Edited by Moaz Khalifa

Merge request reports

Loading
Loading