feat(datastore): container images, blobs, manifest relationships

Why

Step 2 of the OCI Local plan. Lands the three foundational datastores Steps 8–14 compose against: ContainerImageStore, ContainerBlobStore, ContainerManifestRelationshipStore.

What

  • UpsertContainerImage returns (ContainerImage, bool, error) — full row, so Step 8/12 callers avoid a follow-up FindByID.
  • The Upsert/Create pattern is INSERT ... ON CONFLICT DO NOTHING RETURNING with a fallback SELECT. Partitioned tables reject the xmax = 0 single-trip trick (SQLSTATE 0A000); two trips on conflict is the canonical race-safe shape, matching container-registry's createOrFind.
Context for LLM agents

Rationale

  • Single-trip xmax = 0 insert detection. Rejected: SQLSTATE 0A000 on partitioned tables. Chosen: two trips on conflict.
  • Shared errNilContext sentinels following the blob_storage_attachments.go precedent verbatim. Rejected: callers errors.Is-ing across stores cannot attribute the rejection. Chosen: per-store prefixes (errContainerBlobNilContext etc.).
  • UpsertContainerImage returning (int64, bool, error). Rejected: asymmetric with ContainerBlobStore.Create and forces Step 8/12 to re-FindByID when name or container_repository_id is needed. Chosen: full ContainerImage struct.

Non-goals

  • Named-query metrics. Deferred to S03's metric helper; TODO(#92-followup-S03) markers on each store type.
  • Bounded ListByParent/ListByChild. The store enforces no LIMIT; bounding belongs at Step 12's manifest-push handler.
  • container_manifest store. Lands in Step 3 alongside annotations validation.
  • GC coordination locks. Deferred to S20 per the OCI Local plan.

Merge request reports

Loading
Loading