Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 374 additions & 0 deletions Darling/Darling.Tests/DailyStitchLiveTests.cs

Large diffs are not rendered by default.

90 changes: 88 additions & 2 deletions Darling/Darling.Tests/DailySummaryStitchedRangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,42 @@ the literals are identical. */
Assert.Equal(boundaryDay, successorFloor.Date.AddDays(1));
}

/* ─────────────────────────── daily tier (#3653 A6, lane LA-8) ─────────────────────────── */

private const string DailyLegacy = TimescaleSupport.QueryStatsDailyView;
private const string DailySuccessor = "query_stats_interval_daily";

[Fact]
public void DailyTier_IgnoresTheStitch_ReadsExactlyAsTheTwoArgumentFormDoes()
public void DailyTier_NoSuccessorInAvailability_IsByteIdenticalToTheTwoArgumentForm()
{
/* Pre-LB shape: the successor daily is not in RollupAvailability at all, even with hourly floors
cached for the OTHER (hourly-tier) pair — the daily tier's own StitchFloor must gate on its own
registry/availability, never leak the hourly tier's state. */
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal) { [Legacy] = DaysAgo(80), [Successor] = DaysAgo(5) },
new Dictionary<string, DateTime>(StringComparer.Ordinal)
{
[DailyLegacy] = DaysAgo(200),
[Legacy] = DaysAgo(80),
[Successor] = DaysAgo(5),
},
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.WithoutIntervalDailies);

var stitched = DailySummarySql.RangeSqlFor(RetentionTier.Daily, coverage, DaysAgo(10));
var plain = DailySummarySql.RangeSqlFor(RetentionTier.Daily);

Assert.Equal(plain, stitched);
Assert.Contains("queries_ceiling AS (", stitched, StringComparison.Ordinal);
Assert.DoesNotContain("queries_ceiling_legacy", stitched, StringComparison.Ordinal);
}

[Fact]
public void DailyTier_SuccessorEmpty_IsByteIdenticalToTheTwoArgumentForm()
{
/* The successor daily is registered and available but has never refreshed (no floor) — legacy-only,
same text as today. */
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal) { [DailyLegacy] = DaysAgo(200) },
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.All);

Expand All @@ -132,6 +163,61 @@ public void DailyTier_IgnoresTheStitch_ReadsExactlyAsTheTwoArgumentFormDoes()
Assert.Equal(plain, stitched);
}

[Fact]
public void DailyTier_SuccessorReachesBeforeWindowStart_RoutesSuccessorOnly_NeverNamesTheLegacy()
{
var successorHourlyFloor = DaysAgo(90);
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal)
{
[DailyLegacy] = DaysAgo(200),
[DailySuccessor] = DaysAgo(90),
[TimescaleSupport.QueryStatsIntervalHourlyView] = successorHourlyFloor,
},
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.All);

var sql = DailySummarySql.RangeSqlFor(RetentionTier.Daily, coverage, DaysAgo(10));

Assert.Contains($"FROM collect.{DailySuccessor}", sql, StringComparison.Ordinal);
Assert.DoesNotContain(DailyLegacy, sql, StringComparison.Ordinal);
}

[Fact]
public void DailyTier_StitchedPair_SplitsTheNotCarriedProbeOnceEachSide_AtF_d()
{
/* Successor hourly's first bucket is mid-day, so F_d (ceiling-of-day) lands on the NEXT calendar day —
the same day-alignment rule QueriesCteForStitchedCagg documents for the hourly pair. */
var successorHourlyFloor = new DateTime(2026, 9, 19, 14, 0, 0, DateTimeKind.Utc);
var successorDailyFloor = new DateTime(2026, 9, 19, 0, 0, 0, DateTimeKind.Utc);
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal)
{
[DailyLegacy] = DaysAgo(200),
[DailySuccessor] = successorDailyFloor,
[TimescaleSupport.QueryStatsIntervalHourlyView] = successorHourlyFloor,
},
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.All);

var sql = DailySummarySql.RangeSqlFor(RetentionTier.Daily, coverage, DaysAgo(60));

var boundaryDay = new DateTime(2026, 9, 20, 0, 0, 0, DateTimeKind.Unspecified);
var literal = $"TIMESTAMP '{boundaryDay:yyyy-MM-dd HH:mm:ss.ffffff}'";

Assert.Contains($"FROM collect.{DailyLegacy}", sql, StringComparison.Ordinal);
Assert.Contains($"FROM collect.{DailySuccessor}", sql, StringComparison.Ordinal);
Assert.Contains($"bucket >= $2 AND bucket < $3 AND bucket < {literal}", sql, StringComparison.Ordinal);
Assert.Contains($"bucket >= $2 AND bucket < $3 AND bucket >= {literal}", sql, StringComparison.Ordinal);
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(sql, "NOT EXISTS").Count);
Assert.Contains("queries_ceiling_legacy", sql, StringComparison.Ordinal);
Assert.Contains("queries_ceiling_successor", sql, StringComparison.Ordinal);

