Skip to content

Commit 094ad71

Browse files
committed
Merge remote-tracking branch 'origin/enterprise' into feat/TG-1047-deduplicate-run-tables
2 parents 9f60020 + e091289 commit 094ad71

5 files changed

Lines changed: 109 additions & 5 deletions

File tree

testgen/commands/test_thresholds_prediction.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def run(self) -> None:
8989

9090
# Freshness update events are fetched as secondary data only when the suite
9191
# is a monitor — Volume_Trend / Metric_Trend in monitor suites couple to the
92-
# Freshness_Trend signal to avoid stairstep false positives.
92+
# Freshness_Trend signal to avoid stairstep false positives.
9393
freshness_updates_by_table: dict[tuple[str, str], list[str]] = (
9494
self._fetch_freshness_updates_by_table() if self.test_suite.is_monitor else {}
9595
)
@@ -268,10 +268,16 @@ def compute_freshness_threshold(
268268
window_end=schedule.window_end if has_window else None,
269269
)
270270
lower, upper = result.lower, result.upper
271-
staleness = result.staleness
272271
except NotEnoughData:
273272
pass # Keep first-pass thresholds
274273

274+
# An active schedule certifies the cadence is regular, so the tight staleness
275+
# threshold (median * factor) applies. Full-week schedules skip the recompute
276+
# above and use the first-pass result — with no excluded days, those gaps are
277+
# already in business minutes. Non-active stages keep staleness = None so the
278+
# prediction SQL falls back to upper_tolerance (avoids false weekend anomalies).
279+
staleness = result.staleness
280+
275281
# Override upper threshold with schedule-based deadline (daily/weekly only)
276282
if schedule.frequency != "sub_daily":
277283
holiday_dates = resolve_holiday_dates(holiday_codes, history.index) if holiday_codes else None
@@ -344,7 +350,7 @@ def compute_volume_or_metric_threshold(
344350
This avoids the "stairstep" false-positive shape where inter-change plateaus collapse the SE estimate.
345351
The returned prediction JSON is augmented with `freshness_gated` and `baseline_value` so
346352
that test execution can apply dual-branch evaluation.
347-
353+
348354
If the filtered fit fails for any reason, falls back to fit SARIMAX on
349355
the raw value series and emits a prediction JSON without the freshness-gating markers.
350356
@@ -381,5 +387,5 @@ def compute_volume_or_metric_threshold(
381387
exclude_weekends=exclude_weekends,
382388
holiday_codes=holiday_codes,
383389
schedule_tz=schedule_tz,
384-
)
390+
)
385391
return lower, upper, None, prediction
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
SET SEARCH_PATH TO {SCHEMA_NAME};
2+
3+
-- Schema-name matching in the SQL Server DDF query is now case-sensitive,
4+
-- consistent with TestGen's case-sensitive joins and the other flavors.
5+
-- A case-insensitive source previously accepted a table
6+
-- group whose configured schema name differed in case from the database;
7+
-- such a group would now match no tables on its next profiling run.
8+
--
9+
-- data_table_chars.schema_name holds the case the source database actually
10+
-- reported (it is populated from the DDF's c.table_schema, not the entered
11+
-- value), so realign table_group_schema to that case. Guards:
12+
-- * only mssql connections -- the DDF change was made only for that flavor,
13+
-- * only when the catalog reports a single unambiguous schema for the group,
14+
-- * only a case-only difference (LOWER() match), never a different schema,
15+
-- * '<>' is case-sensitive in PostgreSQL, so correct rows are left untouched.
16+
-- After this, the group's next profiling run stamps profile_results with the
17+
-- corrected case, realigning the case-sensitive downstream joins.
18+
UPDATE table_groups tg
19+
SET table_group_schema = actual.schema_name
20+
FROM (
21+
SELECT table_groups_id, MIN(schema_name) AS schema_name
22+
FROM data_table_chars
23+
WHERE drop_date IS NULL
24+
GROUP BY table_groups_id
25+
HAVING COUNT(DISTINCT schema_name) = 1
26+
) actual
27+
WHERE actual.table_groups_id = tg.id
28+
AND tg.table_group_schema <> actual.schema_name
29+
AND LOWER(tg.table_group_schema) = LOWER(actual.schema_name)
30+
AND EXISTS (
31+
SELECT 1 FROM connections cn
32+
WHERE cn.connection_id = tg.connection_id
33+
AND cn.sql_flavor = 'mssql'
34+
);

