Skip to content

Field data confirms the interval-table fix removes the spill, ship it (#3953) - #4208

Merged
erikdarlingdata merged 19 commits into
devfrom
fix/3953-qs-interval-latest
Sep 25, 2026
Merged

erikdarlingdata merged 19 commits into
devfrom
fix/3953-qs-interval-latest

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part of #3953.

Why

#3953 was built, tested, and shelved on 2026-09-23. The shelving measurement was on a small rig. It found a
full 14-day pass only went from about 6.5s to 5.5s. The drill-down got 4x slower (71ms to 286ms). That did
not justify three new tables, a writer inside the Query Store COPY transaction, a replay path, and a
compression-band conflict. The ruling was to reopen if field data from the busy client-site server showed
PLAN_REGRESSION dominating.

It now does. The store ingests 43 SQL Servers' Query Store data (build 3.8.0-nightly.20260924.467, about 5
hours of steady-state passes). On it, PlanRegressionSql, the raw whole-slice dedup this issue targets, is
the store's single heaviest statement:

measure value
share of owner-role store time 22.5% (5,164s of 22,914s)
calls 344 (about one per server per pass)
mean / max time 15.0s / 58.5s
shared blocks read 19.7M (about 162GB)
temp blocks written 10.35M (about 85GB in about 5 hours), 91% of all temp writes among the top 40 statements

At that rate the spill alone is roughly 400GB/day of temp writes. That is exactly the write-amplification
input the original ruling named as a first-class factor. The small rig's 14-day pass never spilled to temp,
which is why it missed this.

What changes

This is the branch built for the original ruling (design v2, adversarial-reviewed before code). A
collector-written table, collect.query_store_interval_latest, holds the latest Regular snapshot per Query
Store interval, with a 15-day horizon. PLAN_REGRESSION and its drill-down aggregate the table instead of
deduplicating raw. Each server and pass reads the table only when coverage matches the raw read's scope
and there are no pending (failed-apply) batches. Otherwise the read falls back to the shipped
raw SQL, unchanged. Full design is in the issue's 2026-09-23 comments.