/* The not-carried probe's source for the successor SIDE is the successor HOURLY (MaterializationHoleTargets,
RollupViews), not raw query_stats directly — the daily's hierarchical source. */
Assert.Contains($"SELECT 1 FROM collect.{TimescaleSupport.QueryStatsIntervalHourlyView} AS s", sql, StringComparison.Ordinal);
}

[Fact]
public void RawTier_IgnoresTheStitch_ReadsExactlyAsTheTwoArgumentFormDoes()
{
Expand Down
75 changes: 75 additions & 0 deletions Darling/Darling.Tests/RollupCoverageRoutingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,81 @@ public void NoReaderOutsideTheBuilder_CallsHourlyRelationForDirectly()
string.Join("\n", offenders));
}

/// <summary>
/// #3653 A6, lane LA-8: no reader outside <c>TimescaleSupport.cs</c> (the builder) may splice a DAILY
/// rollup's name directly into a FROM clause by string interpolation — <c>$"collect.{...DailyView} AS f"</c>
/// or similar — instead of going through <see cref="RollupCoverage.StitchedRelationSql"/>. A bare splice
/// like that can never grow a stitch: it will read only the legacy daily forever, even once a successor
/// daily exists and is backfilled, which is exactly the shape the three FinOps splices and
/// <c>ComposeSourceRouter</c>'s daily arm were in before this lane routed them.
///
/// <para>Matched over comment-stripped text against three regex shapes, so a splice cannot dodge the scan
/// merely by moving the constant reference inside a ternary or a string concatenation:
/// <list type="bullet">
/// <item>an interpolation hole naming a legacy daily constant ANYWHERE in its expression — so
/// <c>collect.{(tier == RetentionTier.Hourly ? ... : TimescaleSupport.QueryStatsDbDailyView)} AS f</c> is
/// caught, not just the bare <c>collect.{TimescaleSupport.X} AS f</c> shape;</item>
/// <item>a <c>"collect." + TimescaleSupport.&lt;X&gt;DailyView</c> (or unqualified) concatenation; and</item>
/// <item>the legacy view NAME itself written straight after <c>collect.</c>, for a caller that already
/// resolved the constant into a local.</item>
/// </list></para>
///
/// <para><see cref="IsExcludedFromScan"/> keeps the test project and build output out of the walk, same
/// carve-out as <see cref="NoReaderOutsideTheBuilder_CallsHourlyRelationForDirectly"/> above;
/// <c>TimescaleSupport.cs</c> is excluded the same way — it is the builder, and
/// <c>s_stitchColumnsByLegacy</c>'s doc comments and the registry itself necessarily say these names beside
/// <c>collect.</c> text.</para>
/// </summary>
[Fact]
public void NoReaderOutsideTheBuilder_SplicesADailyViewNameBySubstitution()
{
var root = FindRepoRoot();

Assert.True(root is not null,
"Could not locate the repository root (walked up from the test binary looking for " +
"PerformanceMonitor.sln). This test scans the source tree, so it cannot run without it — fix the " +
"walk-up rather than skipping.");

const string constants = @"(?:QueryStatsDailyView|ProcedureStatsDailyView|QueryStatsDbDailyView)";
var viewNames = "(?:" + string.Join("|", TimescaleSupport.SupersededDailyRollups.Select(p => Regex.Escape(p.LegacyDaily))) + ")";
var patterns = new[]
{
new Regex(@"collect\.\{[^{}]*\b" + constants + @"\b[^{}]*\}", RegexOptions.CultureInvariant),
new Regex(@"collect\.""\s*\+\s*(?:TimescaleSupport\.)?" + constants + @"\b", RegexOptions.CultureInvariant),
new Regex(@"collect\." + viewNames + @"\b", RegexOptions.CultureInvariant),
};

var offenders = new List<string>();
foreach (var file in Directory.EnumerateFiles(Path.Combine(root!, "Darling"), "*.cs", SearchOption.AllDirectories))
{
var relative = Path.GetRelativePath(root!, file);
if (IsExcludedFromScan(relative)
|| string.Equals(Path.GetFileName(relative), "TimescaleSupport.cs", StringComparison.OrdinalIgnoreCase))
{
continue;
}

var text = StripComments(File.ReadAllText(file));
foreach (var pattern in patterns)
{
foreach (Match match in pattern.Matches(text))
{
var line = text.AsSpan(0, match.Index).Count('\n') + 1;
offenders.Add($"{relative}:{line} ({match.Value})");
}
}
}

Assert.True(offenders.Count == 0,
"These call sites splice a legacy DAILY rollup's name directly into a FROM clause by string " +
"interpolation instead of routing through RollupCoverage.StitchedRelationSql (#3653 A6, lane " +
"LA-8). A reader built this way can never read an interval-honest successor daily once one exists " +
"and is backfilled — it is stuck on the frozen legacy forever, invisibly. Route through " +
"StitchedRelationSql(legacyDaily, alias, windowStartUtc, RollupCoverage.StitchTier.Daily) instead; " +
"the daily view constants stay reserved for probes, logs, and the registry itself.\n\n" +
string.Join("\n", offenders));
}

