feat(oci): container_remote_manifests schema (S16 Step 1c)
What
Adds the container_remote_manifests table — a cached manifest under a
remote-image entry — as the third and last of Step 1's three MRs in the S16
container remote vertical slice. One goose migration, the regenerated jet
types and structure.sql dump, and two integration suites asserting the
schema shape and every constraint's accept and reject paths.
No Go production code and no behavior change: nothing reads or writes this table until the cache-fill and cache-hit serve steps later in S16.
That does not make it cheap to undo. The parent DROP in the Down path takes
ACCESS EXCLUSIVE on namespaces, container_remote_images,
blob_storage_attachments, blob_storage_blobs and the 64 partitions of each
of the latter three, and ACCESS EXCLUSIVE blocks reads as well as writes. The
64 partition DROPs that precede it take no lock on any FK target (see the
Down comment in the migration), so plan a rollback as a single brief stall
across the blob-storage layer, not a no-op.
container_remote_repositories (1a) and container_remote_images (1b) are
already merged. This MR is scoped to container_remote_manifests alone.
Spec: docs/specs/S16-container-remote.md#container_remote_manifests.
Plan: docs/plans/2026-07-30-container-remote.md (Step 1, sub-MR 1c).
Schema
PARTITION BY HASH (namespace_id) × 64, PK (id, namespace_id) on
application-generated UUIDv7, a composite FK to
container_remote_images(id, namespace_id) and two composite FKs into
blob_storage_attachments/blob_storage_blobs (content-addressed storage),
all ON DELETE NO ACTION. Six indexes: a partial unique digest index
(WHERE soft_deleted_at IS NULL), a retention index, an FK-coverage index
each for the two blob-storage references, and two namespace-wide scan
indexes (trash listing, chronological). Every column, constraint, and index
decision is documented at the statement it governs in the migration.
Review follow-ups applied
All six columns the schema guards with a CHECK or a composite FK now carry an
insert-based not_null_violation test: digest, blob_sha256, media_type and
size, because a CHECK is not false for NULL, plus container_remote_image_id
and blob_storage_attachment_id, because neither composite FK declares
MATCH FULL and a MATCH SIMPLE reference with a NULL column is not checked at
all.
The two namespace-wide scan indexes (trash listing, chronological) now carry
EXPLAIN (ANALYZE, BUFFERS) evidence. Both pruning tests seed 5,000 rows, a
quarter of them soft-deleted, and assert the plan rides the intended index in a
forward scan with no post-scan Sort, resolving child index names from the
catalog. At one seeded row the planner chose a sequential scan and sorted
afterwards, so the earlier versions of those tests passed with their
CREATE INDEX statements deleted.
Database Review Evidence
Collected from pipeline 2746717209 on PostgreSQL 17 against an empty database, all three matrix jobs green:
| Phase | Duration |
|---|---|
| Apply | 340.6 ms |
| Rollback | 247.1 ms |
| Re-apply | 449.3 ms |
Full output is in the migration-review.log artifacts of jobs 15804256181,
15804256182, and 15804256183.
The table and its 64 partitions are created empty, so no phase touches existing
rows. The lock exposure, which the timings do not show, is the one in ## What
above.
The retention index still has no such evidence, because no store/query-layer code queries this table yet. That evidence belongs to the MR that adds the first real query against it.
On the size of this MR
This MR is several times the 500 LOC guideline in
docs/dev/development-model.md, matching the shape of the already-merged
Step 1b MR (container_remote_images, !1323 (merged)). About half the migration is the
mechanical 64 CREATE TABLE ... PARTITION OF / 65 DROP TABLE statements
(CONCURRENTLY is unavailable on a partitioned parent, and batching them into a
DO $$ ... LOOP would accumulate all 65 locks in one transaction, which the
per-statement layout avoids). structure.sql and the three jet files are
generated and excluded from the reviewable count. Most of the rest is the
constraints and schema-shape integration suites, doc-commented by the
conventions this package already follows.
Splitting further would mean splitting one table's schema from its own tests. The plan's shape is one table per MR, and this is that table.
End-to-end scenario catalogs
No scenario added or invalidated. This step ships schema only: nothing reads
or writes container_remote_manifests until the cache-fill and cache-hit
serve steps land, so there is no observable client behavior for a scenario to
cover. Both container catalogs (docs/testing/e2e/oci.md,
docs/testing/e2e/docker.md) already list remote repositories as out of
scope until the read paths ship.
Related to #288