This lane's own work was the merge and the re-measurement:

  • Merged origin/dev. The branch's migration was V140. Dev had since grown its own V140
    (checkpointer-timed-count, Store Checkpointer Pressure compares an hourly sync sum to a per-checkpoint bar, so it fires permanently on healthy stores #4037) and V141 (collection-caveats, PostgreSQL-target analysis engine: v2/v3 slices and the v1 lanes' residue (from #3542) #3691). Renumbered this branch's
    migration to V143, the next free version. Updated every version-bump obligation: StorageVersion.SchemaVersion,
    StoreSchemaProbeSql sentinel (arm and parameter), the GetBoolean call
    list, and the rung test files' handoff chain. QueryStoreIntervalLatestRungTests now claims the top rung.
    CollectionCaveatsRungTests does not.
  • Fixed three tests the renumbering broke. CollectionCaveatsRungTests still asserted it was the top
    rung. QueryStoreIntervalLatestWriterTests had one hardcoded Scripts.Single(m => m.Version == 140).
    ViewerCollectionCaveatsGateTests built its "V141 absent" argument array as i < arity - 1, which silently
    stopped meaning "V141's own ordinal" the moment V143 appended a parameter after it. Fixed by finding the
    parameter by name via reflection instead.
  • Fixed a line-ending drift in the two files I hand-resolved through a merge-conflict splice. The repo's
    .gitattributes pins .cs files to CRLF. A Python read/write round-trip during conflict resolution had
    silently flattened both files to LF-only on disk. Git's own clean filter had already fixed the committed
    blob, which is why only the working copy needed the fix. OversizedPlanBacklogPins caught it, since it
    pins a literal CRLF sequence in the migration SQL text.
  • Re-measured with a field-shaped load (below).

Re-measurement

The small rig never spilled. This time the seed was sized to make the OLD read spill. Seed: one server, 14
days, 500 queries x 2 plans x 336 hourly Query Store intervals x 4 re-collections per open
interval. That is 1,344,000 raw rows, deduplicating to 336,000 latest-interval rows, against a plain
PostgreSQL 18 + TimescaleDB rig at default work_mem (4MB). EXPLAIN (ANALYZE, BUFFERS) on both statements,
same parameters, same snapshots:

raw dedup (old) interval table (new) change
execution time 3,428.95 ms 297.76 ms 11.5x faster
shared blocks read 32,109 (~250.9 MB) 3,810 (~29.8 MB) 8.4x less
temp blocks written 42,295 (~330.4 MB) 4,308 (~33.7 MB) 9.8x less

The old plan's Sort node reads Sort Method: external merge Disk: 57888kB per parallel worker. That spills
the full 1.34M-row slice three times over, once per worker, on top of two more external-merge spills at the
GroupAggregate stages above it. The new plan reads the pre-deduplicated 336,000-row table directly. Its
worker-level sort still spills modestly (about 34MB total), but nothing upstream of it does.

This reproduces the field mechanism: heavy temp writes from sorting the whole raw slice. The scale is about
1/40th of one server's share of the real store's roughly 5-hour window. The new shape removes the great
majority of it there. Given the lane's time budget, I did not additionally scale up to the full field row
count. The ratios above are squarely consistent with the field numbers' 22.5%-of-store-time and
91%-of-temp-writes shares.

Result identity is proven at real scale through the real write path. An md5
oracle covers the fact and drill-down, table vs. raw, including a purge-boundary case
(PlanRegressionIntervalTableEquivalenceTests). It was re-run live against this lane's rig and passed,
unchanged by the merge.

Lite parity

S1's claim above ("Lite... I did not change it") is incomplete: this branch's diff against origin/dev already touches 8 files outside Darling. They predate this lane, inherited from the branch's 2026-09-23 revival, not written by S1. Per this lane's brief, they are accounted for here rather than reverted. The PR tender decides what to do with them:

file what the hunk does relation to #3953
Lite/Analysis/DuckDbFactCollector.QueryPerf.cs (+28) Carries best_last_exec through the fact's compared CTE and SELECT. Computes best_plan_age_days metadata and a plan_regression_source flag (0 = Lite's raw read, 1 = Darling's interval table) on the PLAN_REGRESSION fact. Direct: the fields the rest of this list consumes.
Lite/Analysis/DrillDownCollector.Queries.cs (+11/-2) Appends best_plan_last_seen to the drill-down's compared CTE and output row. Direct: same age data, for the drill-down's own display.
PerformanceMonitor.Analysis/AnalysisContext.cs (+9) Adds PlanRegressionReadsIntervalTable (nullable bool): which source Darling's fact read this pass. The drill-down reads the same source, so it never shows a best plan the interval table still holds but raw has already purged. Direct: Darling-only disclosure the interval-table read needs. Lite always leaves it null.
PerformanceMonitor.Analysis/FactAdvice.cs (+7/-1) Appends "(it last ran N days ago)" to the PLAN_REGRESSION advice sentence when best_plan_age_days is present. Direct: the interval table (Darling) and Lite's already-real 14-day window both mean the best plan on record can be up to two weeks old. The advice text now says so, instead of implying it is recent.
PerformanceMonitor.Analysis/FactRemediation.cs (+15/-1) Parses best_plan_last_seen off the drill-down row into a new BestPlanLastSeenUtc on ForcePlanTarget. Direct: plumbing for the bot gate below.
PerformanceMonitor.Analysis/RemediationAction.cs (+7/-1) Adds BestPlanLastSeenUtc to the ForcePlanTarget record, defaulted for wire compatibility. Direct: same plumbing.
PerformanceMonitor.Analysis/ForcePlanBotPolicy.cs (+23) Adds ReasonBestPlanStale and MaxBestPlanAgeDays = 4. The unattended force-plan bot now blocks on a target whose best plan last ran more than 4 days before the pass. Direct: raw Query Store's 4-day retention used to cap "best plan" age automatically. Both the interval table and Lite's real 14-day window remove that cap. The bot's own automation scope is pinned back to the 4-day regime it was scored in. The advisory (display-only) surface still shows an older best plan, with its age, to a human.
PerformanceMonitor.Collectors/CollectorContext.cs (+9) Adds QueryStoreIntervalMisses, a per-cycle count of batches that stored raw rows but missed Darling's interval table and were queued for replay. Direct: Darling-only collector-side observability for the interval table's coverage gaps. Lite leaves it at 0, since it has no such table.

All eight are #3953-scoped. None looked unrelated enough to flag for reversion. None are mentioned in the ## CHANGELOG entry below, which as written only covers the Darling interval-table read. Whether the advice-text and force-plan-bot behavior changes need their own CHANGELOG line is a coordinator call before this leaves draft.

One correction to S1's description while verifying it: the Lite dedup CTE does not use array_agg(...). Like the text above, it is ROW_NUMBER() OVER (... ORDER BY collection_time DESC, execution_count DESC) filtered to rn = 1, with the same effect through a different construct. The description was off. The scale claim next to it is the one this lane was sent to actually measure.

Measuring Lite's twin read at the top of its scale

Seeded Lite's own query_store_stats DuckDB table through the real DuckDbInitializer.InitializeAsync() / CreateArchiveViewsAsync() (production schema, not hand-transcribed), on the same connection string Lite itself uses (memory_limit=1GB;checkpoint_threshold=1GB). The seed:

  • 5 servers x 3 databases x 100 queries (Lite's query_store collector caps at the top 100 queries per database).
  • x 2 plans per query.
  • x 336 hourly Query Store intervals (14 days).
  • x 12 re-collections per open interval. Lite's default query_store schedule polls every 5 minutes. A Query Store interval is 60 minutes, so 60 / 5 = 12.

That is 12,096,000 raw rows, of which 2,419,200 are the one server's slice a single server_id = $1 call reads. Same shape as S1's re-measurement above, scaled down to Lite's real ceiling of a handful of servers instead of 43.

EXPLAIN ANALYZE on both reads against that seed, server 1, params matching production exactly, such as context.TimeRangeStart.AddDays(-14):

read wall time rows scanned spill
FACT: CollectPlanRegressionFactsAsync (DuckDbFactCollector.QueryPerf.cs), unconditional every pass, Lite's actual parity twin of PlanRegressionSql (4 runs) 530-981 ms 2,419,200 (TABLE_SCAN, confirmed in the plan) none: no SPILL / external / temp-file text anywhere in the plan
DRILLDOWN: CollectRegressedQueries (DrillDownCollector.Queries.cs), filtered to the fact's own offenders, the only path the running app actually takes (see below) (1 run) 373 ms (0.308s per EXPLAIN ANALYZE's own reported total) same scan, filtered early none
DRILLDOWN, unfiltered: the SQL's $5 IS NULL branch (4 runs) 1,347-1,622 ms 2,419,200, no early filter none

The unfiltered branch is real SQL, but its only call site never reaches it. DrillDownCollector.cs gates the call on pathKeys.Contains("PLAN_REGRESSION"). The fact sets context.PlanRegressionOffenders to that pass's own (<=20) results before deciding whether to create the finding the drill-down keys off. Every real invocation already carries a non-empty offender list, so in the running app $5/$6 are never actually null.

The 1.3-1.6s number is real, but it is dead code today, not the steady-state cost. I did not change that: it is out of scope for this lane. Noted here for whoever next touches this file.

Peak memory needed a different check. duckdb_memory(), polled every 40ms during each run, read 0.0 in every case, including runs known to use real memory (below). It is not a trustworthy instrument for this workload on this version, so I did not rely on it.

Instead, a calibration sweep throttled memory_limit on the identical seeded data and the identical FACT query. This is not Lite's real 1GB setting. It completed at 300MB (2 runs, 1,673-1,886 ms, still no spill). It hard-failed at both 150MB and 75MB (2 runs each): a DuckDBException: Out of Memory Error, not a graceful spill, every time at "143.0-144.1 MiB used". That pins this read's actual peak working set between about 143MB and 300MB. Lite's real budget, 1GB, sits roughly 3.4x-7x above that range.

Decision: do not port

Both of Lite's real code paths finish in under a second: FACT at 530-981ms, the drill-down's only reachable path at 373ms. Neither spills or comes close to Lite's 1GB ceiling, even in the unreachable worst case. That is short of the brief's "over about 1s, or a large spill" bar for porting.

It also matches the structural reason S1 gave without a number behind it. Every Lite call reads exactly one server's slice under a fixed 1GB budget on a vectorized columnar engine. It never reads several servers' slices competing for one shared 4MB work_mem, the way 43 concurrent Postgres passes do on Darling's store. The conclusion is unchanged from S1's. It now rests on a measurement instead of an assertion about scale.

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s).
  • Every touched test class run individually against the rig: QueryStoreIntervalLatestRungTests,
    CollectionCaveatsRungTests, CheckpointsTimedRungTests, PostmasterStartTimeRungTests,
    ViewerSchemaVersionGateTests, QueryStoreIntervalLatestWriterTests,
    PlanRegressionIntervalTableEquivalenceTests, PgFactCollectorTests, DarlingAnalysisPipelineTests,
    ForcePlanBotPolicyTests, PlanRegressionReadShapeTests, ViewerCollectionCaveatsGateTests,
    OversizedPlanBacklogPins. All pass, including the live (DARLING_TEST_PG) cases.
  • Full Darling.Tests suite, run once: 13,763 total, 2 failed, 696 skipped (no live Postgres or
    pg-runtime for that pass). Both failures were the line-ending drift and the ViewerCollectionCaveatsGateTests
    arity assumption above. Both were fixed and individually reverified passing afterward.
  • Did not re-run the complete 13,763-test suite a second time after the two fixes, given the lane's time
    budget. The affected classes were reverified individually. The rest of the suite did not
    depend on either fix (a line-ending pin and one test's own argument construction).
  • Field-shaped EXPLAIN (ANALYZE, BUFFERS) re-measurement above, on a throwaway probe database, not
    the suite's darlingtest.
  • Rig at port 55961, C:\GitHub\worktrees\rig-s1, stopped and deleted after use.

CHANGELOG entry

SECTION: Fixed
ENTRY:
- **PLAN_REGRESSION no longer deduplicates the whole raw Query Store slice every pass** ([#4208]). On a store ingesting many servers' Query Store data, the plan-regression check was the single heaviest statement in the store. It spent most of its time sorting and spilling the server's entire raw Query Store history to temp files. It now reads a collector-maintained table holding only each interval's latest snapshot. It falls back to the old full read only when coverage is incomplete for a server. Findings are unchanged: the same regressions, the same snapshots.
- **Force-plan bot no longer acts on a best plan older than 4 days** ([#4208]). The unattended force-plan bot now gates on `ForcePlanBotPolicy.MaxBestPlanAgeDays` = 4. A target whose best plan has not been seen in more than 4 days is blocked with reason `best_plan_stale`. This prevents a force on a plan the server evicted. The gate value is pinned by `ForcePlanBotPolicyTests`.
REF:
[#4208]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/4208

erikdarlingdata and others added 12 commits September 23, 2026 04:57
…3953): V140 and the writer

V140 adds collect.query_store_interval_latest (one row per Regular interval
identity), its per-server coverage row, and a pending-replay table. The Query
Store COPY applies each batch in its own transaction behind a savepoint with a
5 s lock_timeout; an apply fault rolls back to the savepoint, records the batch
for replay and lets raw commit, so raw ingestion never depends on the table.
Coverage creation and the hourly gap check run before the transaction with
bare-parameter bounds, so nothing inside it reads raw except by the batch's
collection_time. The viewer probe gets the V140 sentinel and top arm.

Checkpoint: the readers do not use the table yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…coverage holds the raw read's snapshots (#3953)

Both regression reads get a table twin that shares everything above the
dedup with the shipped raw SQL (split into shared constants, byte-identical).
The fact decides the source per server and pass (coverage start, pending
batches, raw's chunk floor, the table's floor) and records it on the
analysis context, so the drill-down reads the same source. Any fault in the
decision reads raw.

A live test seeds real regressions through the write path and pins the fact
and drill-down rows identical from both sources, then shows only the table
still reaching a best plan after raw is purged below it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
DarlingRetention deletes the latest-snapshot table's intervals past
QueryStoreIntervalLatestRetentionDays (15, on first_execution_time) and
pending-replay rows past the same horizon, failure-isolated like every
sibling. It stays out of RawTierCoverage and RetentionPolicies. The batched
DELETE is the whole path until the table's hypertable conversion lands;
drop_chunks joins then, in collection_log's shape.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…kips best plans older than 4 days (#3953)

With the interval table the PLAN_REGRESSION window really reaches 14 days,
so a best plan can be two weeks old. Both drill-downs (Darling's two twins
through the shared tail, and Lite's) append best_plan_last_seen; the shared
extractor carries it onto ForcePlanTarget, and ForcePlanBotPolicy blocks a
target whose best plan last ran more than MaxBestPlanAgeDays (4, raw's
retention) before the pass, with the new reason best_plan_stale. That keeps
the would-force journal in the regime it has been scored in. A target with
no age (a pre-#3953 finding) is not gated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…read (#3953)

Both fact reads (Darling's raw and table twins through the shared suffix,
and Lite's) append the best plan's last run. The fact metadata gains
best_plan_age_days and plan_regression_source (1 = interval table, 0 = raw;
Lite is always 0), and the advice states the age: "the faster plan on
record (it last ran 9 days ago)". The 10 CPU-second floor stays absolute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…xcept by the batch's collection_time (#3953)

Review finding 2 as a test: every statement the apply runs inside the raw
COPY's transaction reads query_store_stats only under server_id = $1 AND
collection_time = $2; the two wider reads run before it with bare-parameter
bounds; the conflict target and batch key render from one identity constant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…te path (#3953)

QueryStoreIntervalLatestRungTests takes the "I am the top rung" claims off
V139's test: the dense ladder (Scripts[^1]), the DDL (three engine-plain
tables, one NULLS NOT DISTINCT unique index whose columns are the writer's
conflict target), and the viewer probe's top arm. It also pins the coverage
claim's first guard: no product source spells a write into query_store_stats,
and no other generic-COPY site handles the Query Store collector; the #1912
slice repair is the named exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
Dev grew its own V140 (checkpointer-timed-count, #4037) and V141
(collection-caveats, #3691) after this branch's V140 was cut. Renumbered
the query-store-interval-latest migration to V142, the next free version,
and updated every version-bump obligation: StorageVersion.SchemaVersion,
the StoreSchemaProbeSql sentinel and its MapProbedSchemaVersion arm and
parameter, the GetBoolean call list, and the rung test files' handoff
chain (CollectionCaveatsRungTests no longer claims the top rung;
QueryStoreIntervalLatestRungTests does).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
CollectionCaveatsRungTests.TheRungIsRegisteredAtTheTopOfADenseLadder still
asserted it was the top rung (RungVersion == SchemaVersion, Same(V141,
Scripts[^1])), which stopped being true once V142 landed above it -
mirroring the same fix CheckpointsTimedRungTests already carries from
when V141 landed on it.

QueryStoreIntervalLatestWriterTests had one hardcoded Scripts.Single(m =>
m.Version == 140) reference (re-running the rung's own SQL after dropping
its index mid-test) that the standard version-bump obligations list
doesn't cover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
The test built its "V141 sentinel absent" argument array with `i < arity
- 1`, which meant "every parameter except the last one" back when V141
was the newest. V142 (#3953) appended its own parameter after
hasCollectionCaveats, so `arity - 1` now names hasQueryStoreIntervalLatest
instead, and the old test left hasCollectionCaveats itself true while
turning off the wrong sentinel -- masked further by MapProbedSchemaVersion
answering 142 outright once the V142 sentinel is true, regardless of
V141's state. Finds hasCollectionCaveats by name via reflection and
requires every sentinel from it up to be false, not just the literal last
parameter.

Also normalizes two files' line endings back to the repo's CRLF
convention (git's clean filter had already silently fixed the committed
blobs on the merge commit; only the on-disk working copy needed it, which
is what a literal-CRLF pin elsewhere caught).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…PlanAgeDays gate (#4208)

PR #4216 merged to dev using V142, so #4208's migration is renumbered to V143. All
references updated: PgMigrations.cs (Migration ctor + const name + doc comment),
StorageVersion.SchemaVersion, ViewerDataService.cs (probe comment + arm comment +
return value), and all pin/rung tests (QueryStoreIntervalLatestRungTests,
QueryStoreIntervalLatestWriterTests, CollectionCaveatsRungTests,
PostmasterStartTimeRungTests, ViewerCollectionCaveatsTests).

The PR body's CHANGELOG entry now includes a second bullet for the force-plan bot
age gate (ForcePlanBotPolicy.MaxBestPlanAgeDays = 4), pinned by
Assert.Equal(4, ForcePlanBotPolicy.MaxBestPlanAgeDays) in ForcePlanBotPolicyTests.

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
@erikdarlingdata erikdarlingdata changed the title DO NOT MERGE (plan regression read): field data confirms the interval-table fix removes the spill (#3953) Field data confirms the interval-table fix removes the spill, ship it (#3953) Sep 25, 2026
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 25, 2026 04:44
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 25, 2026 04:44
erikdarlingdata and others added 3 commits September 25, 2026 01:13
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
…corer (#4206)

The parity test extracted from CollectAndScoreFactsAsync up to ComparePeriodsAsync.
CollectConfigAuditFactsAsync sits between those two methods in DarlingAnalysisService.cs.
After #4206 added _scorer.ScoreAll to the narrow pass, the extraction included two scorer
calls instead of one, failing Assert.Equal(1, CountOf(code, Score)).

ReadBody now stops at CollectConfigAuditFactsAsync when present (Darling only),
falling back to ComparePeriodsAsync when not (Lite).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
erikdarlingdata and others added 2 commits September 25, 2026 02:37
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
…r before 143

Both 141→142 (index on index_object_stats.server_time) and 141→143 (#3953) branched
from common ancestor 141. Merge resolution:
- PgMigrations.cs: V142Sql added before V143Sql; migration list 141,142,143 in order
- StorageVersion.cs: SchemaVersion = 143 (HEAD)
- ViewerDataService.cs: probe SQL includes both V142 (pg_indexes) and V143
  (information_schema.tables) probes; method signature adds hasIndexObjectStatsServerTimeIndex
  at ordinal 117 then hasQueryStoreIntervalLatest at 118; reader call extended to
  GetBoolean(118); V142 arm added between V141 and V143 arms
- CollectionCaveatsRungTests.cs: dev's prose (V142 is the immediate handoff, not V143)
- ViewerCollectionCaveatsTests.cs: dev's constant (ordinal 116 pinned by name)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
erikdarlingdata and others added 2 commits September 25, 2026 03:13
…r from dev

Merging dev into this branch left two declarations of `collectionCaveatsOrdinal`
in ViewerCollectionCaveatsGateTests: the `var` added by V142's dev merge and the
`const int 116` from this branch's original test. Remove the const; use the
Array.FindIndex result throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
V142 (#4196) landed on dev after the fix/3953-qs-interval-latest branch was
cut. Merging dev bumped V143's ProbeOrdinal from 117 to 118 and made V142 no
longer the top rung.

IndexObjectStatsServerTimeIndexRungTests (V142):
- Remove "I am the top rung" assertions (Assert.Equal(RungVersion, SchemaVersion)
  and Assert.Same(V142, Scripts[^1])). Replace with Assert.True(RungVersion < SchemaVersion).
- Change Assert.Equal(ProbeOrdinal, arity - 1) to Assert.True(ProbeOrdinal < arity - 1).
- Add nextArm check that V143's arm sits above V142's in MapProbedSchemaVersion.
- Update class summary and comments to say claims moved to QueryStoreIntervalLatestRungTests.

QueryStoreIntervalLatestRungTests (V143):
- Bump ProbeOrdinal from 117 to 118 (V142 is now at 117).
- Bump PreviousVersion from 141 to 142 (V142 is the rung directly below V143).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
@erikdarlingdata
erikdarlingdata merged commit 7315127 into dev Sep 25, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3953-qs-interval-latest branch September 25, 2026 07:45
erikdarlingdata added a commit that referenced this pull request Sep 25, 2026
…ur (#3953) (#4311)

Ruling item 2 on #3953: the slicer's collection_time floor already carries
interval_start_time_utc <= collection_time as its real bound, so the extra
day below the window start was pure clock-skew slack, over-broad by 23
hours. Narrowed to an hour, which still covers realistic skew between the
monitored server's clock and the store's. The ceiling (30 days, covering
Query Store's max interval length plus collector-outage catch-up) is a
different bound and is untouched.

QueryStoreSlicerSql_KeepsSlackenedCollectionTimeBoundsForChunkExclusion
updated to pin the new hour literal instead of the old day literal.

Ruling item 1 (moving this read, get_query_store_top and the WPF viewer's
QueryStoreTopSql onto query_store_interval_latest) is parked: the table
#4208 shipped is Regular-execution-type only (both by its write filter and
by an identity that omits execution_type_desc) and is missing most of the
columns these two reads select. Full analysis posted on #3953
(issuecomment-5835309188). Follow-up for the rest of the still-deduplicating
Query Store reads filed as #4310.


Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
erikdarlingdata added a commit that referenced this pull request Sep 26, 2026
…33Z UTC (#4440)

Moves the CHANGELOG entries carried in merged pull-request descriptions into [Unreleased]. The cut is PRs merged at or before 2026-09-26T17:37:33Z; the next splice starts after it.

- 128 PRs are spliced: Fixed 87, Changed 20, Added 17 and Security 5. Each entry sits at the top of its section, newest PR first, and its link definition joins the trailing block.
- 22 PRs have no user-visible entry (None, test-only, or deferred to a parent).
- Three entries had no section in their description, and each was assigned from its diff: #4363, #4383 and #4380 go under Security.
- Link fixes: the #4198 references point at the issue, and #4360's [#4203] label points at issue 4203.
- #4208's entry is taken from its diff.
- Two security entries are worded to the current state: #4351's rotation note, and #4363's journal-read line.
- Only CHANGELOG.md changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant