Skip to content

Commit 07164db

Browse files
committed
test(benchmarks): pin DuckShop edge array order and guard the core copy by checksum
Sort the results of ARRAY_EXCEPT, ARRAY_DISTINCT and OBJECT_KEYS, whose element order is unspecified, and recapture their expectations. The core-copy compile guard now compares the copy's compiled files with a recorded checksum, which core_project.py --checksum prints. The core copy also leaves out the edge Rule and its tests, renamed with an edge_ prefix, and the marked edge blocks of sqlbuild_project.toml. The Rule reads only the header's top-level tags, past strings holding ');'. The README and RUNBOOK say which expectations are captured and which are written by hand, that a schema built with the old identifier-case setting needs a clean rebuild, and that DEFAULT_DDL_COLLATION must stay at its default on the edge schema.
1 parent 4234eaf commit 07164db

12 files changed

Lines changed: 217 additions & 64 deletions

File tree

‎benchmarks/duckshop/README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,18 @@ groups:
129129
| Ordering, numbers and text | NULL ordering under `DESC`; ties in ordered aggregates, `MAX_BY`, `MODE` and `ANY_VALUE`; default window frames; `UNION ALL BY NAME`; `ROUND` half-way values, decimal division, `MOD` and 38-digit amounts; integers past 2^53 against `FLOAT`; regular expressions and backslash escapes; `LIKE ... ESCAPE` and `ILIKE ANY`; `SPLIT_PART` and `SUBSTR` before the start; `NOT IN` with a NULL; names in many scripts |
130130
| Scenarios and lifecycle | typed values across model boundaries; `VARIANT` expected output; a star under a lower-case contract and a quoted camelCase export; a cursor-window incremental model over far-apart fixture loads; a change log merged across two loads, checked against a full rebuild |
131131

132-
Every expected value in the edge-case area was captured on Snowflake under the `edge` target's
133-
pinned session, rather than derived by hand. `fct_edge_delivery_events` is disabled: it is the
132+
Unit-test expectations in the edge-case area were captured on Snowflake under the `edge` target's
133+
pinned session, rather than derived by hand; scenario expectations, and the assertions in tests
134+
about ties, are written by hand from those captured values. `fct_edge_delivery_events` is disabled: it is the
134135
acceptance case for cursor `delete_insert` models that emit rows outside the window they replace.
135136
See [RUNBOOK.md](RUNBOOK.md), step 7.
136137

137138
Benchmarks and core measurements leave the area out:
138139

139140
- `python benchmarks/duckshop/scripts/core_project.py <destination>` copies the project without
140-
it; the copy compiles to the same files as the project did before the area existed;
141+
it. The core-copy compile guard compares the copy's compiled files with a recorded checksum
142+
(`core_project.py --checksum`), so a change to the core project's compiled SQL fails the guard
143+
until the checksum is updated on purpose and noted for the benchmark;
141144
- warehouse runs exclude it with `--exclude tag:edge_case "source:edge__*"`, and scenario runs
142145
with `--exclude edge_cases`;
143146
- the construct census counts the core project unless given `--area edge` or `--area all`.

‎benchmarks/duckshop/RUNBOOK.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ no DuckShop names; if it does, stop and pick another schema. Note which state ta
5454
The list includes the edge-case area's names; run the check with `--target edge` too before its
5555
first build.
5656

57+
The project no longer sets `QUOTED_IDENTIFIERS_IGNORE_CASE = true` on its connections. A schema
58+
built while it did may hold objects and SQLBuild state that no longer resolve under Snowflake's
59+
default case rules, so rebuild into a clean schema: drop everything as in step 8, or pick a new
60+
schema, before the first build.
61+
5762
## 3. Load the synthetic data
5863

