chore(datastore): recompute namespace counters and write them back (S22 plan: 13/21)

What this step delivers

NamespaceReconcileStore, in internal/datastore/reconcile_namespace.go, derives a namespace's two counter columns from the rows that back them, and writes both values back.

  • RecomputeComponentsCount returns one sum over the six version-type tables in the schema today: container_manifests, container_remote_manifests, maven_versions, maven_remote_versions, npm_versions, and npm_remote_versions. It is one statement with six scalar subqueries, so a pass pays one round trip rather than six. Every subquery keys on namespace_id alone, which is the hash partition key of all six tables. No subquery carries a soft_deleted_at predicate, on the counted row or on any parent. This counter settles at hard delete, so a tombstoned row still holds storage and still counts until the purge removes it. That one rule separates this count from the repository-level artifacts_count recompute, which takes the predicate on every level of its chain that carries the column.
  • RecomputeDeduplicatedSizeBytes sums size over the blob_storage_blobs_by_namespace shadow table. The shadow's primary key admits one row per (namespace_id, sha256) pair, so the sum is deduplicated by construction. It reuses the package-private builder sumSizeByNamespaceIDStmt instead of a second copy of the same SQL. ADR-007 defines that read once, and the spec's reconciliation table marks it reused as-is. The two call sites keep two query names, because a name identifies one call site and a reconciliation pass is not the standalone sum.
  • WriteBackCounters is an INSERT … ON CONFLICT (namespace_id) DO UPDATE that overwrites both counters and stamps last_reconciled_at from the database clock. It sets rather than adds, because both values are the authoritative ones the recomputes returned. The INSERT arm is the backstop for a namespace that has no namespace_statistics row, which is criterion 23. The stamp reaches only the addressed row, because CounterDrainStore.ApplyNamespaceDeltas matches a chunk's baseline against that column.

The store has no production caller. Step 14 composes the three methods into the reconciliation task, and it declares the consumer-side interface at that consumer.

This step adds no migration, no DDL, no configuration key, and no route.

Decisions recorded here

The write-back fails when the namespaces row is gone

namespace_statistics.namespace_id references namespaces with ON DELETE CASCADE. A namespace that is hard-deleted mid-pass therefore takes its statistics row with it. The INSERT arm of the UPSERT then has no parent row left to reference. The method maps that rejection to the exported sentinel ErrParentNamespaceMissing and wraps it, so a caller separates a vanished namespace from a real database failure with errors.Is and needs no driver import. The rejected INSERT leaves no row behind.

Mapping rather than passing the driver error through follows mapRepositoryDeleteError, which maps the same SQLSTATE to a datastore sentinel that internal/managementapi already consumes with errors.Is from outside the package. The sentinel joins the ErrParent<Entity>Missing family the package already declares for this shape. The match tests the SQLSTATE and the constraint name, fk_namespace_statistics_namespace_id_namespaces, following BlobStorageAttachmentStore.Create. namespace_statistics carries one outbound foreign key today, so the SQLSTATE alone would discriminate against the schema as it stands, but nothing pins that inventory. Naming the constraint keeps the mapping correct if a later migration adds a second foreign key, which matters because the sentinel reports a terminal outcome: a misclassified violation would stop a namespace being reconciled where a retry would have cleared it.

The merged repository-level counterpart diverges here. RepositoryReconcileStore.WriteBackCounters is a plain UPDATE, so a vanished row matches nothing and the pass is a documented no-op.

The divergence is deliberate and it is now both documented and asserted. The doc comment on WriteBackCounters states it, and TestNamespaceReconcileStore_WriteBackCounters/fails_for_a_namespaces_row_that_is_gone pins it against the sentinel. That subtest asserts the sentinel rather than the SQLSTATE, because the mapping removes the driver error from the chain by design; it keeps its zero-row assertions, which prove the database rejected the write independently of the error's shape.

No spec row covers a reconciliation write against a deleted parent at any level, which is a gap for the spec author rather than a defect here.

Flagged for the spec author, not decided here. No spec row covers a reconcile write against a deleted parent at any level. The spec's error table covers the drain chunk against a hard-deleted repository or namespace, which is a different code path with a different answer. The write-back's behavior is therefore pinned by a test and unstated by the spec.

No per-table autovacuum setting on namespace_statistics

The plan's step-13 text concludes that a per-table autovacuum_vacuum_scale_factor on this table is "cheaper to set here than in a later migration". This merge request sets none, and it adds no migration and no DDL. Three measured facts stand behind that.

