chore(datastore): add the container remote eviction composers

Why

Step 27 of the S17 Phase 6 plan: the datastore side of container remote eviction, ahead of the handler arms and the bulk pass (plan Steps 30 and 33). On a remote repository the delete routes evict cached rows rather than deleting owned data (spec: Remote artifact eviction). This MR lands the composers those routes will call: EvictContainerRemoteImage and EvictContainerRemoteManifest set soft_deleted_at on the named row only (the purger reaps the subtree), and the manifest composer refuses while a live cached index still references the target, returning the live parents' digests for the handler's 409 body. These are the first delete-side writers over the container remote cache tables.

What (the non-obvious parts)

  • The blocking check counts only relationship rows whose parent manifest is live, so a marked-but-unreaped index no longer blocks its children. That is the property the bulk pass's dependency order (plan Step 33) composes on. The check ships dormant: no production writer populates container_remote_manifest_relationships until S16's relationship-population follow-up lands, so the suite seeds relationship rows directly, per the plan's own Tests line.
  • Fixtures seed through the package's direct-seed helpers (seedContainerRemoteImage and kin) rather than ContainerRemoteCacheStore.UpsertCacheFill, which landed on main while this branch was open. A fill cannot stage what these cases need: rows already marked, relationship rows no production path writes, and a manifest under a second remote repository in the same namespace.
  • No hard-delete leg: container tags are Steps 20 and 35, and container has no file leg, so nothing here composes the attachment store. The image mark's pre-reap contract (children and attachments stay in place, live and unmarked) is pinned instead.
  • The composers are id-keyed and do not verify the repository chain. The Step 30 caller's resolve owns that, and the two DoesNotVerifyTheRepositoryChain tests pin the absence openly, the same contract as the hosted composers and the maven and npm eviction MRs.
  • No plan Status-table row rides this MR. A single table-owner MR fills the Status rows for the whole Phase 6 fan-out, so concurrent step MRs never collide on the same lines.

Reviewable size

1682 insertions, past the 500-LOC guardrail. By file group: composer 387, unit suite 378, integration suite 917. Splitting would separate the suites from the composer they pin, and the maven and npm eviction MRs took the same one-MR shape.

