From 00a9e112b1e8b0a5dba174e9cb4e33c18b251e43 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 04:28:05 +0000 Subject: [PATCH 01/10] cases: record POSITION and CHARINDEX with an empty needle and a start position --- .../position.empty-needle-start.toml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 cases/translation/functions/position.empty-needle-start.toml diff --git a/cases/translation/functions/position.empty-needle-start.toml b/cases/translation/functions/position.empty-needle-start.toml new file mode 100644 index 000000000..07a8df8dd --- /dev/null +++ b/cases/translation/functions/position.empty-needle-start.toml @@ -0,0 +1,41 @@ +category = "translation" +construct = "sf.functions.position.position" +session_profile = "utc-v1" + +[[case]] +id = "t001_empty_needle_start_pos" +description = "An empty needle with a start position, inside, at and past the end of the haystack, with an empty haystack, and with zero and negative starts. Records what POSITION returns for each start." +columns = [{ name = "a", type = "VARCHAR" }, { name = "b", type = "VARCHAR" }, { name = "p", type = "NUMBER(38,0)" }] +rows = [ + ["''", "'abc'", "1"], + ["''", "'abc'", "2"], + ["''", "'abc'", "3"], + ["''", "'abc'", "4"], + ["''", "'abc'", "5"], + ["''", "'abc'", "10"], + ["''", "''", "2"], + ["''", "'abc'", "0"], + ["''", "'abc'", "-1"], +] +query = "SELECT POSITION(a, b, p) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary", "empty"] + +[[case]] +id = "t002_charindex_empty_needle_start_pos" +description = "The same rows through CHARINDEX(a, b, p)." +columns = [{ name = "a", type = "VARCHAR" }, { name = "b", type = "VARCHAR" }, { name = "p", type = "NUMBER(38,0)" }] +rows = [ + ["''", "'abc'", "1"], + ["''", "'abc'", "2"], + ["''", "'abc'", "3"], + ["''", "'abc'", "4"], + ["''", "'abc'", "5"], + ["''", "'abc'", "10"], + ["''", "''", "2"], + ["''", "'abc'", "0"], + ["''", "'abc'", "-1"], +] +query = "SELECT CHARINDEX(a, b, p) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary", "empty"] From 5476cd2e97111c4bde6660d13a9d8f66e3edc4ce Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 04:34:15 +0000 Subject: [PATCH 02/10] cases: record REGEXP_SUBSTR with position, occurrence and group outside the recorded range --- .../regexp-substr.column-arguments.toml | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 cases/translation/functions/regexp-substr.column-arguments.toml diff --git a/cases/translation/functions/regexp-substr.column-arguments.toml b/cases/translation/functions/regexp-substr.column-arguments.toml new file mode 100644 index 000000000..52ac923d3 --- /dev/null +++ b/cases/translation/functions/regexp-substr.column-arguments.toml @@ -0,0 +1,93 @@ +category = "translation" +construct = "sf.functions.regexp-substr" +session_profile = "utc-v1" + +[[case]] +id = "t001_position_zero" +description = "REGEXP_SUBSTR with position 0 and occurrence 1 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "0", "1"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t002_position_negative" +description = "REGEXP_SUBSTR with position -1 and occurrence 1 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "-1", "1"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t003_position_past_end" +description = "REGEXP_SUBSTR with position 9 and occurrence 1 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "9", "1"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t004_position_far_past_end" +description = "REGEXP_SUBSTR with position 20 and occurrence 1 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "20", "1"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t005_occurrence_zero" +description = "REGEXP_SUBSTR with position 1 and occurrence 0 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "1", "0"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t006_occurrence_negative" +description = "REGEXP_SUBSTR with position 1 and occurrence -1 taken from columns, outside the recorded range. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }, { name = "o", type = "NUMBER(38,0)" }] +rows = [["'a1b2c3d4'", "1", "-1"]] +query = "SELECT REGEXP_SUBSTR(a, '[0-9]', n, o) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t007_group_zero" +description = "REGEXP_SUBSTR with the 'e' parameter and group 0 taken from a column, outside the pattern's two groups. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "g", type = "NUMBER(38,0)" }] +rows = [["'k1=v22 k2=v3'", "0"]] +query = "SELECT REGEXP_SUBSTR(a, 'k([0-9])=v([0-9]+)', 1, 1, 'e', g) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t008_group_past_last" +description = "REGEXP_SUBSTR with the 'e' parameter and group 3 taken from a column, outside the pattern's two groups. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "g", type = "NUMBER(38,0)" }] +rows = [["'k1=v22 k2=v3'", "3"]] +query = "SELECT REGEXP_SUBSTR(a, 'k([0-9])=v([0-9]+)', 1, 1, 'e', g) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t009_group_negative" +description = "REGEXP_SUBSTR with the 'e' parameter and group -1 taken from a column, outside the pattern's two groups. One row per case, since it may raise." +columns = [{ name = "a", type = "VARCHAR" }, { name = "g", type = "NUMBER(38,0)" }] +rows = [["'k1=v22 k2=v3'", "-1"]] +query = "SELECT REGEXP_SUBSTR(a, 'k([0-9])=v([0-9]+)', 1, 1, 'e', g) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] + +[[case]] +id = "t010_anchor_after_start_position" +description = "A ^ anchor with a start position of 2: shows whether ^ anchors at the start position or at the start of the text." +columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }] +rows = [["'a1b2'", "2"]] +query = "SELECT REGEXP_SUBSTR(a, '^[0-9]', n) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination", "boundary"] From 921bf7d9634ed5ba57b0df166519922d7a98347e Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:15:58 +0000 Subject: [PATCH 03/10] cases: probe a colon path directly after a :: cast --- .../functions/get-path.cast-then-path.toml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 cases/translation/functions/get-path.cast-then-path.toml diff --git a/cases/translation/functions/get-path.cast-then-path.toml b/cases/translation/functions/get-path.cast-then-path.toml new file mode 100644 index 000000000..38039dccb --- /dev/null +++ b/cases/translation/functions/get-path.cast-then-path.toml @@ -0,0 +1,30 @@ +category = "translation" +construct = "sf.functions.get-path.colon.syntax" +session_profile = "utc-v1" + +[[case]] +id = "t001_variant_cast_then_path" +description = "A colon path directly after a ::VARIANT cast, without parentheses. Records whether Snowflake accepts it." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT PARSE_JSON('{\"city\":\"x\"}')::VARIANT:city AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t002_structured_object_cast_then_path" +description = "A colon path directly after a ::OBJECT(city VARCHAR) cast, without parentheses. Records whether Snowflake accepts it." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT OBJECT_CONSTRUCT('city', 'x')::OBJECT(city VARCHAR):city AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t003_parenthesised_structured_object_path" +description = "Control: the same structured OBJECT cast in parentheses, then the colon path." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT (OBJECT_CONSTRUCT('city', 'x')::OBJECT(city VARCHAR)):city AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] From 7b15243092efebd7d1a588d64c146211a17e1f3e Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:29:39 +0000 Subject: [PATCH 04/10] cases: probe window functions written without OVER --- .../functions/window-without-over.toml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 cases/translation/functions/window-without-over.toml diff --git a/cases/translation/functions/window-without-over.toml b/cases/translation/functions/window-without-over.toml new file mode 100644 index 000000000..00848209c --- /dev/null +++ b/cases/translation/functions/window-without-over.toml @@ -0,0 +1,30 @@ +category = "translation" +construct = "sf.functions.cume-dist" +session_profile = "utc-v1" + +[[case]] +id = "t001_cume_dist_without_over" +description = "CUME_DIST() written without OVER. Records whether Snowflake refuses it, and how." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT CUME_DIST() AS r FROM (SELECT 1 AS x) t" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t002_percent_rank_without_over" +description = "PERCENT_RANK() written without OVER. Records whether Snowflake refuses it, and how." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT PERCENT_RANK() AS r FROM (SELECT 1 AS x) t" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t003_nth_value_without_over" +description = "NTH_VALUE(x, 1) written without OVER. Records whether Snowflake refuses it, and how." +columns = [{ name = "k", type = "NUMBER(38,0)" }] +rows = [["1"]] +query = "SELECT NTH_VALUE(x, 1) AS r FROM (SELECT 1 AS x) t" +order = "ordered" +tags = ["type-combination"] From eb076e8718bcbe00445d96fefc791dc1ead1bb95 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:49:50 +0000 Subject: [PATCH 05/10] cases: window functions without OVER are compile errors CUME_DIST(), PERCENT_RANK() and NTH_VALUE(x, 1) without OVER: A's screen showed Snowflake refusing each when compiling (2064, 42601, missing window specification). compile_error and tags are not hashed, so the case hashes stay as screened. --- .../functions/window-without-over.toml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cases/translation/functions/window-without-over.toml b/cases/translation/functions/window-without-over.toml index 00848209c..1ea328867 100644 --- a/cases/translation/functions/window-without-over.toml +++ b/cases/translation/functions/window-without-over.toml @@ -4,27 +4,30 @@ session_profile = "utc-v1" [[case]] id = "t001_cume_dist_without_over" -description = "CUME_DIST() written without OVER. Records whether Snowflake refuses it, and how." +compile_error = true +description = "CUME_DIST() written without OVER, which Snowflake refuses when compiling (2064, missing window specification). Catches: a value returned instead of the error" columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT CUME_DIST() AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["type-combination"] +tags = ["error"] [[case]] id = "t002_percent_rank_without_over" -description = "PERCENT_RANK() written without OVER. Records whether Snowflake refuses it, and how." +compile_error = true +description = "PERCENT_RANK() written without OVER, which Snowflake refuses when compiling (2064, missing window specification). Catches: a value returned instead of the error" columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT PERCENT_RANK() AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["type-combination"] +tags = ["error"] [[case]] id = "t003_nth_value_without_over" -description = "NTH_VALUE(x, 1) written without OVER. Records whether Snowflake refuses it, and how." +compile_error = true +description = "NTH_VALUE(x, 1) written without OVER, which Snowflake refuses when compiling (2064, missing window specification). Catches: a value returned instead of the error" columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT NTH_VALUE(x, 1) AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["type-combination"] +tags = ["error"] From 2d94c3ee8aab98ca51bd66289c87babe84d690ec Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:56:04 +0000 Subject: [PATCH 06/10] cases: keep the type-combination tag beside error on the window cases The translation category needs a case covering type-combination inputs; dropping the tag made sqlquack pending refuse the file. --- cases/translation/functions/window-without-over.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cases/translation/functions/window-without-over.toml b/cases/translation/functions/window-without-over.toml index 1ea328867..72bab8ebe 100644 --- a/cases/translation/functions/window-without-over.toml +++ b/cases/translation/functions/window-without-over.toml @@ -10,7 +10,7 @@ columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT CUME_DIST() AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["error"] +tags = ["type-combination", "error"] [[case]] id = "t002_percent_rank_without_over" @@ -20,7 +20,7 @@ columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT PERCENT_RANK() AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["error"] +tags = ["type-combination", "error"] [[case]] id = "t003_nth_value_without_over" @@ -30,4 +30,4 @@ columns = [{ name = "k", type = "NUMBER(38,0)" }] rows = [["1"]] query = "SELECT NTH_VALUE(x, 1) AS r FROM (SELECT 1 AS x) t" order = "ordered" -tags = ["error"] +tags = ["type-combination", "error"] From 45cb6f568e7731b3c11cdc47dff4f7d0ef955ef6 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:21:53 +0000 Subject: [PATCH 07/10] evidence: record window functions written without OVER as compile errors Snowflake results for the three cases in cases/translation/functions/window-without-over.toml at 2d94c3e: each is refused with 2064 (SQLSTATE 42601). --- ...d16012e1e2cc5e054a8ace797e9cc44712438.json | 55 +++++++++++++++++++ ...f4c045dd3fc7453490a33de25e9ac9b31cc52.json | 55 +++++++++++++++++++ ...18425acaf3c08934e219873944ff068f97100.json | 55 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 evidence/results/22ccd171bbd822a887451dc251ad16012e1e2cc5e054a8ace797e9cc44712438.json create mode 100644 evidence/results/3494587befb1edb2e0867ef73daf4c045dd3fc7453490a33de25e9ac9b31cc52.json create mode 100644 evidence/results/d54a21b01972927df8289b313a918425acaf3c08934e219873944ff068f97100.json diff --git a/evidence/results/22ccd171bbd822a887451dc251ad16012e1e2cc5e054a8ace797e9cc44712438.json b/evidence/results/22ccd171bbd822a887451dc251ad16012e1e2cc5e054a8ace797e9cc44712438.json new file mode 100644 index 000000000..87cc8a432 --- /dev/null +++ b/evidence/results/22ccd171bbd822a887451dc251ad16012e1e2cc5e054a8ace797e9cc44712438.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "22ccd171bbd822a887451dc251ad16012e1e2cc5e054a8ace797e9cc44712438", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 2064, + "message": null, + "sqlstate": "42601" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "f7698193528af7ec4813e5de9ec1c230767190abf9c5cab360040668c776d91e", + "recorded_at": "2026-11-06T07:18:08.465533+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/3494587befb1edb2e0867ef73daf4c045dd3fc7453490a33de25e9ac9b31cc52.json b/evidence/results/3494587befb1edb2e0867ef73daf4c045dd3fc7453490a33de25e9ac9b31cc52.json new file mode 100644 index 000000000..ce33c8795 --- /dev/null +++ b/evidence/results/3494587befb1edb2e0867ef73daf4c045dd3fc7453490a33de25e9ac9b31cc52.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "3494587befb1edb2e0867ef73daf4c045dd3fc7453490a33de25e9ac9b31cc52", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 2064, + "message": null, + "sqlstate": "42601" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "f105714f3b4a26e6fb37903190aabc16e315282866c22c7d941a1d1ab9b8dc87", + "recorded_at": "2026-11-06T07:18:08.465533+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/d54a21b01972927df8289b313a918425acaf3c08934e219873944ff068f97100.json b/evidence/results/d54a21b01972927df8289b313a918425acaf3c08934e219873944ff068f97100.json new file mode 100644 index 000000000..7408ab708 --- /dev/null +++ b/evidence/results/d54a21b01972927df8289b313a918425acaf3c08934e219873944ff068f97100.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "d54a21b01972927df8289b313a918425acaf3c08934e219873944ff068f97100", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 2064, + "message": null, + "sqlstate": "42601" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "33673c1a3247d78cb1521eb0294343ed8824a905ad3dd11ec3401b8f385c1771", + "recorded_at": "2026-11-06T07:18:08.465533+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} From 4a9f9b4d1138b8bc67bc55a3d741987e68e34e96 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:15:21 +0000 Subject: [PATCH 08/10] Refuse CUME_DIST, PERCENT_RANK and NTH_VALUE without OVER Snowflake rejects these window functions without an OVER clause when compiling (2064, SQLSTATE 42601, missing window specification; receipts on evidence/d215-receipts 19c3fd96). Nothing caught them: check passed the SQL, and convert only refused it for lacking a rule (SQ0002). The refusal layer now rejects a call to one of the three that no OVER clause applies to as SQ0205 window_without_over, in check and convert, nested or not. The lineage scope rule reports E232 for them as well, for the typed nodes DuckDB parses and the named calls Snowflake's faithful parse keeps. --- .../sqlquack-core/src/diagnostics/codes.lock | 1 + .../src/diagnostics/constants.rs | 12 ++++++ .../src/refusals/_helpers/rejections.rs | 24 ++++++++++- .../src/refusals/main/snowflake_rejections.rs | 9 +++- .../src/refusals/tests/test_refusals.rs | 21 +++++++++ .../lineage/_helpers/checks/scope_rules.rs | 9 +++- .../src/lineage/tests/test_checks.rs | 43 +++++++++++++++++++ crates/sqlquack/tests/cli/test_check.rs | 8 ++++ .../docs/docs/diagnostics/reference.mdx | 15 +++++++ 9 files changed, 138 insertions(+), 4 deletions(-) diff --git a/crates/sqlquack-core/src/diagnostics/codes.lock b/crates/sqlquack-core/src/diagnostics/codes.lock index 876146d43..2c3c4d32b 100644 --- a/crates/sqlquack-core/src/diagnostics/codes.lock +++ b/crates/sqlquack-core/src/diagnostics/codes.lock @@ -20,3 +20,4 @@ SQ0201 set_operation_all SQ0202 lock_wait SQ0203 qualify_without_window SQ0204 star_filter_drops_named_column +SQ0205 window_without_over diff --git a/crates/sqlquack-core/src/diagnostics/constants.rs b/crates/sqlquack-core/src/diagnostics/constants.rs index 1757bdcd4..7bb469f72 100644 --- a/crates/sqlquack-core/src/diagnostics/constants.rs +++ b/crates/sqlquack-core/src/diagnostics/constants.rs @@ -302,4 +302,16 @@ pub const CODES: &[CodeEntry] = &[ case fails it whatever the table holds, so no schema is needed to see it.", example: r#"echo "SELECT * ILIKE '%_id' REPLACE (price * 2 AS price) FROM orders" | sqlquack check"#, }, + CodeEntry { + code: "SQ0205", + name: "window_without_over", + alias: None, + title: "A window function needs an OVER clause", + help: Some("add an OVER clause, such as `OVER (ORDER BY ...)`"), + explanation: "CUME_DIST, PERCENT_RANK and NTH_VALUE are window functions only: \ + Snowflake rejects a call to one without an OVER clause when compiling the query \ + (Snowflake error 2064, missing window specification), so the statement never runs \ + there. Add the OVER clause that says which rows the function ranks or reads.", + example: r#"echo "SELECT CUME_DIST() AS d FROM orders" | sqlquack check"#, + }, ]; diff --git a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs index d45f7486d..d73c37313 100644 --- a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs +++ b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs @@ -3,9 +3,29 @@ use sqlquack_syntax::expressions::names::models::Star; use sqlquack_syntax::{Expression, ExpressionWalk}; -/// The registry name of the rejection a node causes, with the reason, if it causes one. -pub(crate) fn rejection(node: &Expression) -> Option<(&'static str, String)> { +/// Window functions Snowflake refuses without OVER (error 2064, missing window specification). +const WINDOW_ONLY_CALLS: [&str; 3] = ["CUME_DIST", "PERCENT_RANK", "NTH_VALUE"]; + +/// The rejection a node causes, if any, given the calls an OVER clause applies to (`windowed`). +pub(crate) fn rejection( + node: &Expression, + windowed: &[&Expression], +) -> Option<(&'static str, String)> { match node { + Expression::Function(call) + if WINDOW_ONLY_CALLS + .iter() + .any(|name| call.name.eq_ignore_ascii_case(name)) + && !windowed.iter().any(|window| std::ptr::eq(*window, node)) => + { + Some(( + "window_without_over", + format!( + "{} is a window function, which Snowflake runs only with an OVER clause", + call.name.to_ascii_uppercase() + ), + )) + } Expression::Intersect(operation) if operation.all => Some(set_operation_all("INTERSECT")), Expression::Except(operation) if operation.all => { Some(set_operation_all("EXCEPT or MINUS")) diff --git a/crates/sqlquack-core/src/refusals/main/snowflake_rejections.rs b/crates/sqlquack-core/src/refusals/main/snowflake_rejections.rs index fcc28d4af..4c79d9289 100644 --- a/crates/sqlquack-core/src/refusals/main/snowflake_rejections.rs +++ b/crates/sqlquack-core/src/refusals/main/snowflake_rejections.rs @@ -17,10 +17,17 @@ pub fn snowflake_rejections( (source, lines): (&str, &SourceLines), ) -> Vec { let characters: OnceCell> = OnceCell::new(); + let windowed: Vec<&Expression> = statement + .dfs() + .filter_map(|node| match node { + Expression::WindowFunction(window) => Some(&window.this), + _ => None, + }) + .collect(); statement .dfs() .filter_map(|node| { - let (name, reason) = rejection(node)?; + let (name, reason) = rejection(node, &windowed)?; let text = characters.get_or_init(|| source.chars().collect()); Some(Refusal { construct: Some(Construct::new(ConstructFamily::Syntax, name)), diff --git a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs index 2ac00ac8f..258024ddb 100644 --- a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs +++ b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs @@ -114,6 +114,27 @@ fn given_sql_snowflake_parses_then_rejects_when_refusing_then_each_rejection_is_ ], expected_codes: vec!["SQ0204", "SQ0204"], }, + RejectionTestCase { + description: "CUME_DIST, PERCENT_RANK and NTH_VALUE without OVER are rejected, also nested", + sql: "SELECT CUME_DIST() AS c FROM orders; \ + SELECT percent_rank() + 1 AS p FROM orders; \ + SELECT order_id FROM orders WHERE NTH_VALUE(price, 1) > 0", + expected_rejections: vec![ + "window_without_over", + "window_without_over", + "window_without_over", + ], + expected_codes: vec!["SQ0205", "SQ0205", "SQ0205"], + }, + RejectionTestCase { + description: "the same calls under OVER are left to conversion, an inner one without it is not", + sql: "SELECT CUME_DIST() OVER (ORDER BY price) AS c, \ + PERCENT_RANK() OVER (PARTITION BY customer_id ORDER BY price) AS p, \ + NTH_VALUE(price, 1) OVER (ORDER BY order_id) AS n FROM orders; \ + SELECT NTH_VALUE(CUME_DIST(), 1) OVER (ORDER BY order_id) AS n FROM orders", + expected_rejections: vec!["window_without_over"], + expected_codes: vec!["SQ0205"], + }, RejectionTestCase { description: "a star without ILIKE, or ILIKE without REPLACE or RENAME, is left to conversion", sql: "SELECT * REPLACE (price * 2 AS price) FROM orders; \ diff --git a/crates/sqlquack-semantics/src/lineage/_helpers/checks/scope_rules.rs b/crates/sqlquack-semantics/src/lineage/_helpers/checks/scope_rules.rs index 37c07eb96..5cf613e52 100644 --- a/crates/sqlquack-semantics/src/lineage/_helpers/checks/scope_rules.rs +++ b/crates/sqlquack-semantics/src/lineage/_helpers/checks/scope_rules.rs @@ -18,6 +18,8 @@ const COLUMNS_FUNCTION: &str = "columns"; const NOT_GROUPED: &str = "Column must be grouped or used in an aggregate function"; const MISPLACED_AGGREGATE: &str = "Aggregate function is not allowed in this clause or inside another aggregate"; +/// Window functions Snowflake refuses without OVER (2064) that its faithful parse keeps as named calls. +const WINDOW_ONLY_CALLS: [&str; 3] = ["CUME_DIST", "PERCENT_RANK", "NTH_VALUE"]; const MISPLACED_WINDOW: &str = "Window function is not allowed in this clause or inside another aggregate/window function"; @@ -557,7 +559,7 @@ fn placement( continue; } if !placed.window_function - && matches!( + && (matches!( node, Expression::RowNumber(_) | Expression::Rank(_) @@ -567,7 +569,12 @@ fn placement( | Expression::Lead(_) | Expression::FirstValue(_) | Expression::LastValue(_) + | Expression::NthValue(_) ) + || matches!(node, Expression::CumeDist(call) if call.args.is_empty()) + || matches!(node, Expression::PercentRank(call) if call.args.is_empty()) + || matches!(node, Expression::Function(call) + if WINDOW_ONLY_CALLS.iter().any(|name| call.name.eq_ignore_ascii_case(name)))) { findings.push(rule( "E232", diff --git a/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs b/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs index a36c1ee43..7a24d3325 100644 --- a/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs +++ b/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs @@ -32,6 +32,27 @@ fn given_a_duckdb_select_when_checking_its_scope_rules_then_broken_rules_are_fou sql: "SELECT ROW_NUMBER() AS r FROM orders", expected_findings: &[("E232", "Window function requires an OVER clause", 0)], }, + QueryFindingsTestCase { + description: "CUME_DIST without OVER", + sql: "SELECT CUME_DIST() AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 0)], + }, + QueryFindingsTestCase { + description: "PERCENT_RANK without OVER", + sql: "SELECT PERCENT_RANK() AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 0)], + }, + QueryFindingsTestCase { + description: "NTH_VALUE without OVER", + sql: "SELECT NTH_VALUE(id, 1) AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 17)], + }, + QueryFindingsTestCase { + description: "CUME_DIST, PERCENT_RANK and NTH_VALUE under OVER", + sql: "SELECT CUME_DIST() OVER (ORDER BY id) AS c, PERCENT_RANK() OVER (ORDER BY id) AS p, \ + NTH_VALUE(id, 1) OVER (ORDER BY id) AS n FROM orders", + expected_findings: &[], + }, QueryFindingsTestCase { description: "two FROM items under one alias", sql: "SELECT o.id FROM orders o JOIN customers o ON TRUE", @@ -172,6 +193,27 @@ fn given_a_snowflake_query_when_checking_built_in_calls_and_types_then_definite_ 18, )], }, + QueryFindingsTestCase { + description: "CUME_DIST without OVER, as Snowflake parses it", + sql: "SELECT CUME_DIST() AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 7)], + }, + QueryFindingsTestCase { + description: "PERCENT_RANK without OVER, as Snowflake parses it", + sql: "SELECT PERCENT_RANK() AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 7)], + }, + QueryFindingsTestCase { + description: "NTH_VALUE without OVER, as Snowflake parses it", + sql: "SELECT NTH_VALUE(id, 1) AS w FROM orders", + expected_findings: &[("E232", "Window function requires an OVER clause", 7)], + }, + QueryFindingsTestCase { + description: "CUME_DIST, PERCENT_RANK and NTH_VALUE under OVER", + sql: "SELECT CUME_DIST() OVER (ORDER BY id) AS c, PERCENT_RANK() OVER (ORDER BY id) AS p, \ + NTH_VALUE(id, 1) OVER (ORDER BY id) AS n FROM orders", + expected_findings: &[], + }, QueryFindingsTestCase { description: "a number condition in WHERE", sql: "SELECT id FROM orders WHERE amount", @@ -428,3 +470,4 @@ fn given_a_postgres_query_when_checking_scope_rules_then_only_postgres_errors_ar ); } } + diff --git a/crates/sqlquack/tests/cli/test_check.rs b/crates/sqlquack/tests/cli/test_check.rs index 1b43e592a..68a6df592 100644 --- a/crates/sqlquack/tests/cli/test_check.rs +++ b/crates/sqlquack/tests/cli/test_check.rs @@ -60,6 +60,14 @@ fn given_snowflake_sql_when_checked_then_problems_are_coded_and_the_exit_status_ expected_stdout_prefix: "", expected_stderr_mention: "--> :1:25\n", }, + CheckCommandTestCase { + description: "a window function without OVER is SQ0205 and exits 1", + arguments: &["check"], + sql: "SELECT PERCENT_RANK() AS p FROM orders", + expected_exit_code: Some(1), + expected_stdout_prefix: "", + expected_stderr_mention: "error[SQ0205]", + }, CheckCommandTestCase { description: "SQL that does not parse is SQ0001 and exits 1", arguments: &["check"], diff --git a/website/src/content/docs/docs/diagnostics/reference.mdx b/website/src/content/docs/docs/diagnostics/reference.mdx index 4b3d01460..b3114cf4a 100644 --- a/website/src/content/docs/docs/diagnostics/reference.mdx +++ b/website/src/content/docs/docs/diagnostics/reference.mdx @@ -33,6 +33,7 @@ renumbered. See [Reading diagnostics](/docs/diagnostics/overview/) for the outpu | [`SQ0202`](#sq0202) | `lock_wait` | Snowflake does not support WAIT in FOR UPDATE | | [`SQ0203`](#sq0203) | `qualify_without_window` | QUALIFY needs a window function | | [`SQ0204`](#sq0204) | `star_filter_drops_named_column` | REPLACE or RENAME names a column the star's ILIKE pattern removed | +| [`SQ0205`](#sq0205) | `window_without_over` | A window function needs an OVER clause | ## Conversion @@ -321,3 +322,17 @@ In `* ILIKE 'pattern' REPLACE (...)` or `RENAME (...)`, Snowflake applies the pa ```sh echo "SELECT * ILIKE '%_id' REPLACE (price * 2 AS price) FROM orders" | sqlquack check ``` + +### SQ0205 + +**A window function needs an OVER clause** (`window_without_over`) + +CUME_DIST, PERCENT_RANK and NTH_VALUE are window functions only: Snowflake rejects a call to one without an OVER clause when compiling the query (Snowflake error 2064, missing window specification), so the statement never runs there. Add the OVER clause that says which rows the function ranks or reads. + +**Help:** add an OVER clause, such as `OVER (ORDER BY ...)` + +**Example:** + +```sh +echo "SELECT CUME_DIST() AS d FROM orders" | sqlquack check +``` From 1294bb6299764a75dc49770da70c910cba2c3ccc Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:35:39 +0000 Subject: [PATCH 09/10] evidence: record a colon path directly after a :: cast Snowflake results for the three cases in cases/translation/functions/get-path.cast-then-path.toml at 7b15243. --- ...b31d08be80e0b91605da058c3b39ff5d896d.arrow | Bin 0 -> 1018 bytes ...4b31d08be80e0b91605da058c3b39ff5d896d.json | 61 ++++++++++++++++++ ...899f8eb598441541055a23c4dc3d76fa1f94.arrow | Bin 0 -> 1018 bytes ...4899f8eb598441541055a23c4dc3d76fa1f94.json | 61 ++++++++++++++++++ ...61f6de565e062c2fb3df382b39ae2ff6be61.arrow | Bin 0 -> 1018 bytes ...a61f6de565e062c2fb3df382b39ae2ff6be61.json | 61 ++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.arrow create mode 100644 evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.json create mode 100644 evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.arrow create mode 100644 evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.json create mode 100644 evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.arrow create mode 100644 evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.json diff --git a/evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.arrow b/evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.arrow new file mode 100644 index 0000000000000000000000000000000000000000..c88c2b4561ee0a7ccff0d83d90e209e5d5074502 GIT binary patch literal 1018 zcmeHGy-LGS6h5&xF=~b&Mfv~^4i2U*)X@%Z1tA1=EO6! z4j{6R+egH<_6wzr7mDMyaUQ{uk#+7mE{5&_AHXZ{2wVflfCA3Y5B4^}Eqv&^-lb-#4peKFX`{aw4++#W$OzbL;!|t@a6WxxgYzOV)jF z*tekfP>}U!^E@jS#k2zVu?cR9b8`pmkx0(3IEw56<#b#irJhE?N;^_-o^UgU63Bu1 zB?7%$X(_#PyGu^mF||1J2%wESv(mEUhnxLT?A?qbZ|d67yxkjx{gSd~fUZ3~d06*~ hiTZdy1UMUWtxdvbaLU&l|KHbNd|xlv|4{$uKLH@!dg}lH literal 0 HcmV?d00001 diff --git a/evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.json b/evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.json new file mode 100644 index 000000000..af3347396 --- /dev/null +++ b/evidence/results/3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "78aa36fd259a1ffb633095ab8e45edf947528ae4153b9fb9c05638319a1c8ac3", + "case_hash": "3362db7ab26c350e5f51dd0c63b4b31d08be80e0b91605da058c3b39ff5d896d", + "columns": [ + { + "internal_size": 134217728, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 2, + "type_name": "TEXT" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "e6e58604fde117db9380abeeb426b8f544e6c112949eef2d182f9b5443ad8ede", + "recorded_at": "2026-11-06T06:33:11.556124+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 1, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.arrow b/evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.arrow new file mode 100644 index 0000000000000000000000000000000000000000..c1e8714489c33f9b4cfc5767905eb12792eb47c2 GIT binary patch literal 1018 zcmeH`y-or_6ooIcz^W9)5JOEYEG*zyE z7QP97XV{IznAll)lQVPf%$fVg5`^K!Rn2ULJ!H&ECfl*1mfqDT*qF&3-R;d1H6Jqa1D-t2TrL+dU56HfNgLWkGjLo zViLh`(I~$(x~nys^;)Y{KY^?6o5eh8N2C6HV7B~4+Z%MJsrKz#?qg!*z^vCDD)0No z{T6LcHzTuKPIH>#zP?up;u4rdLt{^ ze)3Bc-E1xExtXW4RHv0BmwIjk-Dzg2WmEFQuKHx@zNuA=%{WfTB2D$a>Ri<;qMSvl n&b!V_>sslk$@u|LZ;G`{0zSiKUw!<4Uw`p^y)gg4{pUXc(D{7X literal 0 HcmV?d00001 diff --git a/evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.json b/evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.json new file mode 100644 index 000000000..ae5408a12 --- /dev/null +++ b/evidence/results/7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "13d21a4fc813f47997cb0d107a6fc8a8f14429ef3531144d7bc3611b5d6f8c8d", + "case_hash": "7ca5bf74d8e5e862bb37c4fb22a4899f8eb598441541055a23c4dc3d76fa1f94", + "columns": [ + { + "internal_size": null, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 5, + "type_name": "VARIANT" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "1d729f6088f6279d86ba55f0310708b0bd2a82411ebba51f32234b739ecdeebd", + "recorded_at": "2026-11-06T06:33:11.556124+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 1, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.arrow b/evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.arrow new file mode 100644 index 0000000000000000000000000000000000000000..c88c2b4561ee0a7ccff0d83d90e209e5d5074502 GIT binary patch literal 1018 zcmeHGy-LGS6h5&xF=~b&Mfv~^4i2U*)X@%Z1tA1=EO6! z4j{6R+egH<_6wzr7mDMyaUQ{uk#+7mE{5&_AHXZ{2wVflfCA3Y5B4^}Eqv&^-lb-#4peKFX`{aw4++#W$OzbL;!|t@a6WxxgYzOV)jF z*tekfP>}U!^E@jS#k2zVu?cR9b8`pmkx0(3IEw56<#b#irJhE?N;^_-o^UgU63Bu1 zB?7%$X(_#PyGu^mF||1J2%wESv(mEUhnxLT?A?qbZ|d67yxkjx{gSd~fUZ3~d06*~ hiTZdy1UMUWtxdvbaLU&l|KHbNd|xlv|4{$uKLH@!dg}lH literal 0 HcmV?d00001 diff --git a/evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.json b/evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.json new file mode 100644 index 000000000..0b60456bd --- /dev/null +++ b/evidence/results/9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "78aa36fd259a1ffb633095ab8e45edf947528ae4153b9fb9c05638319a1c8ac3", + "case_hash": "9babe630df393a56dfe78717ce0a61f6de565e062c2fb3df382b39ae2ff6be61", + "columns": [ + { + "internal_size": 134217728, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 2, + "type_name": "TEXT" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "2f4f33e3be3a953aedf94a082bb8542e8ef8b709e7befbc9fa9c9e5f44ee3763", + "recorded_at": "2026-11-06T06:33:11.556124+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 1, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} From 96ff6832d98a3f89e179cdd1055dcb9f76824f90 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:57:40 +0000 Subject: [PATCH 10/10] Pin NTH_VALUE under OVER with FROM FIRST/LAST and IGNORE/RESPECT NULLS Review asked whether a modifier between the call and OVER hides the call from the windowed set. It does not: these spellings report nothing in the refusal layer or the E232 rule, and now stay pinned that way. --- .../sqlquack-core/src/refusals/tests/test_refusals.rs | 3 +++ .../src/lineage/tests/test_checks.rs | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs index 258024ddb..8d6d049df 100644 --- a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs +++ b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs @@ -131,6 +131,9 @@ fn given_sql_snowflake_parses_then_rejects_when_refusing_then_each_rejection_is_ sql: "SELECT CUME_DIST() OVER (ORDER BY price) AS c, \ PERCENT_RANK() OVER (PARTITION BY customer_id ORDER BY price) AS p, \ NTH_VALUE(price, 1) OVER (ORDER BY order_id) AS n FROM orders; \ + SELECT NTH_VALUE(price, 2) FROM LAST IGNORE NULLS OVER (ORDER BY order_id) AS n FROM orders; \ + SELECT NTH_VALUE(price, 1) FROM FIRST RESPECT NULLS OVER (ORDER BY order_id) AS n FROM orders; \ + SELECT NTH_VALUE(price, 1) IGNORE NULLS OVER (ORDER BY order_id) AS n FROM orders; \ SELECT NTH_VALUE(CUME_DIST(), 1) OVER (ORDER BY order_id) AS n FROM orders", expected_rejections: vec!["window_without_over"], expected_codes: vec!["SQ0205"], diff --git a/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs b/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs index 7a24d3325..6160d78ac 100644 --- a/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs +++ b/crates/sqlquack-semantics/src/lineage/tests/test_checks.rs @@ -50,7 +50,10 @@ fn given_a_duckdb_select_when_checking_its_scope_rules_then_broken_rules_are_fou QueryFindingsTestCase { description: "CUME_DIST, PERCENT_RANK and NTH_VALUE under OVER", sql: "SELECT CUME_DIST() OVER (ORDER BY id) AS c, PERCENT_RANK() OVER (ORDER BY id) AS p, \ - NTH_VALUE(id, 1) OVER (ORDER BY id) AS n FROM orders", + NTH_VALUE(id, 1) OVER (ORDER BY id) AS n, \ + NTH_VALUE(id, 2) FROM LAST IGNORE NULLS OVER (ORDER BY id) AS l, \ + NTH_VALUE(id, 1) FROM FIRST RESPECT NULLS OVER (ORDER BY id) AS f, \ + NTH_VALUE(id, 1) IGNORE NULLS OVER (ORDER BY id) AS i FROM orders", expected_findings: &[], }, QueryFindingsTestCase { @@ -211,7 +214,10 @@ fn given_a_snowflake_query_when_checking_built_in_calls_and_types_then_definite_ QueryFindingsTestCase { description: "CUME_DIST, PERCENT_RANK and NTH_VALUE under OVER", sql: "SELECT CUME_DIST() OVER (ORDER BY id) AS c, PERCENT_RANK() OVER (ORDER BY id) AS p, \ - NTH_VALUE(id, 1) OVER (ORDER BY id) AS n FROM orders", + NTH_VALUE(id, 1) OVER (ORDER BY id) AS n, \ + NTH_VALUE(id, 2) FROM LAST IGNORE NULLS OVER (ORDER BY id) AS l, \ + NTH_VALUE(id, 1) FROM FIRST RESPECT NULLS OVER (ORDER BY id) AS f, \ + NTH_VALUE(id, 1) IGNORE NULLS OVER (ORDER BY id) AS i FROM orders", expected_findings: &[], }, QueryFindingsTestCase {