From dc8440971f6b7e79960e8c0099aced068d10d94c Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Wed, 2 Sep 2026 19:21:34 +0100 Subject: [PATCH] docs(parity): file P37/P38, restore skipped examples, triage the smoke suite Examples smoke failures were being cleaned up by deleting or editing the offending SQL. That erased the evidence: three statements removed last night were each demonstrating a real, open engine gap. Restore them behind the statement-level `-- [SKIP]` directive instead, so the queries stay in the tree as end-to-end acceptance tests without reddening the run. Restored and marked: - chemistry.sql OR in JOIN ... ON (P27, open) - jsonl_logs.sql ORDER BY ... NULLS LAST (P13 stage 2, open) - statistical_analysis.sql scalar subquery in a CTE (P38, new) Two new findings, both with corpus cases and DuckDB reference behaviour confirmed: P37 - a window function inline in WHERE returns ZERO rows, with a success exit code and no error. DuckDB rejects the query outright. Silent wrong answer, so it leads the open fix order. Shares a fix site with P15 (ExpressionLifter only walks the SELECT list), so the two are one piece of work. Found in expander_rewriters.sql, which advertises this as a working transformation. P38 - a scalar subquery in a CTE body's SELECT list is never evaluated. Narrower than it first appeared: two controls show the same expression works at the top level, and that plain arithmetic works inside a CTE. Two SELECT-list evaluation paths disagree on which expression types they support - check ENGINE_REFACTORING.md before adding the arm twice. Triage of the smoke suite: 13 failures -> 1. Twelve examples need a live server, post to placeholder hosts, or are nvim-plugin macro templates that contain no runnable SQL; all are marked with file-level `-- [TEST:SKIP]` plus the reason. expander_rewriters is deliberately left failing - it is P26, a real gap, and skipping it would bury it. Smoke failures are already non-blocking, so CI stays green while the bug stays visible. Parity contract holds at 172 cases (141 AGREE). All 33 formal example tests pass. No Rust changed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PpidyXocQt835yaJpKBSHk --- docs/SQL_PARITY.md | 86 ++++++++++++++++++++- examples/chemistry.sql | 32 ++++++++ examples/elasticsearch_app_events.sql | 1 + examples/fix_message_analysis.sql | 1 + examples/fix_messages_with_selectors.sql | 1 + examples/format.sql | 1 + examples/format_body.sql | 1 + examples/jsonl_logs.sql | 18 ++++- examples/macro_templates_guide.sql | 1 + examples/parameterized_trades.sql | 1 + examples/solar_system_with_cte_future.sql | 1 + examples/statistical_analysis.sql | 42 ++++++++++ examples/trade_queries_master.sql | 1 + examples/trade_reconciliation_final.sql | 1 + examples/web_cte_advanced.sql | 1 + examples/web_cte_file_upload.sql | 1 + tests/comparison/corpus/06_ctes_setops.toml | 27 +++++++ tests/comparison/corpus/09_window.toml | 12 +++ 18 files changed, 225 insertions(+), 4 deletions(-) diff --git a/docs/SQL_PARITY.md b/docs/SQL_PARITY.md index aef046d8..a7e7ab6e 100644 --- a/docs/SQL_PARITY.md +++ b/docs/SQL_PARITY.md @@ -76,15 +76,16 @@ Suggested fix order, by silent blast radius: | | Finding | Why first | |---|---|---| | ~~1~~ | ~~[P21](#p21) windows evaluated before `WHERE`~~ | ✅ **Fixed 2026-08-02** | -| ~~2~~ | ~~[P13](#p13) trailing tokens discarded~~ | ✅ **Stage 1 done 2026-08-02**; stage 2 (`NULLS FIRST`/`LAST`) is item 6 below | +| ~~2~~ | ~~[P13](#p13) trailing tokens discarded~~ | ✅ **Stage 1 done 2026-08-02**; stage 2 (`NULLS FIRST`/`LAST`) is in row 9b below | | ~~3~~ | ~~[P30](#p30) `cond AND col IN (list)` returns 0 rows~~ | ✅ **Fixed 2026-08-08** | | ~~5~~ | ~~[P29](#p29) boolean operator after `IN (...)`~~ | ✅ **Fixed 2026-08-08** — same bug as P30, one change closed both | | ~~4~~ | ~~[P28](#p28) `INTO #tmp` stages unfiltered rows~~ | ✅ **Fixed 2026-08-16** — turned out to stage the *whole source table*, and the sweep it prescribed found [P31](#p31) | | ~~7~~ | ~~[P18](#p18)/[P19](#p19) three-valued logic~~ | ✅ **Fixed 2026-08-22** — 125 → 129 AGREE. Delivered as three slices ([R10](ENGINE_REFACTORING.md#r10)); the two no-op ones landed first, so the semantics change reviewed on its own | | ~~8~~ | ~~[P24](#p24) `RANGE` treated as `ROWS`~~ | ✅ **Fixed 2026-08-30** — 129 → 133 AGREE (+2 fixed, +2 new coverage). One defect, not two: the parser already emitted the right default frame, so fixing peer groups closed both cases. Spun off [P33](#p33) | | ~~9a~~ | ~~[P16](#p16) `ORDER BY ` ignored~~ | ✅ **Fixed 2026-08-31** — 134 → 139 AGREE. The literal was being promoted into a hidden *constant* column, so the sort ran on a column where every row tied | +| **6** | [P37](#p37) window in `WHERE` returns 0 rows | **Silent — take this first.** Zero rows, success exit code, no error. Reads as "no matching data", not as a defect (the [P30](#p30) trap). Shares a fix site with [P15](#p15), so the two are one piece of work | | 9b | [P14](#p14), [P17](#p17), [P20](#p20), [P23](#p23), P13 stage 2 | Smaller, self-contained, decisions already taken | -| 10 | [P22](#p22), [P25](#p25), [P26](#p26), [P15](#p15), [P32](#p32) | Hard errors — visible, so less urgent than any of the above | +| 10 | [P22](#p22), [P25](#p25), [P26](#p26), [P15](#p15), [P32](#p32), [P38](#p38) | Hard errors — visible, so less urgent than any of the above | | 11 | [P35](#p35), [P36](#p36) | Not parity obligations — a DuckDB extension and a naming difference. Decide *whether*, not just when | | — | [P27](#p27) `OR` in `JOIN ... ON` | **Reclassified 2026-08-08 — not a quick win.** `JoinCondition` is a `Vec` implicitly AND-ed, so there is nowhere in the AST to put an `OR`; it needs join conditions to become an expression, which reaches the join execution code. Sequence it with the R-log, not here | @@ -485,6 +486,14 @@ annotation be removed. BOTH_ERR. That is the correct intermediate state: a hard error is strictly better than a silently different answer. Only stage 2 flips the NULLS cases to AGREE. +- **Acceptance test for stage 2:** `examples/jsonl_logs.sql` carries the + `ORDER BY latency_ms DESC NULLS LAST LIMIT 5` statement that motivated this, + marked `-- [SKIP]` (2026-09-02) so the examples smoke run stays green, next + to the NULLS-free version that does run. Drop the directive when stage 2 + lands. Note the JSONL fixture *does* contain NULLs inside the filtered set + (16 rows pass `status IS NOT NULL`, one of which has a NULL `latency_ms`), + so unlike the corpus it can pin real NULL-ordering semantics, not just the + lost `LIMIT`. - **Note on fixtures:** every corpus data file is NULL-free, so these cases pin the *lost LIMIT* only — the actual NULL ordering semantics remain untested. Tier 08 needs a fixture containing NULLs before that can be asserted either @@ -531,6 +540,10 @@ annotation be removed. - **Decision:** **Fix.** Join conditions should accept the same boolean expressions `WHERE` does; `AND` already works (multi-condition joins are well covered — see P7/P8), so this is `OR` specifically. +- **Acceptance test:** the `examples/chemistry.sql` statement that found this + is still in the file, marked `-- [SKIP]` (2026-09-02) so the examples smoke + run stays green. Drop the directive when P27 is fixed — it is the + end-to-end check, alongside the corpus case. ### P28 — `SELECT ... INTO #tmp` stages the *unfiltered* rows - **Status:** 🟢 FIXED (2026-08-16) @@ -1272,6 +1285,75 @@ out of date. finding a bug. Measure the churn first, then decide whether the naming is worth it. +### P37 — A window function inline in `WHERE` silently returns zero rows +- **Status:** 🔴 OPEN — **silent wrong answer** +- **Corpus:** `09_window.toml :: window_in_where_inline` (OURS_ONLY). +- **Observed:** `SELECT region, amount FROM international_sales WHERE + ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) <= 2` returns + **0 rows** — header printed, success exit code, no error. The table has 20 + rows. DuckDB refuses the query outright: *"Binder Error: WHERE clause cannot + contain window functions"*. +- **Why it is `OURS_ONLY` and not `GAP`:** we "succeed" where the reference + errors, so the harness scores it as an extension. It is not one — it is an + empty result standing in for an unimplemented feature, which is strictly worse + than the reference's hard error. The bucket is right; the behaviour is not. +- **Found:** 2026-09-02, while triaging examples smoke-test failures. + `examples/expander_rewriters.sql` advertises "expression lifter (window + function in WHERE)" as a working transformation. Half of that query fails + loudly ([P26](#p26)); this half fails silently, which is why it had never been + noticed. +- **Sibling of [P15](#p15) — same root cause, different symptom.** `ExpressionLifter` + only walks the **SELECT list**, so a window function written inline anywhere + else is never hoisted and arrives at `recursive_where_evaluator.rs` as a raw + `WindowFunction`. From QUALIFY that path errors (P15). From WHERE it evaluates + to something falsy for every row instead. One fix site — `expression_lifter` — + covers SELECT-list, QUALIFY and WHERE. +- **Decision:** **Fix.** Two acceptable end states, in order of preference: + 1. Lift it, so `WHERE ` works as `QUALIFY` does once P15 + lands — this is what `examples/expander_rewriters.sql` already claims and + it makes the two clauses consistent. + 2. Failing that, **error** like DuckDB. Either is acceptable; returning zero + rows is not. + Whichever is chosen, the silent-empty path must go first — it is the dangerous + half and it is independent of the feature decision. +- **Related:** [P26](#p26) is the other half of the same example statement, and + [P21](#p21) is the same "when are windows evaluated" confusion from a third angle. + +### P38 — A scalar subquery in a CTE's `SELECT` list is never evaluated +- **Status:** 🔴 OPEN +- **Corpus:** `06_ctes_setops.toml :: scalar_subquery_in_cte_select_list` (GAP). + Controls: `scalar_subquery_in_select_arithmetic`, + `plain_arithmetic_in_cte_select_list` (both AGREE). +- **Observed:** `WITH z AS (SELECT region, amount - (SELECT AVG(amount) FROM + international_sales) AS d FROM international_sales) SELECT * FROM z` → + *"Unsupported expression type for arithmetic evaluation: ScalarSubquery { … }"*. +- **Precisely located by the controls — narrower than it first looks.** The + first diagnosis was "scalar subqueries don't work in arithmetic". That is + wrong. Three probes: + + | Query shape | Result | + |---|---| + | Top-level `SELECT amount - (SELECT AVG(…)) AS d` | ✅ works | + | Top-level `(a - (SELECT …)) / (SELECT …)` | ✅ works | + | Same expression inside a **CTE body** | 🚫 error | + | `SELECT (SELECT AVG(…)) AS a` inside a CTE body (no arithmetic at all) | 🚫 error | + | `SELECT amount * 2` inside a CTE body | ✅ works | + + So neither scalar subqueries nor CTE arithmetic is broken in general. The CTE + SELECT-list path routes items through an evaluator whose expression match has + **no `ScalarSubquery` arm**, while the top-level path has one. The error text + says "arithmetic evaluation" only because that is the evaluator it lands in; + arithmetic is not the trigger. +- **Found:** 2026-09-02, in `examples/statistical_analysis.sql` — a z-score + block computing `(x - (SELECT AVG(x))) / (SELECT STDDEV(x))` inside a CTE. + The statement is restored in that file marked `-- [SKIP]`; drop the directive + when this is fixed and it becomes the end-to-end check. +- **Decision:** **Fix.** Two SELECT-list evaluation paths that disagree on which + expression types they support is a shape problem, not just a missing arm — + when picking this up, check against [`ENGINE_REFACTORING.md`](ENGINE_REFACTORING.md) + whether the honest fix is to converge the two paths rather than add the arm + twice. Adding the arm is the tactical fix if convergence is too large. + --- ## Deferred / won't fix (intentional) diff --git a/examples/chemistry.sql b/examples/chemistry.sql index dd50cf83..91cae82e 100644 --- a/examples/chemistry.sql +++ b/examples/chemistry.sql @@ -141,6 +141,38 @@ FROM periodic_table INNER JOIN year_analysis ON periodic_table.Year = year_analysis.max_discovery_year; GO +-- [SKIP] +-- Show discovery statistics by joining CTEs +-- SKIPPED: OR in a JOIN ... ON clause is not supported (parity issue P27 -- +-- see docs/SQL_PARITY.md). JoinCondition is a Vec of AND-ed conditions, so +-- there is nowhere in the AST to put an OR; this is a parse error today. +-- Restore this statement (and drop the [SKIP]) as the acceptance test when +-- P27 is fixed. +WITH discovery_stats AS ( + SELECT + Year, + COUNT(*) as elements_discovered + FROM periodic_table + WHERE Year IS NOT NULL + GROUP BY Year +), +year_bounds AS ( + SELECT + MAX(Year) as latest_year, + MIN(Year) as earliest_year + FROM periodic_table + WHERE Year IS NOT NULL +) +SELECT + Year, + elements_discovered, + latest_year, + earliest_year +FROM discovery_stats +INNER JOIN year_bounds ON Year = latest_year OR Year = earliest_year +ORDER BY Year; +GO + -- Self-join example: Find pairs of elements discovered in same year -- Note: This demonstrates a workaround since table aliases don't work diff --git a/examples/elasticsearch_app_events.sql b/examples/elasticsearch_app_events.sql index 1b767e42..be6f381b 100644 --- a/examples/elasticsearch_app_events.sql +++ b/examples/elasticsearch_app_events.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] needs a live Elasticsearch server -- Elasticsearch: broad filter at the server, heavy lifting in SQL -- -- Pattern: let Elasticsearch do what it's good at (time-window filtering, diff --git a/examples/fix_message_analysis.sql b/examples/fix_message_analysis.sql index 9731f0d5..f7102125 100644 --- a/examples/fix_message_analysis.sql +++ b/examples/fix_message_analysis.sql @@ -1,4 +1,5 @@ -- #! ../data/fix_messages.json +-- [TEST:SKIP] needs the message-query server on localhost:5050 (FORM_FILE upload) -- Example: Analyzing FIX messages using Web CTE with file upload -- This demonstrates how to upload a JSON file containing FIX messages to a selector service -- and query the flattened results diff --git a/examples/fix_messages_with_selectors.sql b/examples/fix_messages_with_selectors.sql index 8df6cbb9..243dc5c1 100644 --- a/examples/fix_messages_with_selectors.sql +++ b/examples/fix_messages_with_selectors.sql @@ -1,4 +1,5 @@ -- #! data/fix_messages.json +-- [TEST:SKIP] needs the message-query server on localhost:5050 -- Example: Querying FIX messages with custom selectors -- This shows how to specify the actual selector configuration in the SQL query diff --git a/examples/format.sql b/examples/format.sql index 88442444..3e382a1d 100644 --- a/examples/format.sql +++ b/examples/format.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] nvim formatter fixture (deliberately unformatted); also needs live endpoints -- Example: Testing SQL formatter - UNFORMATTED VERSION -- Test these cases in nvim to verify formatting works correctly: -- 1. JSON pretty-printing with $JSON$ delimiters diff --git a/examples/format_body.sql b/examples/format_body.sql index 25091b04..cd258ea6 100644 --- a/examples/format_body.sql +++ b/examples/format_body.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] nvim formatter fixture; posts to the placeholder host tradesapi -- Example: Testing SQL formatter with WEB CTE using BODY with JSON WITH WEB trades AS ( diff --git a/examples/jsonl_logs.sql b/examples/jsonl_logs.sql index af644cab..20b857e9 100644 --- a/examples/jsonl_logs.sql +++ b/examples/jsonl_logs.sql @@ -17,7 +17,7 @@ -- How many events, and what services / levels are in play? SELECT COUNT(*) AS total_events, - COUNT(DISTINCT service) AS distinct_services, + COUNT(DISTINCT service) AS distinct_services, COUNT(DISTINCT level) AS distinct_levels FROM READ_JSONL('data/app_logs.jsonl'); GO @@ -69,7 +69,21 @@ GO SELECT method, path, status, latency_ms FROM READ_JSONL('data/app_logs.jsonl') WHERE status IS NOT NULL -ORDER BY latency_ms DESC +ORDER BY latency_ms DESC +LIMIT 5; +GO + +-- [SKIP] +-- The same query as originally written, with an explicit null-ordering +-- clause. SKIPPED: NULLS FIRST / NULLS LAST is not parsed -- parity issue +-- P13 stage 2 (see docs/SQL_PARITY.md). There is no NULLS handling anywhere +-- in src/sql/. Before P13 stage 1 the clause was silently swallowed along +-- with the LIMIT below it, so this returned every row instead of 5; it is a +-- parse error today. Drop the [SKIP] when stage 2 lands. +SELECT method, path, status, latency_ms +FROM READ_JSONL('data/app_logs.jsonl') +WHERE status IS NOT NULL +ORDER BY latency_ms DESC NULLS LAST LIMIT 5; GO diff --git a/examples/macro_templates_guide.sql b/examples/macro_templates_guide.sql index ba456d90..6c93cc4c 100644 --- a/examples/macro_templates_guide.sql +++ b/examples/macro_templates_guide.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] nvim plugin macro guide -- prose only, contains no runnable statement -- ============================================ -- SQL-CLI Macro Templates Guide -- ============================================ diff --git a/examples/parameterized_trades.sql b/examples/parameterized_trades.sql index b60ce475..e8360177 100644 --- a/examples/parameterized_trades.sql +++ b/examples/parameterized_trades.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] illustrative -- {{PARAM}} templates resolved by the nvim plugin, placeholder host -- Example of parameterized trade queries -- Use {{PARAM_NAME}} to define parameters that will be resolved at execution time diff --git a/examples/solar_system_with_cte_future.sql b/examples/solar_system_with_cte_future.sql index 0653227a..781d3a57 100644 --- a/examples/solar_system_with_cte_future.sql +++ b/examples/solar_system_with_cte_future.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] aspirational design sketch, not bound to a data file -- Solar System Queries with CTEs (Future Capability) -- This file shows what will be possible once CTEs are implemented -- It demonstrates how CTEs solve current limitations with window functions and ORDER BY diff --git a/examples/statistical_analysis.sql b/examples/statistical_analysis.sql index 5f9dd373..bb7f38ce 100644 --- a/examples/statistical_analysis.sql +++ b/examples/statistical_analysis.sql @@ -107,6 +107,48 @@ ORDER BY occurrences DESC LIMIT 1; GO +-- [SKIP] +-- Statistical summary for outlier detection +-- Values more than 2 standard deviations from mean might be outliers +-- SKIPPED: a scalar subquery inside a CTE body's SELECT list is never evaluated +-- -- "Unsupported expression type for arithmetic evaluation: ScalarSubquery". +-- Parity issue P38 (see docs/SQL_PARITY.md). The same expression works at the +-- top level; it is the CTE SELECT-list path that lacks the ScalarSubquery arm. +-- Drop the [SKIP] when P38 is fixed. +WITH sales_with_zscore AS ( + SELECT + product, + region, + sales_amount, + (sales_amount - (SELECT AVG(sales_amount) FROM sales_data)) / + (SELECT STDDEV(sales_amount) FROM sales_data) as z_score_raw + FROM sales_data +), +with_classification AS ( + SELECT + product, + region, + sales_amount, + ROUND(z_score_raw, 2) as z_score, + ABS(z_score_raw) as abs_z_score, + CASE + WHEN ABS(z_score_raw) > 2 THEN 'Potential Outlier' + WHEN ABS(z_score_raw) > 1 THEN 'Above/Below Average' + ELSE 'Normal Range' + END as classification + FROM sales_with_zscore +) +SELECT + product, + region, + sales_amount, + z_score, + classification +FROM with_classification +ORDER BY abs_z_score DESC +LIMIT 10; +GO + -- Coefficient of Variation (CV) - relative variability measure -- CV = (Standard Deviation / Mean) * 100 diff --git a/examples/trade_queries_master.sql b/examples/trade_queries_master.sql index e452ec10..fbe06ac4 100644 --- a/examples/trade_queries_master.sql +++ b/examples/trade_queries_master.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] nvim plugin macro template (expand with \sTe); needs the test server on localhost:5001 -- ============================================ -- SQL-CLI Trade Queries Master Template -- ============================================ diff --git a/examples/trade_reconciliation_final.sql b/examples/trade_reconciliation_final.sql index 20f233fa..1fadf21e 100644 --- a/examples/trade_reconciliation_final.sql +++ b/examples/trade_reconciliation_final.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] template -- expects the user's own trades file (see the Run: line above) -- ============================================================================ -- Trade Reconciliation - Final Version with Dynamic Root Order Extraction -- Uses IndexOf('_') to find underscore and extract root order ID diff --git a/examples/web_cte_advanced.sql b/examples/web_cte_advanced.sql index 47bf3fc6..35e4e2c5 100644 --- a/examples/web_cte_advanced.sql +++ b/examples/web_cte_advanced.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] illustrative -- posts to the placeholder host api.example.com -- Example: Advanced WEB CTE features including POST requests, JSON path extraction, and authentication -- This demonstrates the new capabilities added for REST API integration diff --git a/examples/web_cte_file_upload.sql b/examples/web_cte_file_upload.sql index ccd6fba3..0370eb8b 100644 --- a/examples/web_cte_file_upload.sql +++ b/examples/web_cte_file_upload.sql @@ -1,3 +1,4 @@ +-- [TEST:SKIP] needs a web service to accept the FORM_FILE upload -- Example: Using Web CTE with file uploads -- This demonstrates the FORM_FILE syntax for uploading files to web services diff --git a/tests/comparison/corpus/06_ctes_setops.toml b/tests/comparison/corpus/06_ctes_setops.toml index a6da8ebe..ceab4a5a 100644 --- a/tests/comparison/corpus/06_ctes_setops.toml +++ b/tests/comparison/corpus/06_ctes_setops.toml @@ -57,3 +57,30 @@ sql = "SELECT symbol, price FROM trades WHERE price > (WITH avg_cte AS (SELECT A # primary expression: With"). Fixed 2026-07-25 — the subquery-detection guards # now accept a leading WITH (parse_subquery already dispatched it to the CTE # parser). DuckDB allows a CTE inside a scalar subquery; now AGREE. + +[[case]] +id = "scalar_subquery_in_cte_select_list" +data = "international_sales.csv" +sql = "WITH z AS (SELECT region, amount - (SELECT AVG(amount) FROM international_sales) AS d FROM international_sales) SELECT region, d FROM z ORDER BY region, d" +expect = "GAP" +# P38, added 2026-09-02. "Unsupported expression type for arithmetic +# evaluation: ScalarSubquery { ... }". A scalar subquery in a CTE body's SELECT +# list is never evaluated. Located by the two controls below: the same +# expression at the top level AGREEs, and plain arithmetic inside a CTE AGREEs +# -- so this is the CTE SELECT-list path specifically, not scalar subqueries +# and not CTEs. + +[[case]] +id = "scalar_subquery_in_select_arithmetic" +data = "international_sales.csv" +sql = "SELECT region, amount - (SELECT AVG(amount) FROM international_sales) AS d FROM international_sales ORDER BY region, d" +# Control for P38: identical expression at the TOP level. AGREEs, which is what +# puts the P38 fix site in the CTE SELECT-list path rather than in the +# arithmetic evaluator's handling of ScalarSubquery in general. + +[[case]] +id = "plain_arithmetic_in_cte_select_list" +data = "international_sales.csv" +sql = "WITH z AS (SELECT region, amount * 2 AS d FROM international_sales) SELECT region, d FROM z ORDER BY region, d" +# Control for P38: arithmetic inside a CTE with no subquery. AGREEs, so CTE +# SELECT-list arithmetic is not broken in general -- only the ScalarSubquery arm. diff --git a/tests/comparison/corpus/09_window.toml b/tests/comparison/corpus/09_window.toml index 1aa5c271..08198144 100644 --- a/tests/comparison/corpus/09_window.toml +++ b/tests/comparison/corpus/09_window.toml @@ -285,3 +285,15 @@ expect = "GAP" # column. But the lifter only walks the SELECT list, so a window function # written inline in QUALIFY is never lifted and arrives at the WHERE evaluator # as a raw WindowFunction. Fix site is `expression_lifter`, not QUALIFY. + +[[case]] +id = "window_in_where_inline" +data = "international_sales.csv" +sql = "SELECT region, amount FROM international_sales WHERE ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) <= 2 ORDER BY region, amount" +expect = "OURS_ONLY" +# P37, added 2026-09-02. DuckDB rejects this outright ("WHERE clause cannot +# contain window functions"). We do not error -- we return **zero rows**, with +# a header and a success exit code. Silent empty result, the same class as +# P30/P31. Sibling of P15: the ExpressionLifter only walks the SELECT list, so +# an inline window reaches the WHERE evaluator unlifted; QUALIFY errors there +# (P15) but WHERE quietly filters everything out instead.