Artifact Registry ADR 007: shadow table's namespaces cascade
What
Records that blob_storage_blobs_by_namespace.namespace_id references namespaces(id) with ON DELETE CASCADE, records that blob_storage_blobs deliberately takes no such reference, and corrects statements the shadow table's foreign key makes false.
- The Option B diagram tagged
namespace_idasFKwith no referential action, and no prose declared one. It is nowPK,FKwith the action spelled out, matching how thenamespace_statisticsdiagram already annotates its own cascade. - The
namespace_statisticsentry claimed its foreign key was "the onlyreferences namespaces(id)in this schema carryingON DELETE CASCADE". There are now two. - The
blob_storage_blobsdiagram and entry now state that the table carries nonamespaces(id)reference by design, and why. An earlier revision of this MR recorded the opposite; see Why.
Why
The cascade sits on the same side of the rule stated for repositories as the one it joins — cascade where the children are pure structure, reject where they are user data. A shadow row is a derived copy of a blob_storage_blobs row rather than user data in its own right, so a namespace delete should take it rather than be blocked by it.
A shadow row's lifetime is bounded by the shorter of two, enforced by two reapers: the AFTER DELETE trigger when garbage collection reclaims its blob_storage_blobs row, and this cascade when the namespace goes. Whichever fires first is correct, because a shadow row exists only to make a per-namespace read cheap and both events end the possibility of that read. The shadow is a read accelerator derived from blob_storage_blobs, not a replica of it, so the two are not meant to share a lifetime.
This MR reversed direction on blob_storage_blobs itself. An earlier revision added a references namespaces(id) to its diagram and described the deployed schema as behind this ADR until that reference landed. The opposite is true. blob_storage_blobs owns object_storage_key, the only handle to the stored object, and ADR-025 has garbage collection delete that object before the row it came from — so a cascade would destroy the handle while the object was still live, leaving a storage orphan that S28's own risk table lists with no remediation, recoverable only by the storage enumeration ADR-025 rejects. A blocking reference is no better: it would stall every namespace deletion behind a garbage-collection cycle that is deliberately deferred and rate-limited. The absence is therefore a decision, and S28's namespace-purge resolution already depends on it ("there is no namespace-to-blob cascade").
A namespace hard-delete therefore removes the shadow's rows and leaves the blob_storage_blobs rows they copy, until garbage collection reclaims those on its own schedule. No reader observes the gap — repositories and blob_storage_attachments both reference namespaces without a referential action, so the delete cannot proceed until every repository and attachment under the namespace is gone, and the namespace_statistics row that would drive a reconciliation pass cascades away in the same statement.
Related
Follows handbook!20738 (the namespace_statistics cascade and partitioning exception) and handbook!20699 (the shadow table's index set).
The Artifact Registry side ships the migration that creates the table with this foreign key. docs/adr/ in that repo is synced from here by a daily job, so this is the upstream half.
Related to gitlab-org/ops/artifact-registry#515