File size: 6,198 Bytes
c9dd6cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
Semantic (non-fatal) checks in the post-load quality gate.

The original gate only caught measures that loaded 100% zero/null — a
guaranteed blank tile. These checks catch data that *renders* but reads wrong,
which is worse in a demo because nobody notices until a prospect does:

  - Tixr shipped TICKET_SELL_THROUGH_RATE = 1.96 (a 196% sell-through).
  - Triumph's *_MONTH bucket columns held random days of the month.
  - 500 fact rows spread over 24 months left most periods nearly empty.

These are warnings, not failures, by design — see _run_semantic_checks.

Run with: pytest tests/test_quality_gate_semantics.py -v
"""

import pytest

from demoprep_app.integrations.snowflake.quality_gate import (
    MIN_ROWS_PER_MONTH_WARN,
    bounded_range_for_column,
    is_date_column,
    is_month_bucket_column,
    run_measure_quality_gate,
)


class TestClassifiers:
    def test_rate_and_ratio_are_zero_to_one(self):
        assert bounded_range_for_column("TICKET_SELL_THROUGH_RATE") == (0.0, 1.0)
        assert bounded_range_for_column("WIN_RATIO") == (0.0, 1.0)

    def test_percent_columns_are_zero_to_one_hundred(self):
        assert bounded_range_for_column("BROKER_MARGIN_PCT") == (0.0, 100.0)
        assert bounded_range_for_column("DISCOUNT_PERCENT") == (0.0, 100.0)
        assert bounded_range_for_column("UPTIME_PERCENTAGE") == (0.0, 100.0)

    def test_unbounded_columns_return_none(self):
        assert bounded_range_for_column("TOTAL_REVENUE_USD") is None
        assert bounded_range_for_column("ORDER_COUNT") is None
        # "RATE" must be a suffix, not a substring — RATED_CAPACITY is not a rate.
        assert bounded_range_for_column("RATED_CAPACITY") is None

    def test_month_bucket_requires_date_type(self):
        assert is_month_bucket_column("ORDER_MONTH", "DATE")
        assert is_month_bucket_column("ORDER_MONTH", "TIMESTAMP_NTZ")
        # A numeric *_MONTH is a month number, not a bucket date.
        assert not is_month_bucket_column("FISCAL_MONTH", "NUMBER(2,0)")
        assert not is_month_bucket_column("ORDER_DATE", "DATE")

    def test_is_date_column(self):
        assert is_date_column("DATE")
        assert is_date_column("TIMESTAMP_LTZ")
        assert not is_date_column("VARCHAR(10)")
        assert not is_date_column("NUMBER")


class SemanticCursor:
    """Routes each query shape to a canned answer.

    Distinguishes the four query shapes the gate issues: the column catalogue,
    the per-table non-zero aggregate, bounds violations, month-start
    violations, and the density probe.
    """

    def __init__(self, columns_rows, aggregate, *, out_of_range=(0, None, None),
                 bad_month_rows=0, density=(1000, 12)):
        self._columns_rows = columns_rows
        self._aggregate = aggregate
        self._out_of_range = out_of_range
        self._bad_month_rows = bad_month_rows
        self._density = density
        self._result = None

    def execute(self, sql, params=None):
        if "INFORMATION_SCHEMA.COLUMNS" in sql:
            self._result = self._columns_rows
        elif "DATE_TRUNC" in sql:
            self._result = self._density
        elif "DAY(" in sql:
            self._result = (self._bad_month_rows,)
        elif "MIN(" in sql and "MAX(" in sql:
            self._result = self._out_of_range
        else:
            self._result = self._aggregate

    def fetchall(self):
        return self._result

    def fetchone(self):
        return self._result

    def close(self):
        pass


class SemanticConnection:
    def __init__(self, cursor):
        self._cursor = cursor

    def cursor(self):
        return self._cursor


COLUMNS = [
    ("FACT_TICKET_SALE", "TICKET_ID", "NUMBER"),
    ("FACT_TICKET_SALE", "TICKETS_SOLD", "NUMBER"),
    ("FACT_TICKET_SALE", "TICKET_SELL_THROUGH_RATE", "FLOAT"),
    ("FACT_TICKET_SALE", "SALE_MONTH", "DATE"),
]
# (row_count, nonzero TICKETS_SOLD, nonzero RATE)
CLEAN_AGG = (1000, 1000, 1000)


def _run(cursor):
    return run_measure_quality_gate(SemanticConnection(cursor), "DEMO_SCHEMA")


def test_out_of_range_rate_warns_but_does_not_fail():
    cursor = SemanticCursor(COLUMNS, CLEAN_AGG, out_of_range=(7, 0.0, 1.96))
    profile = _run(cursor)

    warning = " ".join(profile["semantic_warnings"])
    assert "TICKET_SELL_THROUGH_RATE" in warning
    assert "7 row(s) outside expected range" in warning
    assert "1.96" in warning
    assert profile["failures"] == []


def test_non_month_start_dates_warn():
    cursor = SemanticCursor(COLUMNS, CLEAN_AGG, bad_month_rows=412)
    profile = _run(cursor)

    warning = " ".join(profile["semantic_warnings"])
    assert "SALE_MONTH" in warning
    assert "412 row(s) are not month-start" in warning


def test_sparse_panel_warns():
    """500 rows over 24 months = ~21/month, below the density threshold."""
    cursor = SemanticCursor(COLUMNS, CLEAN_AGG, density=(500, 24))
    profile = _run(cursor)

    warning = " ".join(profile["semantic_warnings"])
    assert "FACT_TICKET_SALE is sparse over time" in warning
    assert "20.8/month" in warning


def test_dense_panel_is_silent():
    cursor = SemanticCursor(COLUMNS, CLEAN_AGG, density=(12000, 24))
    profile = _run(cursor)
    assert not any("sparse" in w for w in profile["semantic_warnings"])


def test_clean_data_produces_no_semantic_warnings():
    cursor = SemanticCursor(COLUMNS, CLEAN_AGG)
    profile = _run(cursor)
    assert profile["semantic_warnings"] == []


def test_semantic_failure_cannot_break_the_run():
    """A broken semantic probe degrades to a note, never an exception."""

    class ExplodingCursor(SemanticCursor):
        def execute(self, sql, params=None):
            if "INFORMATION_SCHEMA.COLUMNS" in sql:
                self._result = self._columns_rows
            elif "SUM(IFF" in sql:
                self._result = self._aggregate
            else:
                raise RuntimeError("probe blew up")

    profile = _run(ExplodingCursor(COLUMNS, CLEAN_AGG))
    assert any("semantic checks skipped" in w for w in profile["warnings"])
    assert profile["failures"] == []


def test_density_threshold_is_named_not_magic():
    assert MIN_ROWS_PER_MONTH_WARN == 30