/// <summary>
/// True when a path is generated build output, or the test project's own source. Compared as whole
/// path SEGMENTS against BOTH separator characters, so the scanned set is the same wherever this runs
Expand Down
31 changes: 29 additions & 2 deletions Darling/Darling.Tests/StitchedRelationSqlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,36 @@ public void EveryPair_HasAColumnListRegistered()
/* ─────────────────────────── daily tier ─────────────────────────── */

[Fact]
public void DailyStitch_IsInertWhenSuccessorDailyIsAbsent()
public void DailyStitch_SuccessorAbsentFromAvailability_IsLegacyOnly_ByteIdenticalToToday()
{
/* No successor daily registered/available at all (today's shipped shape, before LB): legacy-only. */
/* Pre-LB shape (RollupAvailability.WithoutIntervalDailies): the successor daily's name is not in
RollupAvailability at all, so Has() answers false — legacy-only, even though BOTH the successor
daily's and its successor hourly's floors happen to be cached (a store whose store-schema probe
predates LB but whose floor dictionary was seeded anyway; Has() is the authoritative gate, never the
floor). Both floors are seeded so that, absent the gate, StitchedRelationSql would have enough to
build a stitch instead of falling through some other null check — this proves the gate itself, not a
side effect of a missing floor. Byte-identical to today's un-stitched single-relation text
(#3653 A6 design v2 §2). */
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal)
{
[TimescaleSupport.QueryStatsDailyView] = DaysAgo(200),
["query_stats_interval_daily"] = DaysAgo(5),
[TimescaleSupport.QueryStatsIntervalHourlyView] = DaysAgo(5),
},
new Dictionary<string, DateTime>(StringComparer.Ordinal),
RollupAvailability.WithoutIntervalDailies);

var sql = coverage.StitchedRelationSql(TimescaleSupport.QueryStatsDailyView, "f", DaysAgo(10), RollupCoverage.StitchTier.Daily);

Assert.Equal($"collect.{TimescaleSupport.QueryStatsDailyView} AS f", sql);
}

[Fact]
public void DailyStitch_SuccessorPresentButEmpty_IsLegacyOnly()
{
/* The successor daily is registered and available (Has() true) but has never been refreshed or
backfilled — no floor. Empty never wins while the legacy holds rows (#3653 A6 design v2 §2). */
var coverage = new RollupCoverage(
new Dictionary<string, DateTime>(StringComparer.Ordinal) { [TimescaleSupport.QueryStatsDailyView] = DaysAgo(200) },
new Dictionary<string, DateTime>(StringComparer.Ordinal),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,10 @@ a guard that stays honest. Do not "simplify" it back. */
into the window as the legacy does, the legacy otherwise. Same dimensions, same column names on both,
so the compiled SQL and ComposeCaggValueMapper are identical either way — the #1849 arrangement, one
relation name swapped. The Query Store family has no successor here and comes back unchanged. The
daily tier is untouched: the dailies are hierarchical from the LEGACY hourly and have no successor of
their own yet (TimescaleSupport.SupersededHourlyRollups states why). */
daily tier's RELATION is chosen the same way as the hourly's, below (#3653 A6): CaggRelation is still
the by-name answer for probes/logs/registry lookups, and CaggFromClause may stitch the legacy daily to
its own interval-honest successor daily at F_d — the tier CHOICE itself stays on the legacy pair
(coverage.For above), only the RELATION within the chosen tier can be a stitch. */
return tier switch
{
RetentionTier.Raw => (ComposeRoute.Raw, null),
Expand All @@ -350,7 +352,14 @@ compiler appended " AS f" itself. */
coverage.HourlyRelationNameFor(hourlyView, windowStartUtc),
coverage.StitchedRelationSql(hourlyView, ComposeRoute.FactAlias, windowStartUtc, RollupCoverage.StitchTier.Hourly)),
tierCoverage.HourlyFloorUtc),
_ => (new ComposeRoute(ComposeSourceTier.Daily, dailyView!), tierCoverage.DailyFloorUtc),
/* #3653 A6: same shape as the hourly arm above, but StitchTier.Daily — the successor daily's own
floor and its successor hourly's ceiling-of-day decide F_d (RollupCoverage.StitchedRelationSql).
With no successor daily this is byte-identical to "collect.<dailyView> AS f", same as today. */
_ => (new ComposeRoute(
ComposeSourceTier.Daily,
dailyView!,
coverage.StitchedRelationSql(dailyView!, ComposeRoute.FactAlias, windowStartUtc, RollupCoverage.StitchTier.Daily)),
tierCoverage.DailyFloorUtc),
};
}
}
Loading
Loading