test(datastore): seed the footprint plan pin past the index crossover

Why

TestSumDistinctBlobSizes_PlanIsIndexOnlyOnOneShadowPartition failed six test:integration jobs across five pipelines on 2026-08-26, on PostgreSQL 16, 17, and 18, one of them the post-merge main pipeline of the change that introduced it. test:integration is required, so every failure latched its pipeline: one dropped a merge train car, and another hit a merge request whose own diff does not contain the file. Evidence table in Flaky plan-shape pin: TestSumDistinctBlobSizes ... (#922 - closed) • Hayley Swimelar • 19.4.

The fixture seeded two blobs. Two indexes on blob_storage_blobs_by_namespace can answer the read: the primary key (namespace_id, sha256) INCLUDE (size), which the pin asserts, and (namespace_id) INCLUDE (size), which carries no digest and needs the heap to join on one. At two rows the two cost within noise of each other, so the planner chooses between them on statistics the fixture does not control.

What

The namespace under test now holds 20,000 blobs and the same two are attached, so the digest set stays at two and every assertion keeps the value it had. Large namespace slice against a small digest set is the axis Measure the artifact-level size lookup: base ta... (#564) • Hayley Swimelar measured, and past the crossover per-digest primary-key probes beat one pass over the narrower index on all three versions.

Two measurements a reviewer should have, both taken by running this test itself against 16, 17, and 18 under CI's .pg-service-options:

  • Where the crossover is. 7,000 rows still reads the slice through the narrow index on 16 and 17. 8,000 probes the primary key on all three, over three repeats. 20,000 leaves a 2.5-fold margin.
  • A smaller seed makes it worse, not intermittent. 512 and 4,096 rows both fail on 16 and 17, a bitmap heap scan at 512 and a plain index scan at 4,096, so a fixture in that band fails the pin on every roll rather than one roll in several. The two-row fixture passes as often as it does only because it sits below the band. 18 clears the crossover by 512.

At 20,000 the shape also survives perturbation of what the fixture does not control: over digest sets of 1, 2, and 15 and 0 to 16,000 rows of other namespaces in the same partition, a SQL rig running the statement jet emits reached the primary key index-only in all 27 cells.

Why the crossover sits in the thousands when Measure the artifact-level size lookup: base ta... (#564) • Hayley Swimelar found the primary key serving its whole grid from 100 blobs up: that grid reads its digests from a list the planner sizes exactly, and this read draws them from blob_storage_attachments, whose 64 hash partitions floor the estimate at 64 rows however few digests are really there. Two real digests estimate as 64, fifteen as 65.

Cost: the test's own wall clock goes from 0.3 s to about 7 s, against the package's 560 s.

Test plan

Full internal/datastore integration package, five runs per version on 16, 17, and 18, in Docker under .pg-service-options. Fifteen runs, fifteen green, and the pinned read planned as Aggregate > Nested Loop > Index Only Scan using blob_storage_blobs_by_namespace_pNN_pkey every time. TestSumDistinctBlobSizes and its eight subtests run in that package and keep their expected sums, because the ballast blobs carry no attachment row and no digest set reaches them.

Context for LLM agents

Rationale

  • Widen the pin to accept the bitmap shape. Rejected because the property worth pinning is that the read touches no heap page, and only the primary key carries both sha256 and size, so accepting the narrow index accepts a heap fetch per matched digest.
  • Pin enable_bitmapscan = off alongside explainListPlan's enable_seqscan = off, the treatment explainKeysetPlan gives the same class of instability. Rejected on measurement: at two rows with 16,000 same-partition rows from other namespaces, all three fall through to a plain Index Scan on the narrow index, so the setting removes one wrong answer and leaves another.
  • Seed 128 or 256 rows. Rejected on measurement: that band is where 16 and 17 pick the narrow index unconditionally.
  • Chosen: 20,000 unattached blobs in the namespace, seeded in one statement.

Consequences: the fixture's own size is now load-bearing, so a require.Equalf precondition asserts the slice before the EXPLAIN rather than letting a later edit return the pin to a coin flip.

Non-goals

  • Vacuuming the shadow before the EXPLAIN, to give the index-only scan the all-visible pages production's autovacuum would. Measured: a VACUUM (ANALYZE) does populate the visibility map, unevenly across versions, and moves no cell of the grid toward the primary key.
  • Reseeding the fixture into its own namespace-partitioned rig, the shape test(datastore): seed the tags deep-page siblin... (!1952 - merged) • Moaz Khalifa • 19.4 used for the deep-page sibling. The partition already holds one namespace's slice, and what moved the plan was the slice's size.

Closes #922 (closed) Related to #316

Merge request reports

Loading
Loading