1. The default already vacuums this table about five times per reconciliation pass. The trigger threshold is autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × rows, which is 50 + 0.2 × 50,000 = 10,050 dead rows at the spec's own measured 50,000 namespaces. One pass at the default reconciliation_interval of 1h writes one row per namespace, so it produces 50,000 dead rows. That is 4.98 vacuum runs per pass, with no per-table setting at all.

2. HOT-ness governs index churn, not the visibility map. Any update clears the page's all-visible bit, HOT or not. The counter-only drain UPDATE clears it too, even though it changes no indexed value. So the write-back's forfeited HOT status costs an index entry in every index on the table, pk_namespace_statistics included, rather than only in index_namespace_statistics_on_last_reconciled_at_and_ns_id whose column it moves, and it does not cost the two consumers their index-only scans. Both of those consumers — step 15's staleness walk and step 16's backlog count — read only the stale end of that index. This write-back stamps last_reconciled_at to now(), so it moves rows to the fresh end and leaves the stale range alone.

3. One value does not carry across deployment sizes, because the fixed threshold of 50 does not scale. Under the default 0.2, vacuum runs per pass are 4.98 at 50,000 namespaces (50,000 dead rows against a 10,050 threshold) and 3.33 at 500 namespaces (500 against 150). Per row written the direction inverts: one run per 10,000 rows at 50,000 namespaces, and one per 150 rows at 500. A value picked against 50,000 namespaces is therefore wrong at both ends of a real fleet, in opposite directions.

Correction to the plan's premise. ALTER TABLE … SET for an autovacuum storage parameter takes SHARE UPDATE EXCLUSIVE, measured on PostgreSQL 17.10 through pg_locks. That mode does not conflict with ACCESS SHARE or ROW EXCLUSIVE, so it blocks no SELECT, INSERT, UPDATE, or DELETE. A later migration is one statement that blocks no traffic, so "cheaper here" does not hold. This correction is recorded here rather than as a plan edit. It is aimed at whoever implements steps 15 and 16.

The query-name budget

The budget is 350 names, asserted by TestNameBudget_CoversEveryDeclaredQueryName across two catalogs. This step adds three names, all in internal/datastore/query_names.go.

Measured at 7a10944a, after this branch was rebased onto main:

Where query_names.go internal/storage/queries.go Total
Merge base, tip 46dd2f02 311 12 323 of 350
This branch, tip 7a10944a 314 12 326 of 350

The branch total is main's plus this step's three names, and merging spends those three. !1652 (merged) has merged, so its own name is already inside the main figure rather than pending alongside this step's. The ceiling of 350 is the value on main today; !1697 (merged) proposes raising it to 400 and has not merged.

Corrections to the plan text

Both items below are gaps in the plan's own text, not in this diff. They are recorded here rather than as plan edits.

internal/datastore/query_names.go appears in no step's Files: entry anywhere in the plan. Every statement in internal/datastore needs a query name, and three package tests enforce it. Step 2b modified the same file for the same reason, so the precedent is already on the stack. This step modifies it for its three new names.

internal/datastore/reconcile_namespace_test.go is in neither this step's Files: entry nor its Tests: entry. Step 11 named its equivalent, internal/datastore/reconcile_repository_test.go, in its own Tests: line. The file carries the database-free half of the suite: the constructor's nil-client panic and every method's argument guards. It is untagged rather than integration-tagged, so it runs in the unit job and under CI lint.

Database Review Evidence

Query mode, as the plan's step-13 MR extras line requires.

Measured against PostgreSQL 17.10 with this branch's full migration chain applied. The statements are the exact text the store emits, captured from the server's own log_statement = 'all' output while the integration suite ran.

The plans below are evidence for review, not a committed contract: this branch adds no test that pins the scan shape, so a later change that lost a predicate would not fail the suite. The plan routes this step's evidence to this section rather than to a test, and the sibling steps that consume these reads are where a regression would surface.