Test plan

  • go test ./internal/datastore/ (unit: the argument-guard table, constructor panic, exact-order bind pins, and statement SQL pins on both marks and the blocking check)
  • go test -tags=integration -count=1 -run 'ContainerRemoteEvict' ./internal/datastore/ (testcontainers PG: 13 top-level tests covering mark invisibility through the cache reads, the pre-reap subtree survival, live-parent blocking with server-derived digests, marked-parent unblock, idempotency, cross-namespace isolation, the chain-absence pins, re-cache after a mark, and EXPLAIN single-partition pins)
  • golangci-lint at the CI-pinned 2.12, plain (0 issues) and --build-tags=integration --max-same-issues=0 --max-issues-per-linter=0 (zero new findings in the branch files; the package's pre-existing contextcheck seed-helper class only)
  • gofmt, go vet (plain and integration tags), go build ./...
Spec coverage (from the test-author commit)

Spec coverage

Spec: docs/specs/S17-rest-management-api.md Plan: docs/plans/2026-08-13-s17-phase6-virtual-remote-repositories.md, Step 27

Scope: the internal/datastore composer. Acceptance criteria are numbered by position in the spec's auto-numbered list (123 items); the Phase 6 eviction block spans AC-100 to AC-109.

Acceptance criteria

# Criterion Tests
AC-100 An evicted-but-unreaped row (marked soft_deleted_at) is absent from every list and detail read Composer slice, per the plan (no container remote management list/detail reads exist yet): invisibility under the suite's own soft-delete predicate (containerRemoteImageRowsByID / containerRemoteManifestRowsByID live counts) and through the marker-filtering cache reads (ContainerRemoteCacheStore.LookupRow on the tag, manifest, and blob routes) in TestContainerRemoteEvictor_EvictContainerRemoteImage/marks an active image and hides every cache read beneath it and TestContainerRemoteEvictor_EvictContainerRemoteManifest/marks a manifest no live index references and hides it from the cache reads (the tag-pointing-at-a-marked-manifest miss included). The guard and scope live in the SQL: TestContainerRemoteEvictionMarkStmts
AC-101 DELETE on a remote image (and the other four targets) returns 202, the target disappears from reads, no outbound request, re-cache on next pull Handler step (plan Step 30). The composer's share: the disappearance (AC-100 rows), the no-op vocabulary the handler's 404 is built on (.../no-ops on an image id that does not exist, .../no-ops on a manifest id that does not exist), and the re-cache substrate (TestContainerRemoteEvictor_EvictContainerRemoteImage_MarkedNameRecaches, TestContainerRemoteEvictor_EvictContainerRemoteManifest_MarkedDigestRecaches: a fresh same-coordinate row inserts beside the tombstone and the digest lookup resolves the fresh row)
AC-102 Evictions emit no artifact_registry_artifact_deleted event Handler step (plan Step 30). The composer has no event surface; its statements reach one table each (TestContainerRemoteEvictionMarkStmts single-table pins)
AC-103, AC-104 npm packument and Maven/npm package-subtree criteria npm and Maven composer steps (plan Steps 25-26). No npm or Maven surface here
AC-105 Evicting a remote container image removes, once the reap has run, its manifests, tags, relationship rows, and container_remote_blobs rows, each attachment-carrying row with its blob_storage_attachments row in the same step In-step slice only (the reap is S20-A's, per the plan): TestContainerRemoteEvictor_EvictContainerRemoteImage_MarksTheNamedRowOnly pins the pre-reap state — the image mark writes soft_deleted_at alone, and the manifests, tag, blob, relationship row, every attachment row, and the CAS blobs all stay in place, live and unmarked, as the purger's inventory (assertContainerRemoteEvictionSubtreeIntact)
AC-106 Remote tag upsert 405; tag DELETE evicts Tag steps (plan Steps 20, 30, 35). These composers carry no tag surface: the marks' SQL omits container_remote_tags (TestContainerRemoteEvictionMarkStmts)
AC-107 Evicting a remote manifest that a live cached manifest indexes returns 409 with the parent digests; one indexed only by an already-marked manifest returns 202; a bulk batch naming an index and its child evicts both Composer slices: live parent blocks with the digests and the target NOT marked (TestContainerRemoteEvictor_EvictContainerRemoteManifest/a live index blocks its child and returns the live parent digests), only live parents answer (.../returns every live parent digest and only the live ones), only-marked parents evict (.../a child indexed only by an already-marked index evicts), and children never block their parent (.../an index with live children evicts: a child never blocks its parent) — the dependency-order substrate the bulk batch slice (plan Step 33) composes; the SQL side is TestListLiveContainerRemoteParentDigestsStmt_SQL (child-side key, liveness arm)
AC-108, AC-109 Remote bulk selectors and ordered pass; remote repository delete under destructive Bulk steps (plan Steps 31-33) and the repository-delete step (Step 34 with S20-A). No route or worker surface here
AC-1 to AC-99, AC-110 to AC-123 Phases 1-4 and 8 surface, virtual repositories, contract documents, and every route-level criterion Not this step. No route, contract, or non-container surface changes here

Behavior obligations from the spec prose

# Obligation Tests
B-1 Eviction markers: the request sets soft_deleted_at on the NAMED row; row removal is the purger's Mark subtests assert total=1, live=0 (marked, not removed); assertContainerRemoteEvictionSubtreeIntact; TestContainerRemoteEvictionMarkStmts pins the single-table UPDATE with the soft_deleted_at IS NULL guard and omits every child table and blob_storage_attachments
B-2 Re-cache at zero distance: the mark hides the row from the partial unique indexes, so a fill mints a fresh live row rather than colliding or resurrecting TestContainerRemoteEvictor_EvictContainerRemoteImage_MarkedNameRecaches and TestContainerRemoteEvictor_EvictContainerRemoteManifest_MarkedDigestRecaches (composer half; the schema half is TestContainerRemoteImagesConstraints_PartialUniqueAllowsRecreateAfterSoftDelete / TestContainerRemoteManifestsConstraints_PartialUniqueAllowsRecreateAfterSoftDelete in internal/datastore/migrations)
B-3 The blocking check counts only relationship rows whose parent manifest is live .../returns every live parent digest and only the live ones (mixed live+marked+live parents); TestListLiveContainerRemoteParentDigestsStmt_SQL pins the soft_deleted_at IS NULL arm on the joined parent
B-4 Dependency order: indexes evict first and a marked index unblocks its children within one pass .../an index with live children evicts and .../a child indexed only by an already-marked index evicts run the two-call sequence; the child-side-only key is pinned in SQL
B-5 Idempotent marks: a repeat call keeps the first mark's timestamp .../a second call marks nothing new in both suites; the soft_deleted_at IS NULL fragment pins
B-6 The check ships dormant (no production writer populates container_remote_manifest_relationships), so acceptance tests seed relationship rows directly seedContainerRemoteManifestRelationship (direct INSERT, documented as the only staging path); every blocking walk runs on directly-seeded rows
B-7 The image mark composes in the caller's transaction TestContainerRemoteEvictor_EvictContainerRemoteImage_ComposesInACallerTransaction (rollback leaves the row active, commit persists)
B-8 Failure paths write nothing and report no blockers TestContainerRemoteEvictor_EvictContainerRemoteManifest_CancelledContextEvictsNothing (error, empty parents, row live); assertNoBlockedParents across the guard table; TestContainerRemoteEvictor_EvictContainerRemoteImage_ExecError (wrapped driver error, no identifiers in the wrap)

Plan obligations (Step 27)

# Obligation Tests
P-1 Exported surface EvictContainerRemoteImage and EvictContainerRemoteManifest in internal/datastore/container_remote_eviction.go The suites compile against those names; the skeleton fixes the file and signatures
P-2 Marked rows invisible under the soft-delete predicate, asserted in the composer suite's own queries The AC-100 row's own-predicate live counts
P-3 The manifest composer's blocking check counts only live-parent relationship rows and returns the parent digests for the 409 details body AC-107 and B-3 rows
P-4 Integration walks with directly-seeded relationship rows B-6 row
P-5 Constructor panics on a nil client; the zero value answers a sentinel TestNewContainerRemoteEvictor_NilClientPanics, TestContainerRemoteEvictor_ArgumentGuards (zero-value receiver throughout, nil-client row)
P-6 Partition-pruned statements (every table hash-partitions on namespace_id) TestContainerRemoteEvictor_EvictionStatements_SinglePartitionPrune (both marks, and the check's two-table join pruning each side)

Error cases

# Condition Tests
E-1 Remote manifest eviction: manifest is indexed by a live cached manifest returns 409 conflict with parent digests in error.details.parents The 409 rendering is the Step 30 handler's; the composer's whole share is pinned: blocked answer carries the live parents' digests as a value, nothing written, target unmarked (AC-107 rows)
E-2 Artifact delete: target missing or outside the URL's parent chain returns 404 The 404 mapping is the Step 30 handler's, built on the composer's benign no-ops: absent ids and foreign-namespace ids write nothing (the ghost and cross-namespace subtests in both suites)
E-3 A malformed call: nil context, nil db handle, or a zero namespace, image, or manifest id TestContainerRemoteEvictor_ArgumentGuards (eight guard cases, each on its own sentinel, no DB round-trip via recordingDB)
E-4 The spec's other route-level Error Cases rows (400, 401, 403, 413, 422, 405, 503) Route-level; handler and bulk steps. This composer has no request surface

Security considerations

# Concern Tests
S-1 Tenant isolation: every query scoped by namespace_id, the partition key; a guessed UUID from another namespace resolves nothing Cross-namespace subtests in both suites (foreign rows untouched and unaddressable); namespace fragments and bound parameters pinned in TestContainerRemoteEvictionMarkStmts and TestListLiveContainerRemoteParentDigestsStmt_SQL; pruning pinned by TestContainerRemoteEvictor_EvictionStatements_SinglePartitionPrune. The parent-chain verification stays with the caller's resolve, pinned as ABSENT by TestContainerRemoteEvictor_EvictContainerRemoteImage_DoesNotVerifyTheRepositoryChain and TestContainerRemoteEvictor_EvictContainerRemoteManifest/does not verify the repository chain
S-2 Existence hiding on writes: no failure response states whether a foreign artifact exists A foreign-namespace call and a plain miss return the same no-op vocabulary (nil error, empty parents), asserted across the tenant and ghost subtests; the image mark's error wrap carries no identifiers (TestContainerRemoteEvictor_EvictContainerRemoteImage_ExecError)
S-3 Echoed input: the manifest 409 names parent digests the server derived rather than echoing submitted values The parents value is asserted equal to the digests of the seeded live parent rows (server-derived from the join), never the submitted id (.../a live index blocks its child and returns the live parent digests)
S-4 Injection: all queries use the Jet builder with bound parameters TestContainerRemoteEvictionMarkStmts and TestListLiveContainerRemoteParentDigestsStmt_SQL assert every scope id is bound as a parameter, never inlined
S-5 Authentication, authorization (delete_artifact on evictions), bounded blast radius Handler and bulk steps behind the S08/S09 stubs (plan Steps 30-33). No request surface here

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


</details>

<details><summary>Context for LLM reviewers</summary>

Design rationale and rejected alternatives:

- One `ContainerRemoteEvictor` type over `*postgres.Client`, rather than methods on `ContainerRemoteCacheStore`: the cache store is the protocol-path read seam, eviction is the management surface's first writer over these tables, and the Step 30 handler composes the evictor as one seam. Same split as the maven and npm evictors.
- `EvictContainerRemoteImage` takes the caller's `qrm.DB` (one guarded UPDATE, composes in the caller's transaction). `EvictContainerRemoteManifest` owns one transaction over check-then-mark, the hosted `ContainerManifestDeleter.DeleteManifestByDigest` envelope, and returns the live parents' digests as a value: non-empty means blocked with nothing written. The 404 and 409 mappings stay with the Step 30 handler's resolve.
- The blocked answer carries digests server-derived from the relationship join, never echoed input.

Non-goals a reviewer might reasonably raise:

- No handler, route, or worker wiring: plan Step 30 arms the delete routes. The composers are intentionally uncalled in production code in this MR.
- No tag surface: tag eviction is plan Steps 20 and 35, and the marks' SQL omits `container_remote_tags` (pinned by the statement tests).
- No reap and no scan levels: marked subtrees are the S20-A purger's to remove. The removal clauses in the spec's image-eviction criterion are reap-time, and the suite pins the pre-reap state instead.
- No lock-order doc clause, now for a different reason than when this MR opened. `ContainerRemoteCacheStore.UpsertCacheFill` has since landed and holds the `container_remote_images` row lock to end of transaction, so it is a real counterparty for the image mark and the two serialize on that row. A lock-order clause still does not apply, because each eviction takes exactly one row lock (`container_remote_images` for the image mark, `container_remote_manifests` for the manifest mark), and a single-lock path cannot invert an order. `EvictContainerRemoteImage` composes on a caller's handle, so a caller holding further locks owns their order.
- The live-parents check is uncapped by design: the 409 contract returns every live parent digest, and the hosted twin takes the same shape. A links-per-child cap belongs to S16's relationship-population follow-up.
- `errContainerRemoteNilDB`'s message text names the repositories family. It lives in `container_remote_credentials.go`, three of its four users are that family, and reusing the shared identity is the maven-precedented choice. Renaming is a follow-up chore outside this MR's files.

</details>

## Database Review Evidence

### Queries

> [!NOTE]
> Plans are from `EXPLAIN (ANALYZE, BUFFERS)` against an ephemeral
> PostgreSQL `17.10` container (matching `GL_PG_CURR_VERSION` from
> `.gitlab-ci-other-versions.yml`), with synthesized seed data rolled
> back per query and the container torn down at the end of the run.
> Numbers reflect moderate cardinality and do not capture
> production-scale effects. See
> [Database review evidence](docs/dev/database-migrations.md#database-review-evidence)
> for seed sizing, methodology, and the anomalies the skill flags.
> Expand each row's details for the seed shape, rendered SQL, bound args,
> and raw plan.

| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|--------|-----------|-------|----------------------|------|------|----------------------|------------|
| `datastore.EvictContainerRemoteImage` | Update | `n/a` | 0 / 0 | 1.75 | 0.178ms | 35 / 2 | 1/64 |
| `datastore.EvictContainerRemoteManifest.ListLiveParents` | Nested Loop | `container_remote_manifest_re_namespace_id_child_container_idx28` | 4 / 3 | 46.86 | 0.030ms | 16 / 0 | 1/64, 1/64 |
| `datastore.EvictContainerRemoteManifest.Mark` | Update | `n/a` | 0 / 0 | 2.75 | 0.174ms | 38 / 0 | 1/64 |

<details>
<summary><code>datastore.EvictContainerRemoteImage</code></summary>

**Summary**: Plan matches the mark's intent: the bound `namespace_id` prunes to one of 64 hash partitions, and the id equality plus the idempotency leg (`soft_deleted_at IS NULL`) matched exactly the target row (1 planned / 1 actual, 49 live siblings filtered). At the 50-row write-target seed the planner takes a Seq Scan of the 1-page partition on cost; re-seeded to ~5000 live rows in the same rolled-back session, it switches to the partition primary key (`container_remote_images_p28_pkey`, Index Cond on `id` and `namespace_id`), so the statement is index-served at production cardinality. Execution time is FK-trigger dominated (1.19 of 1.54 ms, the per-row re-checks on the updated row), the expected single-row UPDATE overhead. No anomalies.

**Seed shape**: `namespaces=1, repositories=1, container_remote_repositories=1, container_remote_images=50`

**Rendered SQL**:

```sql
UPDATE public.container_remote_images
SET soft_deleted_at = NOW()
WHERE ((container_remote_images.namespace_id = $1::uuid) AND (container_remote_images.id = $2::uuid)) AND (container_remote_images.soft_deleted_at IS NULL);

Bound args: [11111111-1111-1111-1111-111111111111, 44444444-4444-4444-4444-444444444444]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

Update on container_remote_images  (cost=0.00..1.75 rows=0 width=0) (actual time=0.178..0.178 rows=0 loops=1)
  Update on container_remote_images_p28 container_remote_images_1
  Buffers: shared hit=35 read=2
  ->  Seq Scan on container_remote_images_p28 container_remote_images_1  (cost=0.00..1.75 rows=1 width=18) (actual time=0.005..0.007 rows=1 loops=1)
        Filter: ((soft_deleted_at IS NULL) AND (namespace_id = '11111111-1111-1111-1111-111111111111'::uuid) AND (id = '44444444-4444-4444-4444-444444444444'::uuid))
        Rows Removed by Filter: 49
        Buffers: shared hit=1
Planning:
  Buffers: shared hit=306
Planning Time: 1.021 ms
Trigger for constraint fk_container_remote_images_container_remote_repository_id on container_remote_images_p28: time=0.904 calls=1
Trigger for constraint fk_container_remote_images_namespace_id_namespaces on container_remote_images_p28: time=0.289 calls=1
Execution Time: 1.535 ms

Timings: planning 1.021ms, execution 1.535ms, total 2.556ms.

datastore.EvictContainerRemoteManifest.ListLiveParents

Summary: Plan matches the blocking check's intent: a Nested Loop reads the 4 edges naming the child from the child-side index (container_remote_manifest_re_namespace_id_child_container_idx28, the partition clone of index_crmr_on_ns_id_child_manifest_id under PostgreSQL's 63-character name limit), then probes each parent through container_remote_manifests_p28_pkey with the namespace equality in the Index Cond and the liveness arm as a filter (4 edges matched, 3 live digests returned, the one soft-deleted parent filtered). Both partitioned tables prune statically to 1 of 64 partitions off the bound namespace, the two-table shape the branch's EXPLAIN pin test asserts. No LIMIT by design: the result is one digest per live parent, and the 409 detail contract renders every blocking digest, so a truncating LIMIT would report a blocked eviction while hiding part of what blocks it. No anomalies.

Seed shape: namespaces=1, repositories=1, container_remote_repositories=1, container_remote_images=1, blob_storage_blobs=1, blob_storage_attachments=1, container_remote_manifests=5000, container_remote_manifest_relationships=5003

Rendered SQL:

SELECT container_remote_manifests.digest AS "container_remote_manifests.digest"
FROM public.container_remote_manifest_relationships
     INNER JOIN public.container_remote_manifests ON ((container_remote_manifests.id = container_remote_manifest_relationships.parent_container_remote_manifest_id) AND (container_remote_manifests.namespace_id = container_remote_manifest_relationships.namespace_id))
WHERE ((container_remote_manifest_relationships.namespace_id = $1::uuid) AND (container_remote_manifest_relationships.child_container_remote_manifest_id = $2::uuid)) AND (container_remote_manifests.soft_deleted_at IS NULL);

Bound args: [11111111-1111-1111-1111-111111111111, 00000000-0000-0000-0000-000000000001]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

Nested Loop  (cost=0.56..46.86 rows=4 width=33) (actual time=0.022..0.030 rows=3 loops=1)
  Buffers: shared hit=16
  ->  Index Scan using container_remote_manifest_re_namespace_id_child_container_idx28 on container_remote_manifest_relationships_p28 container_remote_manifest_relationships  (cost=0.28..13.61 rows=4 width=32) (actual time=0.013..0.014 rows=4 loops=1)
        Index Cond: ((namespace_id = '11111111-1111-1111-1111-111111111111'::uuid) AND (child_container_remote_manifest_id = '00000000-0000-0000-0000-000000000001'::uuid))
        Buffers: shared hit=3
  ->  Index Scan using container_remote_manifests_p28_pkey on container_remote_manifests_p28 container_remote_manifests  (cost=0.28..8.30 rows=1 width=65) (actual time=0.003..0.003 rows=1 loops=4)
        Index Cond: ((id = container_remote_manifest_relationships.parent_container_remote_manifest_id) AND (namespace_id = '11111111-1111-1111-1111-111111111111'::uuid))
        Filter: (soft_deleted_at IS NULL)
        Rows Removed by Filter: 0
        Buffers: shared hit=13
Planning:
  Buffers: shared hit=690 read=1
Planning Time: 2.119 ms
Execution Time: 0.054 ms

Timings: planning 2.119ms, execution 0.054ms, total 2.173ms.

datastore.EvictContainerRemoteManifest.Mark

Summary: Plan matches the mark's intent and is the same statement shape as the image mark on container_remote_manifests: pruned to one of 64 hash partitions off the bound namespace_id, with the id equality and the idempotency leg matching exactly the target row (1 planned / 1 actual, 49 live siblings filtered). At the 50-row write-target seed the planner takes a Seq Scan of the 2-page partition on cost; re-seeded to ~5000 live rows in the same rolled-back session, it switches to the partition primary key (container_remote_manifests_p28_pkey), so the statement is index-served at production cardinality. Execution time is FK-trigger dominated (3.08 of 3.41 ms across the table's four FK re-checks on the updated row), the expected single-row UPDATE overhead. No anomalies.

Seed shape: namespaces=1, repositories=1, container_remote_repositories=1, container_remote_images=1, blob_storage_blobs=1, blob_storage_attachments=1, container_remote_manifests=50

Rendered SQL:

UPDATE public.container_remote_manifests
SET soft_deleted_at = NOW()
WHERE ((container_remote_manifests.namespace_id = $1::uuid) AND (container_remote_manifests.id = $2::uuid)) AND (container_remote_manifests.soft_deleted_at IS NULL);

Bound args: [11111111-1111-1111-1111-111111111111, 66666666-6666-6666-6666-666666666666]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

Update on container_remote_manifests  (cost=0.00..2.75 rows=0 width=0) (actual time=0.174..0.174 rows=0 loops=1)
  Update on container_remote_manifests_p28 container_remote_manifests_1
  Buffers: shared hit=38
  ->  Seq Scan on container_remote_manifests_p28 container_remote_manifests_1  (cost=0.00..2.75 rows=1 width=18) (actual time=0.006..0.010 rows=1 loops=1)
        Filter: ((soft_deleted_at IS NULL) AND (namespace_id = '11111111-1111-1111-1111-111111111111'::uuid) AND (id = '66666666-6666-6666-6666-666666666666'::uuid))
        Rows Removed by Filter: 49
        Buffers: shared hit=2
Planning:
  Buffers: shared hit=222
Planning Time: 1.314 ms
Trigger for constraint fk_container_remote_manifests_blob_storage_attachment_id_bsa on container_remote_manifests_p28: time=0.076 calls=1
Trigger for constraint fk_container_remote_manifests_container_remote_image_id on container_remote_manifests_p28: time=2.931 calls=1
Trigger for constraint fk_container_remote_manifests_namespace_id_namespaces on container_remote_manifests_p28: time=0.020 calls=1
Trigger for constraint fk_container_remote_manifests_ns_id_blob_sha256_blobs on container_remote_manifests_p28: time=0.050 calls=1
Execution Time: 3.408 ms

Timings: planning 1.314ms, execution 3.408ms, total 4.722ms.

Related to #314

Edited by Hayley Swimelar

Merge request reports

Loading
Loading