feat(datastore): container manifest persistence for manifest push (S12 Step 12, 1/3)
Why
First of three MRs splitting the OCI manifest push endpoint (S12 Step 12) into reviewable layers, at a reviewer's request on the original combined MR, feat(oci): manifest push endpoint (S12 Step 12) (!511 - closed) • Hayley Swimelar. This slice is the datastore layer, reviewable on its own for SQL correctness and the persist transaction, with no format-layer or HTTP concern.
Review the stack in order: this MR, then the manifest logic in feat(oci): manifest push parsing, validation, a... (!519 - merged) • Hayley Swimelar, then the wiring and e2e in feat(oci): wire manifest push endpoint with dat... (!520 - merged) • Hayley Swimelar.
What (non-obvious)
- The 6b persist transaction lives in
datastore.ContainerManifestPersister, mirroring Step 9'sContainerBlobLinker, so raw SQL stays in the datastore layer per ADR-023 and the format package never importsdatabase/sql. - Child-manifest existence is one batched
WHERE digest = ANY(...)query per push regardless of child count (never per-descriptor), and the same query feeds the Image Index tree-size computation. - Image Index relationships insert in one batched statement (
ContainerManifestRelationshipStore.CreateBatch), so an index push holds the tagFOR UPDATElock for one round-trip rather than one per child. - No migration: the
container_*andblob_storage_*schema already exists from the S06 foundation.
Reviewer notes
- Size: this slice runs to ~1,860 reviewable LOC, over the 500 ceiling. It is the datastore third of the reviewer-requested Step 12 split (the plan estimated Step 12 at ~850 LOC and flagged it over 500), and roughly half the diff is integration tests, including the
ContainerManifestPersistertransaction tests. Splitting further would separate the persist transaction from its tests. - Query plans: the new transactional queries (
resolveChildManifestIDsand the manifest/tag counts) are served by the existing unique and partition-key indexes. CapturingEXPLAIN (ANALYZE, BUFFERS)against a cap-sized image (25,000 manifests, 1,000 tags) is a follow-up; it is not meaningful on a small dataset.
Test plan
go test -tags=integration ./internal/datastore/... (the persister and reference-existence integration tests) runs in CI's test:integration job. go build ./..., go vet (default and integration tags), and golangci-lint 2.12 pass locally.
Related to #19 (closed)