components_count recompute: six subqueries, each pruned to one partition

 Result  (cost=59.33..59.35 rows=1 width=8)
   InitPlan 1
     ->  Aggregate  (cost=8.16..8.17 rows=1 width=8)
           ->  Index Only Scan using container_manifests_p20_namespace_id_container_image_id_cr_idx1 on container_manifests_p20 container_manifests
                 Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
   InitPlan 2
     ->  Aggregate  (cost=9.51..9.52 rows=1 width=8)
           ->  Bitmap Heap Scan on container_remote_manifests_p20 container_remote_manifests
                 Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
                 ->  Bitmap Index Scan on container_remote_manifests_p20_namespace_id_created_at_id_idx
                       Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
   InitPlan 3
     ->  Aggregate  (cost=9.51..9.52 rows=1 width=8)
           ->  Bitmap Heap Scan on maven_versions_p20 maven_versions
                 Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
                 ->  Bitmap Index Scan on maven_versions_p20_namespace_id_created_at_idx
                       Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
   InitPlan 4
     ->  Aggregate  (cost=11.29..11.30 rows=1 width=8)
           ->  Bitmap Heap Scan on maven_remote_versions_p20 maven_remote_versions
                 Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
                 ->  Bitmap Index Scan on maven_remote_versions_p20_namespace_id_created_at_idx
                       Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
   InitPlan 5
     ->  Aggregate  (cost=9.51..9.52 rows=1 width=8)
           ->  Bitmap Heap Scan on npm_versions_p20 npm_versions
                 Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
                 ->  Bitmap Index Scan on npm_versions_p20_namespace_id_created_at_idx
                       Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
   InitPlan 6
     ->  Aggregate  (cost=11.29..11.30 rows=1 width=8)
           ->  Bitmap Heap Scan on npm_remote_versions_p20 npm_remote_versions
                 Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
                 ->  Bitmap Index Scan on npm_remote_versions_p20_namespace_id_created_at_idx
                       Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)

Six InitPlan nodes, one per source table, and each names exactly one partition. The _p20 suffix is the hash of this namespace identifier, so a different namespace gives a different suffix. The asserted property is one partition per subquery, not the number 20. The plan reaches no repositories row and no format's package or image stub, because namespace_id is the partition key of all six tables.

deduplicated_size_bytes recompute: one partition on the shadow table

 Aggregate  (cost=12.68..12.69 rows=1 width=32)
   ->  Bitmap Heap Scan on blob_storage_blobs_by_namespace_p20 blob_storage_blobs_by_namespace
         Recheck Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)
         ->  Bitmap Index Scan on blob_storage_blobs_by_namespace_p20_namespace_id_size_idx
               Index Cond: (namespace_id = '01a0179c-3736-7bff-93fa-9105b5c3248b'::uuid)

This is the plan of the shared builder sumSizeByNamespaceIDStmt. Reuse extends the single-partition guarantee to this call site, where a copied statement would leave a second statement unpinned.

Write-back: the conflict arbiter is the primary key

 Insert on namespace_statistics  (cost=0.00..0.01 rows=0 width=0)
   Conflict Resolution: UPDATE
   Conflict Arbiter Indexes: pk_namespace_statistics

The conflict target is namespace_id, and pk_namespace_statistics is declared on that column. One namespace therefore holds at most one row, however many passes write it. namespace_statistics is not partitioned, which is the ADR-007 exception the table's own migration carries.

Spec coverage

The table below is the test-author table. One row is corrected against it: error case E-12's namespace half is now asserted rather than open, by the subtest named in The write-back fails when the namespaces row is gone.

Spec: docs/specs/S22-storage-accounting.md

Acceptance criteria

