Repository navigation
Daily-tier reads now stitch in the interval-honest successor daily (#3653 A6) - #4184
Conversation
…, gated on availability StitchedRelationSql and StitchFloor's daily branches used LA's placeholder s_supersededDailyRollupsStub (name lookup only) instead of LB's real TimescaleSupport.SupersededDailyRollups (#4181), and never checked RollupAvailability.Has() for the successor daily the way the hourly branch already does. Delete the stub and route both methods through the real registry, gating on Has() as well as the floor so an absent successor daily is never named in SQL even when a stale floor happens to be cached for it. Add pure tests for the three daily states: absent from availability (even with both successor floors cached) gives legacy-only text byte-identical to today; present but empty gives legacy-only; present with a floor already gives the F_d stitch (existing test). All 12 StitchedRelationSqlTests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…m through the stitch builder The daily tier's ResolveFamily arm only ever set ComposeRoute.CaggRelation (the by-name answer); CaggFromClause stayed null, so ComposeCompiler always fell back to "collect.<dailyView> AS f" even once a successor daily exists. Set CaggFromClause the same way the hourly arm already does, via RollupCoverage.StitchedRelationSql(dailyView, FactAlias, windowStartUtc, StitchTier.Daily). The tier CHOICE is untouched (still coverage.For on the legacy pair); only the relation within the chosen daily tier can stitch. With no successor daily this is byte-identical to today: 306 Compose tests green (ComposeSourceRouterTests, DarlingComposeTests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…s through the stitch builder
DatabaseResourceUsageSqlFor/TopResourceConsumersByTotalSqlFor/
TopResourceConsumersByAvgSqlFor's coverage-aware overloads stitched the
hourly branch but spliced the daily rollup by bare name
("collect.<dailyView> AS f"). Route the daily branch through
RollupCoverage.StitchedRelationSql(..., StitchTier.Daily) the same way,
so a successor daily gets read once LB backfills it. With no successor
daily this is byte-identical to today: 31 tests green (ViewerFinOpsTests,
RetentionTierRouterTests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…he daily tier at F_d DailySummarySql.RangeSqlFor(tier, coverage, windowStartUtc) only ever stitched the hourly tier; the daily tier fell through to the plain, unstitched form even once a successor daily exists. Extend it to both tiers: StitchFloor is asked on the tier's own legacy/StitchTier pair, and a genuine stitch runs QueriesCteForStitchedCagg's not-carried probe twice, split at F_d, with each side's own MaterializationHoleTargets source (the successor daily's source is the successor hourly, per RollupViews/#4181). Also fixes a latent bug the extension surfaced: the legacy-only/ successor-only branch used to redirect through the two-argument RangeSqlFor(tier, relation) overload, whose OWN daily branch always names the legacy regardless of what is passed — so a "successor already covers the window" daily answer would have been silently downgraded back to the legacy. Route through QueriesCteForCagg directly with the name the builder actually chose, for both tiers. Replace the stale DailyTier_IgnoresTheStitch pin (no longer true) with five daily-tier tests mirroring the hourly ones: absent from availability, empty, successor-only, and the F_d stitch itself. All 8 DailySummaryStitchedRangeTests green, 14 more in the surrounding classes unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…y-view splice
Add NoReaderOutsideTheBuilder_SplicesADailyViewNameBySubstitution: a source
scan (same carve-out rule as NoReaderOutsideTheBuilder_CallsHourlyRelationForDirectly)
that fails on a literal collect.{...DailyView} interpolation outside
TimescaleSupport.cs — the exact shape the three FinOps splices and
ComposeSourceRouter's daily arm were in before this lane routed them, and
the shape that can never grow a stitch once a successor daily exists.
Proved red by reverting ViewerDataService.FinOps.Workload.cs:139 to its old
bare splice: the scan caught it at the exact line, then restored. 41
RollupCoverageRoutingTests green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…ot, route the 3 FinOps daily splices through it
The daily-splice scan (NoReaderOutsideTheBuilder_SplicesADailyViewNameBySubstitution)
matched only a literal collect.{TimescaleSupport.XDailyView} substring, so it missed
the three FinOps one-argument overloads, which splice the same constant from inside a
ternary: collect.{(tier == RetentionTier.Hourly ? ...HourlyView : ...DailyView)} AS f.
Replaced the literal patterns with three regexes over the same comment-stripped text:
an interpolation hole naming the constant anywhere in its expression (catches the
ternary), a "collect." + constant concatenation, and the bare view name after
collect. Confirmed red on exactly the three FinOps sites (FinOps.Workload.cs:119,
148, 170) before routing them.
Routed DatabaseResourceUsageSqlFor(tier), TopResourceConsumersByTotalSqlFor(tier) and
TopResourceConsumersByAvgSqlFor(tier) through their three-argument stitch-builder
overloads with RollupCoverage.Unknown (RollupAvailability.None), which makes
StitchedRelationSql answer collect.<legacy> AS f for both tiers -- the same bytes the
bare splice produced. Fixed their doc comments, which said the one-argument form
"reads the legacy hourly" even on the daily tier.
Scan now green; RetentionTierRouterTests pins (FinOpsReaders_RouteWithoutTrippingTheirDriftGuards)
pass unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
RangeSqlFor(tier, hourlyRelation)'s doc comment said the three-argument overload "calls this one only with the legacy-only splice's name (never a daily successor)". Since LA-8a that's no longer true: the three-argument overload builds its own queries CTE directly off QueriesCteForCagg rather than calling this one, so a successor-only daily keeps its own name. Corrected the doc comment. Renamed the three-argument overload's local legacyOnlySplice to singleSplice: the surrounding comment already says the builder can answer either the legacy OR the successor here (a single relation, not always the legacy), so the old name was misleading. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
LA-8b1 (scan hole and doc fixes)Two commits pushed to this shared branch ( 1. Scan-test hole
I replaced the literal Before routing the FinOps sites, the rewritten scan went red on exactly those three call sites, confirming it now catches what it used to miss: That is 3 offenders, matching the brief. I did not commit that red state. 2. Routed the three FinOps sites
I also fixed each function's The scan is green. The 3. DailySummarySql.cs(a) (b) I renamed the three-argument overload's local Both changes are comment and naming only. No SQL text changed. Build
I left these for whichever lane next touches those two files, since they are outside this lane's files. TestsRan by class, MTP exe, no PG rig,
Combined run across all 9 classes: Total 235, Failed 0, Errors 0. I did not run the full suite. CI runs it on push, per the brief. 🤖 Generated with Claude Code |
… FinOps, Compose, daily summary and all six pairs New DailyStitchLiveTests.cs, own scratch TimescaleDB store. One seeded server over six fixed-anchor calendar days (D0..D5) with a per-day row the interval-honest successors exclude and the legacy counts, so legacy/successor totals and unique_queries differ every day. Legacy refreshed for all six days; successor refreshed from D3 (F_d), with D4's successor DAILY deliberately left unrefreshed (while its successor HOURLY is) to exercise the not-carried NULL path. Proves with real numbers: FinOps' DatabaseResourceUsageSqlFor at the daily tier (39.0 ms / 183 executions, neither all-legacy 60.0/186 nor all-successor 18.0/180); a Compose panel through ComposeSourceRouter + ComposeCompiler (one point per day the stitched relation holds, D4 absent); DailySummarySql's not-carried probe (3/3/3/2/NULL/2 across the six days, D4 NULL because its source is the refreshed successor hourly); and a re-probe with RollupAvailability.WithoutIntervalDailies showing the pre-A6-daily numbers (D3=3, D4=3) the real probe replaced. A second fact forces all six stitch pairs (three hourly, three daily) into their UNION ALL form with a hand-built RollupCoverage and confirms SELECT * LIMIT 0 succeeds on each. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Part of #3653 (A6, lane LA-8).
Why
#4181 (lane LB) shipped the real daily-pair registry (
TimescaleSupport.SupersededDailyRollups) and theRollupAvailabilityflags for the three successor dailies, plus the three interval-honest successor dailyaggregates themselves. #4182 (lane LA) routed every hourly-tier reader through the stitch builder. But the
daily branch of that builder read a private placeholder (
s_supersededDailyRollupsStub) that no reader called.Once the successor dailies are backfilled, nothing would actually read them. Every daily-tier reader would keep
reading the frozen legacy daily forever. This lane (LA-8) wires the daily side up the same way the hourly side
already works.
What changes
Step 1, registry. Deleted
s_supersededDailyRollupsStub.RollupCoverage.StitchedRelationSqlandStitchFloor's daily branches now read the realTimescaleSupport.SupersededDailyRollups. They also gate on_availability.Has(successorDaily)in addition to the floor, the same way the hourly branch already does. Anabsent successor daily is never named in SQL, even if a stale floor happens to be cached for it.
Step 2, route the daily FROM-clause sites. One commit per file:
ComposeSourceRouter.cs: the daily arm ofResolveFamilynow setsCaggFromClauseviaStitchedRelationSql(dailyView, FactAlias, windowStartUtc, StitchTier.Daily), mirroring the hourly arm.CaggRelation(the by-name answer) is unchanged.ViewerDataService.FinOps.Workload.cs: the three coverage-aware overloads(
DatabaseResourceUsageSqlFor,TopResourceConsumersByTotalSqlFor,TopResourceConsumersByAvgSqlFor) had aternary that stitched the hourly branch but spliced the daily rollup by bare name
(
$"collect.{...DailyView} AS f"). The daily branch now callsStitchedRelationSql(..., StitchTier.Daily)too.
DailySummarySql.cs:RangeSqlFor(tier, coverage, windowStartUtc)only ever stitched the hourly tier.It now handles both tiers.
StitchFlooris asked on the tier's own legacy/StitchTierpair, and a genuinestitch runs
QueriesCteForStitchedCagg's not-carried probe twice, split at F_d, with each side's ownMaterializationHoleTargetssource (the successor daily's source is the successor hourly, already wired byRollupViews/issue-3653 A6 lane LB: the 3 interval-honest successor dailies + backfill runbook #4181, so no registry change was needed here).Extending this avoided a trap. The single-relation branch (legacy-only or successor-only) used to call the
two-argument
RangeSqlFor(tier, relation)overload, whose own daily branch always names the legacy daily.Sending the daily tier down that path would have turned a successor-only daily answer back into a legacy
read. On dev the daily tier never took that branch, so no shipped code had the bug. The branch now builds
the single-relation SQL itself, with the name the stitch builder chose, for both tiers.
Everything the design calls "name-only" was checked and left alone:
DarlingHealthReader.cs,DarlingMcpTrendTools.cs,DarlingTrendReader.cs,ViewerDataService.QueryTrends.cs, and the remainingViewerDataService.FinOps.Workload.cs/DailySummary.cscall sites all pass a daily view name only intocoverage.For(...)for the tier CHOICE, never into a FROM clause.The trend/query-history family (
DarlingTrendReader,DarlingMcpTrendTools,ViewerDataService.QueryTrends.cs)is stronger than "not yet routed".
DurationTrendRouting.ResolveTiercan only returnRaworHourly, neverDaily. Those readers have no daily FROM splice to route: the daily view name is architecturally unreachable asa FROM target there. Full grep census and disposition:
ComposeSourceRouter.cs:341(dailyView!)ViewerDataService.FinOps.Workload.cs:139/161/183DailySummarySql.csRangeSqlFor(tier, coverage, windowStart)DarlingHealthReader.cs:420DailySummarySql.RangeSqlForabove.ViewerDataService.DailySummary.cs:83ViewerDataService.FinOps.Workload.cs:256/417/485DarlingTrendReader.cs:913/921ResolveTiernever returns it).DarlingMcpTrendTools.cs:383ViewerDataService.QueryTrends.cs:328/337/508The tier CHOICE stays on the legacy pair everywhere (
coverage.For(legacyHourly, legacyDaily)). Only therelation spliced within the chosen tier can be a stitch.
Step 3, scan test. Added
NoReaderOutsideTheBuilder_SplicesADailyViewNameBySubstitutiontoRollupCoverageRoutingTests. It is a source scan, with the same carve-out rule as the existingNoReaderOutsideTheBuilder_CallsHourlyRelationForDirectly, that fails on a literalcollect.{...DailyView}string interpolation outside
TimescaleSupport.cs. That is the exact shape the three FinOps splices andComposeSourceRouter's daily arm were in before this lane. Proved red once by revertingViewerDataService.FinOps.Workload.cs:139to its old bare splice: the scan caught it at the exact line, thenthe revert was undone.
Test plan
StitchedRelationSqlTests: 12/12 green (registry + gating, step 1)ComposeSourceRouterTests+DarlingComposeTests: 306/306 green (step 2,ComposeSourceRouter.cs)ViewerFinOpsTests+RetentionTierRouterTests: 31/31 green (step 2,FinOps.Workload.cs)DailySummaryStitchedRangeTests: 8/8 green, plusDailySummaryNotCarriedTests+DailySummaryReadShapeTests+
IntervalHonestHourlyRollupTests: 14/14 green (step 2,DailySummarySql.cs)RollupCoverageRoutingTests: 41/41 green, new scan proved red once by reverting one site (step 3)Darling.Testssuite, once,DARLING_TEST_PGunset. Result: 13738 total, 0 Errors, 0 Failed, 686Skipped (live tests that need
DARLING_TEST_PG), 1 Not Run. No[FAIL]lines. The coordinator's ownbase run on dev (
la8-base-suite.log, informational, not a formalbase-failures.txt) likewise showedzero
[FAIL]lines, so there was nothing to diff against. The "1 Not Run" is not a failure. The dev baserun had the same 1 Not Run. I did not chase its identity given the deadline. It is worth a look if it
recurs.
FinOps, Compose and daily-summary reads all split cleanly at F_d with no gap and no overlap, plus a
not-carried hole and all six stitch pairs. No trend/query-history read is included.
DurationTrendRouting.ResolveTieronly ever returnsRaworHourly, so no trend reader can reach adaily FROM clause to prove. Numbers below.
CHANGELOG entry
Section: Changed
(FinOps database and top-consumer panels, the Compose chart builder, and the Performance Calendar / daily
summary) used to read only the legacy daily rollup. Once the store's successor dailies are backfilled, these
reads use the legacy daily for days before the successor's floor. They use the interval-honest successor
daily from that floor on, with no gap and no overlap. A store without the successor dailies, or with empty
ones, is unaffected: the read is byte-identical to before.
Live proofs (lane LA-8b2)
The new file
Darling/Darling.Tests/DailyStitchLiveTests.csholds two live tests. Each runs on its own scratch TimescaleDB store (#1776 own-store). Both passed withDARLING_TEST_PGset. The main test callsstop_background_workers()before the ensure sweep and cleans up throughLiveStoreCleanup.RunAsync. The six-pairs test only reads withLIMIT 0, and the scratch store's own disposal drops its database.Seed
The main test seeds one server and one database over six fixed UTC days, from
D0 = 2026-01-05toD5 = 2026-01-10. It never reads the wall clock. Each day gets threequery_statsrows:A6DAILYQ1: 1,000 worker units and 10 executions,sample_interval_seconds = 3600.A6DAILYQ2: 2,000 worker units and 20 executions,sample_interval_seconds = 3600.A6DAILYRESTART: 7,000 worker units and 1 execution,sample_interval_seconds = 0. This is a restart row. Every successor'sWHERE sample_interval_seconds IS DISTINCT FROM 0leaves it out, and no other row has its query hash.So each day's legacy total is 10,000 units, 31 executions and 3 distinct hashes. Each day's successor total is 3,000 units, 30 executions and 2 distinct hashes.
Refresh and F_d
query_stats_*andquery_stats_db_*) are refreshed for all six days.coverage.StitchFloor(QueryStatsDailyView, StitchTier.Daily, D0)returned exactly D3, inside the[D0, D6)window.FinOps
DatabaseResourceUsageSqlFor(RetentionTier.Daily, coverage, D0)reads the database grain, which has no empty day. It returnedcpu_time_ms = 39.0andexecution_count = 183. That is the legacy for D0 to D2 (3 x 10,000 units) plus the successor for D3 to D5 (3 x 3,000 units). An all-legacy read gives 60.0 ms and 186 executions, and an all-successor read gives 18.0 ms and 180.Compose
A
query_worker_uspanel with a daily bucket went throughComposeSourceRouterandComposeCompiler. The compiled SQL containsUNION ALLand names bothquery_stats_dailyandquery_stats_interval_daily. Five points came back:Daily summary
DailySummarySql.RangeSqlFor(RetentionTier.Daily, coverage, D0)returned six rows, one per day, with no gap and no overlap.unique_querieswas 3, 3, 3, 2, NULL, 2 for D0 to D5. D4 is NULL because the successor side's not-carried probe reads the successor hourly. That hourly holds rows for D4, but the successor daily does not.Contrast proof
The test probes the same store again with the product's
TimescaleSupport.DetectRollupCoverageAsync, this time withRollupAvailability.WithoutIntervalDailies, the shape before this PR.StitchFloorthen returns null, and the whole window reads the legacy. D3 comes back as 3 instead of 2, and D4 as 3 instead of NULL. The stitched assertions above fail on these numbers. The test keeps both reads side by side as permanent assertions, so it needs no temporary revert.All six stitch pairs
The second test builds a
RollupCoverageby hand, with fixed floors andRollupAvailability.All. Those floors force each of the three hourly pairs and the three daily pairs into theUNION ALLform. The test checks that the SQL containsUNION ALL, then runsSELECT * FROM <StitchedRelationSql(...)> LIMIT 0on a fresh store. It succeeded for all six pairs, so eachs_stitchColumnsByLegacylist exists on both sides.Totals
DailyStitchLiveTests: 2 of 2 passed.StitchedRelationSqlTests,DailySummaryStitchedRangeTests,ViewerFinOpsTests,RetentionTierRouterTests,ComposeSourceRouterTests,DarlingComposeTests,IntervalHonestHourlyRollupTests,IntervalHonestHourlyRollupLiveTests,SuccessorDailyLiveTests,DailySummaryNotCarriedTests,DailySummaryReadShapeTests): 375 of 375 passed, 0 skipped.RollupCoverageRoutingTests.cs,DailySummarySql.csandViewerDataService.FinOps.Workload.cs), a rerun of 51 tests inDailyStitchLiveTests,RollupCoverageRoutingTests,ViewerFinOpsTestsandDailySummaryStitchedRangeTestspassed.DailyStitchLiveTests.cs. The full suite was not run here, because CI runs it.LA-8b1's report on those three files is a separate PR comment. The merge had no conflicts, and the PostgreSQL rig on port 55981 was stopped after the run.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3