5964
```bash
@@ -162,6 +167,11 @@ values were captured under: UTC, and Snowflake's defaults for every parameter th
162167
result or how text is parsed, including `QUOTED_IDENTIFIERS_IGNORE_CASE = false`. Run it on that
163168
target only; under another session some expected values differ by design.
164169

170+
`DEFAULT_DDL_COLLATION` also changes results (comparisons, ordering and `LIKE` on text columns),
171+
but it is an account, database or schema parameter, not a session one, so the connection cannot
172+
pin it. The edge target's schema, and its database and account, must leave it at the default (no
173+
collation). Check with `SHOW PARAMETERS LIKE 'DEFAULT_DDL_COLLATION' IN SCHEMA <database>.<schema>`.
174+
165175
### Load, build and test
166176

167177
```bash
@@ -202,8 +212,8 @@ chose.
202212

203213
### Capture expected values
204214

205-
Edge-case expectations are Snowflake's own results on the fixtures, not values derived by hand.
206-
To capture them for a new or changed edge test, write its `__expected__<model>` CTE with the
215+
Unit-test expectations in the edge-case area are Snowflake's own results on the fixtures, not
216+
values derived by hand. To capture them for a new or changed edge test, write its `__expected__<model>` CTE with the
207217
columns to compare (any values, such as `NULL AS column_name`), then run:
208218