testgen/template/flavors/mssql/data_chars/get_schema_ddf.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ SELECT
5454
FROM information_schema.columns c
5555
LEFT JOIN approx_cts a ON c.table_schema = a.schema_name AND c.table_name = a.table_name
5656
LEFT JOIN information_schema.tables it ON c.table_schema = it.table_schema AND c.table_name = it.table_name
57-
WHERE c.table_schema = '{DATA_SCHEMA}' {TABLE_CRITERIA}
57+
WHERE c.table_schema = '{DATA_SCHEMA}' COLLATE Latin1_General_BIN {TABLE_CRITERIA}
5858
ORDER BY c.table_schema, c.table_name, c.ordinal_position;

tests/unit/common/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,24 @@ def _gen_subdaily_gap_schedule_phase() -> list[tuple[pd.Timestamp, float]]:
294294
return _to_csv_rows(_make_observations(start, end, 2, updates))
295295

296296

297+
def _gen_twice_daily_outage() -> list[tuple[pd.Timestamp, float]]:
298+
"""Updates every 12h (00:00/12:00 UTC), 7 days a week, 5 weeks, then updates stop.
299+
300+
The 12h cadence classifies as "daily" frequency with a full-week active schedule,
301+
so no excluded days or sub-daily window apply — staleness must come from the
302+
first-pass gap thresholds.
303+
"""
304+
start = datetime(2025, 10, 6, 0, 0)
305+
end = datetime(2025, 11, 11, 12, 0)
306+
outage_start = datetime(2025, 11, 10, 0, 0)
307+
updates: set[datetime] = set()
308+
d = start
309+
while d < outage_start:
310+
updates.add(d)
311+
d += timedelta(hours=12)
312+
return _to_csv_rows(_make_observations(start, end, 12, updates))
313+
314+
297315
def _gen_weekly_early() -> list[tuple[pd.Timestamp, float]]:
298316
start = datetime(2025, 8, 7, 10, 0)
299317
end = datetime(2025, 11, 6, 22, 0)

tests/unit/common/test_freshness_scenarios.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
_gen_subdaily_gap_schedule_phase,
2626
_gen_subdaily_regular,
2727
_gen_training_only,
28+
_gen_twice_daily_outage,
2829
_gen_weekly_early,
2930
_run_scenario,
3031
)
@@ -472,3 +473,48 @@ def test_recovery_passes(self, results: list[ScenarioPoint]) -> None:
472473
assert post_recovery[0].result_code == 0
473474
# Second update after recovery should pass
474475
assert post_recovery[1].result_code == 1
476+
477+
478+
# ─── Scenario 9: Twice-Daily Outage (full-week schedule) ────────────
479+
480+
481+
class Test_TwiceDailyOutage:
482+
"""Updates every 12h around the clock for 5 weeks, then updates stop.
483+
484+
Full-week "daily"-frequency schedule: no excluded days and no sub-daily
485+
window, so the staleness threshold must come from the first-pass gap
486+
computation. With a 720-min median gap, staleness = 720 * 0.85 = 612 min,
487+
so the first missed check (gap 720 min) flags Late.
488+
"""
489+
490+
@pytest.fixture(scope="class")
491+
def results(self) -> list[ScenarioPoint]:
492+
rows = _gen_twice_daily_outage()
493+
return _run_scenario(rows, PredictSensitivity.medium, exclude_weekends=False, tz="UTC")
494+
495+
def test_schedule_goes_active_all_days(self, results: list[ScenarioPoint]) -> None:
496+
sched = _schedule(_updates(results)[-1])
497+
assert sched is not None
498+
assert sched.get("schedule_stage") == "active"
499+
assert sched.get("active_days") == [0, 1, 2, 3, 4, 5, 6]
500+
501+
def test_staleness_stored_once_active(self, results: list[ScenarioPoint]) -> None:
502+
last_update = _updates(results)[-1]
503+
assert last_update.staleness == pytest.approx(720 * 0.85)
504+
505+
def test_no_anomalies_before_outage(self, results: list[ScenarioPoint]) -> None:
506+
outage_start = pd.Timestamp("2025-11-10")
507+
assert all(p.timestamp >= outage_start for p in _anomalies(results))
508+
509+
def test_first_missed_check_flags_late(self, results: list[ScenarioPoint]) -> None:
510+
anomalies = _anomalies(results)
511+
assert len(anomalies) > 0
512+
first = anomalies[0]
513+
assert first.timestamp == pd.Timestamp("2025-11-10 00:00")
514+
assert first.value == 720
515+
516+
def test_all_missed_checks_flag(self, results: list[ScenarioPoint]) -> None:
517+
outage_start = pd.Timestamp("2025-11-10")
518+
missed_checks = [p for p in results if p.timestamp >= outage_start]
519+
assert len(missed_checks) == 4
520+
assert all(p.result_code == 0 for p in missed_checks)

0 commit comments

Comments
 (0)