Skip to content

Commit c3a2295

Browse files
authored
feat: support quoted mixed-case columns and read SQL with adapter lexical rules (#1270)
- Every compile-time scanner (declaration references, macro calls, SQL macros, `@@` interpolation) reads strings and comments with the adapter's lexical rules, including E-strings; `@py` arguments use Python string rules. - Quoted mixed-case column names such as `"listingId"` work in MODEL headers, fixtures, contracts, Snowflake planning and builds, and runtime contract, type-enforcement, incremental and diff checks match them as the compiler does. - DuckDB existence checks follow `SET search_path`, case-insensitively. - A build reuses the planner's Snowflake schema listings for up to a minute. - **Behaviour change:** a `rule_ignores` path without glob characters that does not exist now fails compilation, as `rule_exceptions` already did. - `sqb query` shows transient progress only for slow steps; the Snowflake adapter turns off connector platform detection unless the user set `SNOWFLAKE_DISABLE_PLATFORM_DETECTION`; the import hook skips installed packages. - Pins `polyglot-sql-chio==0.13.10`, makes three DuckShop core models deterministic, and moves repository links to the sqlstack-io organization.
1 parent f448e6b commit c3a2295

155 files changed

Lines changed: 2852 additions & 412 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ junction = { version = "2.1.0", default-features = false }
7272
mimalloc = { version = "0.1", default-features = false, features = ["local_dynamic_tls"] }
7373
proc-macro2 = { version = "1", features = ["span-locations"] }
7474
pyo3 = { version = "0.26", features = ["abi3-py312"] }
75-
polyglot-sql = { git = "https://github.com/chio-labs/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
75+
polyglot-sql = { git = "https://github.com/sqlstack-io/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
7676
"ast-tools",
7777
"builder",
7878
"all-dialects",
7979
"semantic",
8080
"stacker",
8181
] }
82-
polyglot-sql-function-catalogs = { git = "https://github.com/chio-labs/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
82+
polyglot-sql-function-catalogs = { git = "https://github.com/sqlstack-io/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
8383
"dialect-duckdb",
8484
] }
8585
blake3 = { version = "1", features = ["rayon"] }

‎Makefile‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ E2E_DUCKDB_BUILD_CORE_PATHS := \
116116
tests/e2e/src/sqlbuild/cli/commands/main/build/test_python_check_outcomes.py \
117117
tests/e2e/src/sqlbuild/cli/commands/main/build/test_duckdb_attach.py \
118118
tests/e2e/src/sqlbuild/cli/commands/main/build/test_unbuilt_dependencies.py \
119-
tests/e2e/src/sqlbuild/cli/commands/main/build/test_validation_failures.py
119+
tests/e2e/src/sqlbuild/cli/commands/main/build/test_validation_failures.py \
120+
tests/e2e/src/sqlbuild/cli/commands/main/build/test_quoted_mixed_case_columns.py
120121

121122
E2E_DUCKDB_BUILD_INCREMENTAL_PATHS := \
122123
tests/e2e/src/sqlbuild/cli/commands/main/build/own_change_replay \
@@ -156,6 +157,7 @@ E2E_DUCKDB_CLI_DATA_PATHS := \
156157
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_fixture_columns.py \
157158
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_local_variant.py \
158159
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_local_lambda.py \
160+
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_local_escaped_quotes.py \
159161
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_snapshot_staleness.py \
160162
tests/e2e/src/sqlbuild/cli/commands/main/scenario/test_unmocked_inputs.py
161163

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="https://raw.githubusercontent.com/chio-labs/sqlbuild/main/.github/sqlbuild-logo-clean.png" alt="SQLBuild" width="100%">
2+
<img src="https://raw.githubusercontent.com/sqlstack-io/sqlbuild/main/.github/sqlbuild-logo-clean.png" alt="SQLBuild" width="100%">
33
</p>
44

55
<p align="center">
@@ -21,7 +21,7 @@ no external state database, no manifest files and no paid tier.
2121
Its compiler and planner run in Rust. Warehouse execution and extensions use Python.
2222

2323
<p align="center">
24-
<img src="https://raw.githubusercontent.com/chio-labs/sqlbuild/main/.github/demos/rename.gif" alt="Moving and renaming an incremental model with sqb mv: sqb plan migrates the existing table instead of rebuilding it" width="100%">
24+
<img src="https://raw.githubusercontent.com/sqlstack-io/sqlbuild/main/.github/demos/rename.gif" alt="Moving and renaming an incremental model with sqb mv: sqb plan migrates the existing table instead of rebuilding it" width="100%">
2525
</p>
2626

2727
Move an incremental model to a new folder and name with `sqb mv`, and `sqb plan` migrates its table
@@ -100,7 +100,7 @@ running on local DuckDB.
100100
`SELECT` fails at compile time, before anything reaches the warehouse.
101101

102102
<p align="center">
103-
<img src="https://raw.githubusercontent.com/chio-labs/sqlbuild/main/.github/demos/contract.gif" alt="sqb compile fails because units_sold is not in the enforced contract and the declared waffles_sold column is missing" width="100%">
103+
<img src="https://raw.githubusercontent.com/sqlstack-io/sqlbuild/main/.github/demos/contract.gif" alt="sqb compile fails because units_sold is not in the enforced contract and the declared waffles_sold column is missing" width="100%">
104104
</p>
105105

106106
Both sides are reported: the new column isn't in the contract, and the declared one is gone.
@@ -133,7 +133,7 @@ A mart that reads `__source("raw__payments")` directly now fails, and so does th
133133
has no mock for it:
134134

135135
<p align="center">
136-
<img src="https://raw.githubusercontent.com/chio-labs/sqlbuild/main/.github/demos/rules.gif" alt="sqb compile reports the custom rule XSQBRARCH001 on the line that reads a raw source, and a unit test with no mock for that source" width="100%">
136+
<img src="https://raw.githubusercontent.com/sqlstack-io/sqlbuild/main/.github/demos/rules.gif" alt="sqb compile reports the custom rule XSQBRARCH001 on the line that reads a raw source, and a unit test with no mock for that source" width="100%">
137137
</p>
138138

139139
See [rules](https://sqlbuild.com/docs/concepts/rules/).
@@ -143,7 +143,7 @@ See [rules](https://sqlbuild.com/docs/concepts/rules/).
143143
`sqb scope --as-path` previews moving a model before you move it.
144144

145145
<p align="center">
146-
<img src="https://raw.githubusercontent.com/chio-labs/sqlbuild/main/.github/demos/scope.gif" alt="sqb scope previews moving daily_revenue: the enum and macro it uses would be lost, so both usages are invalidated" width="100%">
146+
<img src="https://raw.githubusercontent.com/sqlstack-io/sqlbuild/main/.github/demos/scope.gif" alt="sqb scope previews moving daily_revenue: the enum and macro it uses would be lost, so both usages are invalidated" width="100%">
147147
</p>
148148

149149
Look at `Lost` and `Invalidated usages`: the model uses an enum and a macro that are private to

‎benchmarks/duckshop/README.md‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ project and does not change it:
119119
- raw `edge__*` sources in `sources/edge_cases.yml`, loaded by `python/loaders/edge_cases.py` from
120120
fixed rows that `data_scale` does not change, so core data and core expectations stay the same.
121121

122-
It has 38 models (7 staging, 22 intermediate, 9 marts), 61 unit tests and 4 scenarios, in four
122+
It has 38 models (7 staging, 22 intermediate, 9 marts), 62 unit tests and 4 scenarios, in four
123123
groups:
124124

125125
| Group | Covers |
@@ -145,6 +145,14 @@ Benchmarks and core measurements leave the area out:
145145
with `--exclude edge_cases`;
146146
- the construct census counts the core project unless given `--area edge` or `--area all`.
147147

148+
Changes to the core project's compiled SQL, after which benchmark results are not comparable with
149+
earlier ones:
150+
151+
- `int_cart_funnel`, `int_delivery_slot_utilisation` and `rpt_delivery_exceptions_monthly` give the
152+
same results on every run: added SKUs are sorted, the weekday average utilisation is an exact
153+
decimal sum over a count, the busiest slot of equally busy ones is the earliest, and the top
154+
exception code of equally frequent ones is the first in alphabetical order.
155+
148156
## Status
149157

150158
Last verified on Snowflake with SQLBuild 0.175.0 at `data_scale = "1"`:
@@ -156,7 +164,7 @@ Last verified on Snowflake with SQLBuild 0.175.0 at `data_scale = "1"`:
156164
| `sqb test` | 266 of 266 pass |
157165
| `sqb scenario test` | 8 of 8 pass |
158166
| `sqb scenario capture` | Captures all 8; snapshots are in `project/tests/_scenario_snapshots/` |
159-
| `sqb scenario test --local --strict` | In the core copy, 0 of 8 pass: 5 fail in translation, 3 report a stale snapshot |
167+
| `sqb scenario test --local --strict --exclude edge_cases` | 0 of 8 pass: 5 fail in translation, 3 report a stale snapshot |
160168

161169
Local replay translates each scenario's Snowflake SQL to DuckDB on a best-effort basis. These five
162170
failures are the baseline that better translation should remove:
@@ -181,12 +189,12 @@ the same rows, apart from the snapshots, which record build times, and three mod
181189
vary from one run to the next (`int_cart_funnel`, `int_delivery_slot_utilisation` and
182190
`rpt_delivery_exceptions_monthly`).
183191

184-
The edge-case area, verified on Snowflake with SQLBuild 0.184.0 on the `edge` target:
192+
The edge-case area, verified on Snowflake with SQLBuild built after 0.185.0 on the `edge` target:
185193

186194
| Step | Result |
187195
|------|--------|
188-
| `sqb build --select tag:edge_case --exclude stg_edge_partner_listings+` | Passes; the disabled model is skipped |
189-
| `sqb test --select tag:edge_case` | 61 of 61 pass |
196+
| `sqb build --select tag:edge_case` | Passes; the disabled model is skipped |
197+
| `sqb test --select tag:edge_case` | 62 of 62 pass |
190198
| `sqb scenario test --select edge_cases` | 4 of 4 pass |
191199
| Two-load replay of the delivery change log | The merged status equals the full rebuild after each load |
192200

@@ -213,15 +221,10 @@ noted here:
213221
- Compilation accepts SQL that Snowflake rejects: `CONNECT_BY_ISLEAF`, which Snowflake does not
214222
have, and a CTE whose body is a bare `VALUES` list. `int_category_paths` finds leaf categories
215223
with a join instead, and test case tables use `SELECT ... FROM VALUES ... AS v(...)`.
216-
- A source column whose name is quoted mixed case, such as `"listingId"`, cannot be read once the
217-
table exists: planning folds the quoted reference to lower case and fails with `B002`. Fixtures
218-
cannot name such columns either (`B302` in unit tests, `S510` in scenarios), and a `MODEL`
219-
header cannot declare a quoted column name. `stg_edge_partner_listings` has a reviewed
220-
`SQBRTEST202` ignore, `rpt_edge_partner_listing_export` a reviewed `SQBRTEST201` ignore, and the
221-
edge build excludes `stg_edge_partner_listings+`.
222-
- Local scenario replay stops at compile (`P001`, unclosed quoted string) on a model with a
223-
backslash-escaped quote in a string literal, `'O\'Brien'` in `stg_edge_ticket_notes`, so local
224-
replay runs in the core copy.
224+
- On Snowflake, a model that names a column in quoted mixed case, such as `"unitPrice"`, loses the
225+
static types of the columns it reads from a CTE through a table alias (`FROM listings AS l`), so
226+
`rpt_edge_partner_listing_export` declares only the name and audits of `"listingId"`, without
227+
types.
225228
- `SQBRSQL018` can only take proof from single-column keys. Append-only feeds redeliver rows, so
226229
their id is unique only together with `_loaded_at` (the `unique_per_delivery` audit), and the
227230
staging models that keep the latest delivery carry reviewed exceptions.

‎benchmarks/duckshop/RUNBOOK.md‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,13 @@ on Snowflake. Report any failure rather than editing the expectations to match.
133133

134134
```bash
135135
sqb scenario capture --exclude edge_cases
136-
python ../scripts/core_project.py /tmp/duckshop-core
137-
(cd /tmp/duckshop-core && sqb scenario test --local --strict)
136+
sqb scenario test --local --strict --exclude edge_cases
138137
```
139138

140139
Capture materialises each scenario's fixtures in Snowflake, downloads them as JSONL into
141140
`tests/_scenario_snapshots/`, and drops what it created. The local run then replays all 8
142141
scenarios on your machine from those files, with no warehouse connection; `--strict` fails on a
143-
missing or stale snapshot instead of skipping it. It runs in a copy of the project without the
144-
edge-case area, because local replay of the full project stops at compile on an edge-case model
145-
(see Known limitations in the README). Delete the copy afterwards.
142+
missing or stale snapshot instead of skipping it.
146143

147144
With SQLBuild 0.175.0 the local run is expected to end with `ERROR=8`: five scenarios fail while
148145
translating Snowflake SQL to DuckDB, and three report `X602` (stale snapshot) because of a
@@ -175,18 +172,14 @@ collation). Check with `SHOW PARAMETERS LIKE 'DEFAULT_DDL_COLLATION' IN SCHEMA <
175172
### Load, build and test
176173

177174
```bash
178-
sqb build --target edge --select tag:edge_case --exclude stg_edge_partner_listings+ --concurrency 8
175+
sqb build --target edge --select tag:edge_case --concurrency 8
179176
sqb test --target edge --select tag:edge_case --concurrency 8
180177
sqb scenario test --target edge --select edge_cases
181178
```
182179

183180
The build loads the `edge__*` sources from fixed rows that `data_scale` does not change, then
184181
builds the edge models and runs their tests and audits. `fct_edge_delivery_events` is disabled and
185182
reports `skip: disabled` (see [Pending acceptance cases](#pending-acceptance-cases)).
186-
`stg_edge_partner_listings` and its downstream models are excluded because planning fails once the
187-
`edge__partner_listings` table exists (see Known limitations in the README); the
188-
`edge_partner_listings__star_contract_and_quoted_export` scenario covers the models downstream of
189-
it.
190183

191184
### Replay the delivery change log as two loads
192185

‎benchmarks/duckshop/project/models/intermediate/clickstream/int_cart_funnel.sql‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
MODEL (
22
description """
33
One row per web session with its cart activity worked out from event item arrays using FILTER
4-
and TRANSFORM lambdas: items added, removed, net units, and the checkout steps reached.
4+
and TRANSFORM lambdas: items added (distinct SKUs in sorted order), removed, net units, and the
5+
checkout steps reached.
56
""",
67
columns (
78
session_id (type VARCHAR, nullable false, audits [not_null, unique]),
@@ -91,10 +92,13 @@ final AS (
9192
s.session_id,
9293
s.anonymous_id,
9394
s.session_start_utc,
94-
ARRAY_DISTINCT(TRANSFORM(
95-
ARRAY_FLATTEN(COALESCE(sc.added_item_lists, ARRAY_CONSTRUCT())),
96-
i -> UPPER(i:sku::VARCHAR)
97-
))::ARRAY AS added_skus,
95+
-- ARRAY_AGG and ARRAY_DISTINCT leave element order unspecified; sort for a stable result.
96+
ARRAY_SORT(
97+
ARRAY_DISTINCT(TRANSFORM(
98+
ARRAY_FLATTEN(COALESCE(sc.added_item_lists, ARRAY_CONSTRUCT())),
99+
i -> UPPER(i:sku::VARCHAR)
100+
))
101+
)::ARRAY AS added_skus,
98102
COALESCE(sc.removed_sku_count, 0)::INT AS removed_sku_count,
99103
COALESCE(sc.net_units, 0)::INT AS net_units,
100104
sc.max_added_unit_minor::INT AS max_added_unit_minor,

‎benchmarks/duckshop/project/models/intermediate/fulfilment/int_delivery_slot_utilisation.sql‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,29 @@ warehouse_regions AS (
4949
ON w.country_code = c.country_code
5050
),
5151

52+
busiest_slots AS (
53+
SELECT
54+
s.warehouse_code,
55+
s.slot_start_local::DATE AS slot_date_local,
56+
s.slot_start_local AS busiest_slot_start_local
57+
FROM slots AS s
58+
WHERE s.capacity <> 0
59+
-- The earliest of equally busy slots, so ties give one answer.
60+
QUALIFY
61+
ROW_NUMBER() OVER (
62+
PARTITION BY s.warehouse_code, s.slot_start_local::DATE
63+
ORDER BY s.booked / s.capacity DESC, s.slot_start_local, s.slot_id
64+
) = 1
65+
),
66+
5267
by_day AS (
5368
SELECT
5469
s.warehouse_code,
5570
s.slot_start_local::DATE AS slot_date_local,
5671
COUNT(*) AS slot_count,
5772
SUM(s.capacity) AS total_capacity,
5873
SUM(s.booked) AS total_booked,
59-
COUNT_IF(s.remaining_capacity = 0) AS full_slot_count,
60-
MAX_BY(s.slot_start_local, s.booked / NULLIF(s.capacity, 0)) AS busiest_slot_start_local
74+
COUNT_IF(s.remaining_capacity = 0) AS full_slot_count
6175
FROM slots AS s
6276
GROUP BY
6377
s.warehouse_code,
@@ -72,13 +86,16 @@ with_calendar AS (
7286
d.total_capacity,
7387
d.total_booked,
7488
d.full_slot_count,
75-
d.busiest_slot_start_local,
89+
b.busiest_slot_start_local,
7690
r.region_code,
7791
(
7892
h.holiday_date IS NOT NULL
7993
) AS is_holiday,
8094
d.total_booked / NULLIF(d.total_capacity, 0) AS utilisation_raw
8195
FROM by_day AS d
96+
LEFT JOIN busiest_slots AS b
97+
ON d.warehouse_code = b.warehouse_code
98+
AND d.slot_date_local = b.slot_date_local
8299
LEFT JOIN warehouse_regions AS r
83100
ON d.warehouse_code = r.warehouse_code
84101
LEFT JOIN holidays AS h
@@ -108,9 +125,13 @@ final AS (
108125
@const("unknown_region_code")
109126
)::VARCHAR AS region_code,
110127
d.is_holiday::BOOLEAN AS is_holiday,
128+
-- The average as an exact decimal sum over a count, so rounding gives one answer.
111129
ROUND(
112130
d.utilisation_raw
113-
- AVG(d.utilisation_raw) OVER (
131+
- SUM(d.utilisation_raw) OVER (
132+
PARTITION BY d.warehouse_code, DAYOFWEEKISO(d.slot_date_local)
133+
)
134+
/ COUNT(d.utilisation_raw) OVER (
114135
PARTITION BY d.warehouse_code, DAYOFWEEKISO(d.slot_date_local)
115136
),
116137
4

‎benchmarks/duckshop/project/models/marts/edge_cases/rpt_edge_partner_listing_export.sql‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ MODEL (
44
and prices in major units
55
""",
66
tags [edge_case],
7+
columns (
8+
"listingId" (nullable false, audits [not_null, unique]),
9+
),
710
);
811

912
WITH listings AS (

0 commit comments

Comments
 (0)