209219
```bash
@@ -216,7 +226,8 @@ replaces each expected CTE with the actual rows as typed literals. Without a tes
216226
recaptures every edge test, which must leave the files unchanged. Check every captured value
217227
against the case the test describes before committing: a capture records what Snowflake returned,
218228
which is the point, but a fixture mistake is captured as faithfully as a real edge. Scenario
219-
expectations are written by hand from the captured unit-test values.
229+
expectations, and the assertions in tests about ties (where Snowflake may return any of the tied
230+
rows), are written by hand from the captured values.
220231

221232
Capture scenario snapshots for the edge scenarios with
222233
`sqb scenario capture --target edge --select edge_cases`.

‎benchmarks/duckshop/project/models/intermediate/edge_cases/int_edge_payload_arrays.sql‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,21 @@ final AS (
3737
p.payload:gift_code::VARCHAR
3838
)::ARRAY AS codes_compact,
3939
ARRAY_COMPACT(p.payload:tags::ARRAY)::ARRAY AS tags_compact,
40-
ARRAY_EXCEPT(p.payload:tags::ARRAY, p.payload:removed_tags::ARRAY)::ARRAY AS tags_kept,
40+
-- ARRAY_EXCEPT and ARRAY_DISTINCT leave element order unspecified; sort ascending, NULLs last.
41+
ARRAY_SORT(
42+
ARRAY_EXCEPT(p.payload:tags::ARRAY, p.payload:removed_tags::ARRAY),
43+
TRUE,
44+
FALSE
45+
)::ARRAY AS tags_kept,
4146
ARRAY_CAT(
4247
p.payload:tags::ARRAY,
4348
p.payload:extra_tags::ARRAY
4449
)::ARRAY AS tags_all,
45-
ARRAY_DISTINCT(p.payload:tags::ARRAY)::ARRAY AS tags_distinct,
50+
ARRAY_SORT(
51+
ARRAY_DISTINCT(p.payload:tags::ARRAY),
52+
TRUE,
53+
FALSE
54+
)::ARRAY AS tags_distinct,
4655
ARRAY_POSITION(p.payload:category_id, p.payload:eligible_ids::ARRAY)::INT AS category_position,
4756
ARRAY_CONTAINS(p.payload:category_id, p.payload:eligible_ids::ARRAY)::BOOLEAN AS is_eligible,
4857
ARRAYS_OVERLAP(

‎benchmarks/duckshop/project/models/intermediate/edge_cases/int_edge_payload_objects.sql‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ final AS (
7575
n.payload:pick_keys::ARRAY
7676
)::OBJECT AS picked_by_array,
7777
OBJECT_PICK(n.payload::OBJECT, n.payload:pick_keys)::OBJECT AS picked_by_variant,
78-
OBJECT_KEYS(n.payload::OBJECT)::ARRAY AS payload_keys
78+
-- OBJECT_KEYS leaves key order unspecified; sort ascending.
79+
ARRAY_SORT(OBJECT_KEYS(n.payload::OBJECT), TRUE, FALSE)::ARRAY AS payload_keys
7980
FROM normalised AS n
8081
)
8182

‎benchmarks/duckshop/project/rules/area_tags.py‎

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""DuckShop house rule that keeps the edge-case area selectable on its own."""
2+
3+
import re
4+
5+
from sqlbuild.rules import Finding, Model, RuleContext, rule
6+
7+
EDGE_FOLDER = "edge_cases"
8+
EDGE_TAG = "edge_case"
9+
_TAGS_KEY = re.compile(r"tags\s*\[([^\]]*)\]")
10+
_QUOTES = ('"""', '"', "'")
11+
12+
13+
def _string_end(sql: str, start: int) -> int:
14+
"""The index after the string literal opening at `start`."""
15+
16+
quote = next(q for q in _QUOTES if sql.startswith(q, start))
17+
index = start + len(quote)
18+
while index < len(sql):
19+
if len(quote) == 1 and sql[index] == "\\":
20+
index += 2
21+
elif sql.startswith(quote, index):
22+
return index + len(quote)
23+
else:
24+
index += 1
25+
return len(sql)
26+
27+
28+
def _header_tags(sql: str) -> tuple[set[str], int]:
29+
"""The tags the MODEL header declares at its top level, and their line."""
30+
31+
depth = 0
32+
index = sql.find("(")
33+
while 0 <= index < len(sql):
34+
char = sql[index]
35+
if char in "\"'":
36+
index = _string_end(sql, index)
37+
continue
38+
if char == "(":
39+
depth += 1
40+
elif char == ")":
41+
depth -= 1
42+
if depth == 0:
43+
break
44+
elif depth == 1 and sql.startswith("tags", index) and not sql[index - 1].isalnum():
45+
match = _TAGS_KEY.match(sql, index)
46+
if match is not None:
47+
tags = {t.strip().strip("\"'") for t in match.group(1).split(",") if t.strip()}
48+
return tags, sql[:index].count("\n") + 1
49+
index += 1
50+
return set(), 1
51+
52+
53+
@rule(
54+
code="XSQBRDUCK002",
55+
message="Models in an edge_cases folder, and only those, must declare the edge_case tag",
56+
remediation=(
57+
"Add `tags [edge_case]` to the header of every model under models/<layer>/edge_cases/, "
58+
"and move a model that declares the tag into that folder, so benchmark and core runs can "
59+
"exclude the edge-case area with --exclude tag:edge_case."
60+
),
61+
)
62+
def edge_area_is_tagged(*, model: Model, ctx: RuleContext) -> list[Finding]:
63+
parts = ctx.project.tree.relative_parts(path=model.path, under="models")
64+
in_area = EDGE_FOLDER in parts
65+
tags, line = _header_tags(ctx.sql.for_model(model).authored.source)
66+
if in_area == (EDGE_TAG in tags):
67+
return []
68+
return [ctx.finding(subject=model, line=line)]

‎benchmarks/duckshop/project/sqlbuild_project.toml‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ user = "example_user"
1010
role = "EXAMPLE_ROLE"
1111
warehouse = "EXAMPLE_WH"
1212

13+
[targets.dev]
14+
connection = "warehouse"
15+
database = "DUCKSHOP"
16+
schema = "DEV"
17+
18+
[targets.prod]
19+
connection = "warehouse"
20+
database = "DUCKSHOP"
21+
schema = "ANALYTICS"
22+
23+
# >>> edge-case area (scripts/core_project.py leaves this block out of the core copy)
1324
# The edge-case area's expected values are captured on Snowflake under this pinned session: UTC,
1425
# with every parameter that changes a result or how text is parsed set to Snowflake's default.
15-
# Run the edge area on this connection only (see RUNBOOK.md, "Edge cases").
26+
# Run the edge area on this connection only (see RUNBOOK.md, step 7).
1627
[connections.edge]
1728
account = "example-account"
1829
user = "example_user"
@@ -43,20 +54,11 @@ TIMESTAMP_DAY_IS_ALWAYS_24H = false
4354
JSON_INDENT = 2
4455
STRICT_JSON_OUTPUT = false
4556

46-
[targets.dev]
47-
connection = "warehouse"
48-
database = "DUCKSHOP"
49-
schema = "DEV"
50-
5157
[targets.edge]
5258
connection = "edge"
5359
database = "DUCKSHOP"
5460
schema = "EDGE"
55-
56-
[targets.prod]
57-
connection = "warehouse"
58-
database = "DUCKSHOP"
59-
schema = "ANALYTICS"
61+
# <<< edge-case area
6062

6163
[settings]
6264
default_audit_severity = "error"
@@ -90,9 +92,11 @@ valuation_date = "2026-03-31"
9092
lookup_customer_id = "C000001"
9193
# Volume multiplier for the synthetic data loaders in python/loaders; 1 is the small default.
9294
data_scale = "1"
95+
# >>> edge-case area (scripts/core_project.py leaves this block out of the core copy)
9396
# Last load of the edge-case delivery change log to include; "1" replays the first load only (see
94-
# RUNBOOK.md, "Edge cases").
97+
# RUNBOOK.md, step 7).
9598
edge_delivery_batch = "2"
99+
# <<< edge-case area
96100

97101
[scenario.local_type_overrides.snowflake]
98102
"VARIANT" = "JSON"
@@ -273,6 +277,7 @@ The KPI grid is every reporting day for every region and metric pair that has ev
273277
days without activity report zero instead of disappearing. Both sides are small and bounded.
274278
"""
275279

280+
# >>> edge-case area (scripts/core_project.py leaves this block out of the core copy)
276281
[[rules.rule_ignores]]
277282
rules = ["SQBRSQL027"]
278283
paths = ["models/marts/edge_cases/rpt_edge_stock_reconciliation.sql"]
@@ -298,3 +303,4 @@ Unit-test and scenario fixtures for a source whose declared columns are quoted c
298303
rejected (B302 and S510), so this model is covered by building the edge-case area on Snowflake,
299304
where it reads the loaded quoted columns, instead of by a unit test.
300305
"""
306+
# <<< edge-case area

benchmarks/duckshop/project/tests/test_rule_area_tags.py renamed to benchmarks/duckshop/project/tests/test_edge_area_tags.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from rules.area_tags import edge_area_is_tagged
1+
from rules.edge_area_tags import edge_area_is_tagged
22

33
from sqlbuild.rules import RuleCase, evaluate_rule
44

@@ -64,3 +64,22 @@ def test_given_untagged_model_outside_edge_folder_when_evaluating_then_passes()
6464
)
6565

6666
assert result.finding_count == 0
67+
68+
69+
def test_given_tags_text_in_description_when_evaluating_then_reads_only_header_tags() -> None:
70+
result = evaluate_rule(
71+
rule=edge_area_is_tagged,
72+
test_case=RuleCase(
73+
description="a description holding ');' and tags text",
74+
source=(
75+
'MODEL (\n description """Orders (see note);\n tags [edge_case]""",\n'
76+
" columns (\n order_id (type VARCHAR),\n ),\n"
77+
" tags [orders],\n);\nSELECT 1 AS order_id\n"
78+
),
79+
path="models/staging/edge_cases/stg_edge_orders.sql",
80+
expected_finding_count=1,
81+
),
82+
)
83+
84+
assert result.finding_count == 1
85+
assert result.findings[0].line == 7

‎benchmarks/duckshop/project/tests/unit/intermediate/edge_cases/test_int_edge_payload_arrays.sql‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ __expected__int_edge_payload_arrays AS (
2626
PARSE_JSON('["SPRING",undefined]')::ARRAY AS codes,
2727
PARSE_JSON('["SPRING"]')::ARRAY AS codes_compact,
2828
PARSE_JSON('["bath","gift","bath","bath"]')::ARRAY AS tags_compact,
29-
PARSE_JSON('["gift","bath",null,"bath"]')::ARRAY AS tags_kept,
29+
PARSE_JSON('["bath","bath","gift",null]')::ARRAY AS tags_kept,
3030
PARSE_JSON('["bath","gift","bath",null,"bath","gift",null]')::ARRAY AS tags_all,
3131
PARSE_JSON('["bath","gift",null]')::ARRAY AS tags_distinct,
3232
TRUE AS shares_extra_tags

0 commit comments

Comments
 (0)