# Criterion Tests
AC-1 Repo-scoped increment reaches repositories after a drain tick Buffer, drain-trigger and chunk-worker steps. Not this MR.
AC-2 Namespace-scoped increment reaches namespace_statistics after a drain tick Buffer, drain-trigger and chunk-worker steps. Not this MR.
AC-3 Concurrent increments sum with no lost updates Redis buffer step. Not this MR.
AC-4 Re-increment during a claimed batch; overlap corrected on the next pass Drain and reconciliation-task steps. Not this MR.
AC-5 A stale chunk bails, re-adds its scopes and counts the bail Chunk-worker step. Not this MR.
AC-6 A chunk failing every attempt re-adds before the terminal error Chunk-worker step. Not this MR.
AC-7 Redis unavailable at increment time does not fail the operation Emit-API and reconciliation-task steps. Not this MR.
AC-8 Buffer cleared before the scan; the write-back equals the source value Ordering is the task step's. The write-back half is TestNamespaceReconcileStore_WriteBackCounters (sets rather than adds, over a non-zero starting value), plus ..._WriteBackCounters/creates_the_row_from_both_recompute_results, which feeds both recompute results to the write-back and reads the row back against them.
AC-9 Soft-delete visibility per format — the components_count-includes clause TestNamespaceReconcileStore_RecomputeComponentsCount_TombstoneRoutes (11 cases, one route per level per table), ..._VersionTypeTables (both routes per table, asserted twice per case). The artifacts_count-excludes clause is the merged repository-level recompute's; the size_bytes-includes clause is the repository-level size_bytes step's; the does-not-re-inflate-on-a-second-pass clause is the task step's.
AC-10 Positive hit per version-type table — the namespace components_count half TestNamespaceReconcileStore_RecomputeComponentsCount_VersionTypeTables (one case per table), ..._SumsEveryVersionTypeTable, ..._ScopedToTheRequestedNamespace. The repository artifacts_count half is the merged repository-level recompute's.
AC-11 Drift observed on the unit-matched histogram before the override Reconciliation-task step. Not this MR.
AC-12 A crash between HINCRBY and SADD is still captured Task fault suite. Not this MR.
AC-13 Sliding TTL refreshed on every write Redis buffer step. Not this MR.
AC-14 Migrations apply cleanly and roundtrip through the ORM namespace_statistics and shadow-table migration steps. Not this MR.
AC-15 Shadow triggers keep the shadow consistent; the shadow read equals the base-table sum Shadow-table step owns it. Re-asserted through this store by TestNamespaceReconcileStore_RecomputeDeduplicatedSizeBytes/sums_the_namespace's_blobs_and_agrees_with_the_base_table, which stages through blob_storage_blobs and lets the trigger fill the shadow.
AC-16 Every namespace has a zeroed statistics row by construction namespace_statistics step. Not this MR.
AC-17 npm publish and unpublish emit on the pipeline npm call-site step. Not this MR.
AC-18 OCI emits increments at blob finalize, mount and manifest PUT OCI call-site step. Not this MR.
AC-19 OCI emits decrements at the delete handler OCI call-site step. Not this MR.
AC-20 Maven upload emits from a post-commit site Maven call-site step. Not this MR.
AC-21 Repository cascade hard-delete (gated on #464 (closed)) Gated outside this plan's steps. Not this MR.
AC-22 Exactly one asynq task per namespace candidate Reconciliation task and trigger steps. Not this MR.
AC-23 A namespace with no namespace_statistics row gets one created with the recomputed value on its first pass TestNamespaceReconcileStore_WriteBackCounters/creates_the_row_for_a_namespace_that_has_none (row absent beforehand, created with both values and a stamped last_reconciled_at), with ..._WriteBackCounters/a_second_pass_updates_the_row_rather_than_inserting_another pinning the conflict arm.
AC-24 Concurrency never exceeds reconciliation_max_in_flight Reconciliation-task step. Not this MR.
AC-25 Source-first ordering at every emit site Call-site steps. Not this MR.
AC-26 The namespace stamp advances only after every repository is written back Ordering is the task step's. The stamp itself, and that it reaches only the addressed row, is ..._WriteBackCounters/overwrites_both_counters_and_stamps_only_its_own_row (bounded by the database clock, bystander namespace still at epoch).
AC-27 A trigger fire selects only namespaces stale beyond the interval Candidate-store step. Not this MR.
AC-28 UniqueByArgs caps a namespace at one outstanding task Reconciliation-trigger step. Not this MR.
AC-29 reconciliation_backlog exposed by a single-writer collector Backlog step. Not this MR.
AC-30 A chunk reconciled since its baseline read skips the scope Drain and task steps. Not this MR.
AC-31 counter_dirty_set_size sampled once per tick before SPOP Drain-trigger step. Not this MR.
AC-32 Config load rejects each invalid configuration Config step. Not this MR.
AC-33 The six S22-defined metrics are registered with their shapes Metrics-carrying steps. Not this MR.
AC-34 Saturation returns for re-enqueue rather than shedding or blocking Reconciliation-task step. Not this MR.
AC-35 A namespace-scoped chunk with no statistics row drops its delta and deletes :flushed Drain steps. The UPSERT backstop that closes it is AC-23's coverage here.
AC-36 A failed recovery SADD loses no delta Chunk-worker and task steps. Not this MR.
AC-37 Management-API deletes emit their deltas (gated on #313 (closed)) Deferred outside this plan's steps. Not this MR.
AC-38 A persistently failing task leaves its namespace re-enqueueable Reconciliation-task step. Not this MR.

Error cases

# Condition Tests
E-1 Redis unavailable at increment time Emit-API step. Not this MR.
E-2 Redis unavailable at drain-trigger time Drain-trigger step. Not this MR.
E-3 Chunk's Postgres UPDATE fails Chunk-worker step. Not this MR.
E-4 :flushed DEL fails after the UPDATE succeeded Chunk-worker step. Not this MR.
E-5 Chunk exhausts all retry attempts Chunk-worker step. Not this MR.
E-6 Recovery SADD itself fails Chunk-worker and task steps. Not this MR.
E-7 Chunk dequeued past drain_chunk_stale_timeout Chunk-worker step. Not this MR.
E-8 Worker dies mid-chunk after merging into :flushed Chunk-worker step. Not this MR.
E-9 Two chunks run the same scope concurrently Drain and task steps. Not this MR.
E-10 Trigger's EnqueueTx fails while the process is alive Drain-trigger step. Not this MR.
E-11 Crash between SPOP and EnqueueTx Drain-trigger step. Not this MR.
E-12 Assigned repository or namespace row hard-deleted before its chunk drains Drain steps own the chunk behavior. The reconciliation-side counterpart for a vanished namespace is TestNamespaceReconcileStore_WriteBackCounters/fails_for_a_namespaces_row_that_is_gone, which asserts the exported sentinel ErrParentNamespaceMissing and asserts that the rejected INSERT leaves no row behind. No spec row states that contract at any level.
E-13 Namespace-scoped chunk finds no namespace_statistics row Drain steps. The backstop it defers to is AC-23's coverage here.
E-14 Crash between HINCRBY and SADD Task fault suite. Not this MR.
E-15 Crash between the pre-scan clear and the counter write Reconciliation-task step. Not this MR.
E-16 Reconciliation scan races a concurrent increment Reconciliation-task step. Not this MR.
E-17 A drain chunk and a reconciliation process one scope concurrently Drain and task steps. Not this MR.
E-18 Reconciliation finds a discrepancy Reconciliation-task step. Not this MR.
E-19 A namespace has no namespace_statistics row when its task runs: the UPSERT creates it and stamps, not an error TestNamespaceReconcileStore_WriteBackCounters/creates_the_row_for_a_namespace_that_has_none — the datastore half. Reaching the task through the orphan sweep is the sweep step's.
E-20 Task fails before its final UPSERT Reconciliation-task step. Not this MR.
E-21 A namespace can never be reconciled Reconciliation-task and backlog steps. Not this MR.

Security considerations

# Concern Tests
S-1 Redis keys carry namespace_id/repository_id UUIDs with no user-controlled text Redis key-grammar step. This store touches no Redis key.
S-2 Counter values are non-secret but feed billing, so an inflated or deflated value has financial impact The accuracy assertions are the correction path this concern relies on: ..._VersionTypeTables, ..._SumsEveryVersionTypeTable, ..._TombstoneRoutes and ..._ScopedToTheRequestedNamespace for the count, and ..._RecomputeDeduplicatedSizeBytes for the bytes, including the shadow-versus-base equality that is the only assertion catching a silently divergent billing input. Every recompute case runs twice, so a value that drifts across passes fails.
S-3 Redis and Postgres connectivity reuse existing clients; no new credential surface TestNewNamespaceReconcileStore_NilClientPanics — the constructor takes the shared *postgres.Client and opens no connection of its own.

Readings chosen where the spec was ambiguous

Chosen provisionally by test-author, and open for the operator to confirm:

  • Two recompute methods rather than one returning a pair. The spec states two counters with two units and the drift histograms observe them separately, so a single method would have to be split at its only caller.
  • The deduplicated_size_bytes recompute is asserted through the store method's contract, not through a statement, leaving reuse-versus-own-builder to implementation.
  • A namespace holding no version rows and no blobs recomputes to zero on both counters rather than reporting a miss. The spec gives the namespace counters no not-found state.
  • A blob two namespaces both hold counts in each namespace's own sum. Deduplication is within a namespace.
  • The write-back does not filter on the namespaces row's lifecycle. A namespace carrying deleted_at whose contents are not yet purged still holds what its counters account for, and the staleness walk reads namespace_statistics, which has no lifecycle column.

e2e scenario catalog

No change to docs/testing/, and no scenario is affected.

The catalog's structure is what settles it. docs/testing/ holds README.md and the directory docs/testing/e2e/. That directory holds README.md and four format-scoped catalogs: docker.md, oci.md, maven.md, and npm.md. There is no storage-accounting family and no datastore family, so namespace-level accounting has no catalog to gain a scenario in.

The e2e-catalog guardrail binds feat and fix work, and the plan gives step 13 Type: chore. This step also changes no runtime behavior: no route, no job kind, no configuration key, no middleware, no metric registration, and no composition-root wiring.

No configuration-reference pairing and no Bruno pairing is owed either. The diff touches none of internal/config/**, proto/artifactregistry/config/**, config.example.yaml, and api/openapi/**.

Diff size

The diff passes the 500-reviewable-LOC ceiling, so the development model asks for a split or a justification. This is the justification.

Measured at 7a10944a against merge base 46dd2f02. Added and deleted counts come from git diff --numstat. Reviewable counts come from grep -cvE '^[[:space:]]*(//.*)?$', which drops blank lines and whole-line // comments.

File group File Added Deleted Reviewable
Production internal/datastore/reconcile_namespace.go (new) 469 0 134
Production internal/datastore/query_names.go 17 5 8
Production subtotal 486 5 142
Tests internal/datastore/reconcile_namespace_integration_test.go (new) 959 0 585
Tests internal/datastore/reconcile_namespace_test.go (new) 226 0 131
Tests subtotal 1,185 0 716
Docs docs/plans/2026-08-04-s22-storage-accounting.md 1 1 not counted
Total 5 files 1,672 6 858

query_names.go was already on main, so its reviewable count is its added lines rather than a whole-file count. Those eight lines are three new constants and five realignments of existing ones. Both other files are new on this branch, so their whole-file counts are their added counts.

Tests are 716 of the 858 reviewable lines, which is 83 percent. Production alone is 142 reviewable lines, well inside the ceiling, so splitting the production file removes nothing a reviewer has to hold at once.

The test half is not compressible, because two acceptance criteria between them ask for 17 seeded cases:

  • Criterion 10 asks for a positive hit per version-type table, which is six cases across six tables.
  • Criterion 9 asks for a tombstone route per level per table, which is 11 cases. It is 11 rather than 12 because container_manifests carries no soft_deleted_at of its own and reaches the count only through its parent container_images.

Each case seeds a real row chain through the format's own tables and reads the counter back, so no case shares another's fixture. A split that separated the store from those cases would leave a recompute with no assertion of what it counts, which is the whole contract.

Review findings declined

Two review findings were considered and not taken, recorded here so a later reader does not have to reconstruct the reasoning from the threads.

No shared helper between RecomputeDeduplicatedSizeBytes and BlobStorageBlobsByNamespaceStore.SumSizeByNamespaceID. The two methods already share their SQL through sumSizeByNamespaceIDStmt, so a predicate that moves in one moves in both. What a package-private helper would deduplicate is the destination struct and the instrumentQuery call, about four lines carrying no logic, and dupl at threshold 150 does not report the pair. The three tokens that differ each differ for a stated reason: the per-store sentinels follow this package's rule that per-store sentinels keep error messages attributable across stores, the two query names are forced by TestQueryNames_EachUsedExactlyOnce, and the error strings name their own operation.

No reconcile_namespace_explain_integration_test.go in this step. TestBlobStorageBlobsByNamespaceStore_SumPrunesToOnePartition already EXPLAINs sumSizeByNamespaceIDStmt on main and asserts a single partition, and it runs against the builder rather than a copy, so it covers RecomputeDeduplicatedSizeBytes as well. The unpinned claim is recomputeNamespaceComponentsCountStmt's, and the plan routes this step's plan-shape evidence to the "Database Review Evidence" section above rather than to a test.

Merge order

Merge order. !1652 (merged) first, then this merge request, and !1652 (merged) has merged. This step reads blob_storage_blobs_by_namespace, which !1652 (merged)'s triggers and seed make consistent. Merging this one first would have left the deduplicated_size_bytes recompute reading a table that no trigger maintains.

Step 14 is the first caller. It composes the two recomputes and the write-back into the reconciliation task, and it declares the consumer-side interface at that consumer. Until step 14 lands, this store has no production caller and changes nothing a running service does.

The plan's Status row

This merge request fills its own row in the plan's Status table: step 13 now carries !1695.

Related to #515

This is a bot message 🤖 — /smurfit

Edited by Pawel Rozlach

Merge request reports

Loading
Loading