From 5778f8de0266dd79e64346c1096c45e760eb89e4 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:50:23 +0000 Subject: [PATCH 1/9] cases: the names of unaliased ROUND and current-time select items Two playground shapes over input(n NUMBER(10,2)): ROUND with one and two arguments in mixed case and spacing, and CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP() (values diverge under current_time; the names and types are the receipt). --- cases/relational/unaliased_round_current.toml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 cases/relational/unaliased_round_current.toml diff --git a/cases/relational/unaliased_round_current.toml b/cases/relational/unaliased_round_current.toml new file mode 100644 index 000000000..e860448d0 --- /dev/null +++ b/cases/relational/unaliased_round_current.toml @@ -0,0 +1,42 @@ +# The names Snowflake gives unaliased ROUND and current-time items, as a playground query writes them. +# The recorded column names are the receipts for naming them. +category = "relational" +construct = "node:alias" +session_profile = "utc-v1" + +[[case]] +id = "unaliased_round" +description = "Unaliased ROUND with one and two arguments, in upper and lower case and with inner spaces, over a NULL, a half and a negative." +columns = [{ name = "n", type = "NUMBER(10,2)" }] +rows = [ + ["1.25"], + ["1.25"], + ["-2.55"], + ["NULL"], +] +query = "SELECT ROUND(n, 1), ROUND(n), round( n, 0 ) FROM input" +order = "ordered" +tags = ["null", "boundary", "duplicates"] + +[[case]] +id = "unaliased_round_no_rows" +description = "Unaliased ROUND over no rows still names its columns." +columns = [{ name = "n", type = "NUMBER(10,2)" }] +rows = [ + ["1.00"], +] +query = "SELECT ROUND(n, 1), ROUND(n) FROM input WHERE n > 5" +order = "ordered" +tags = ["empty-input"] + +[[case]] +id = "unaliased_current_time" +description = "Unaliased CURRENT_DATE(), CURRENT_DATE and CURRENT_TIMESTAMP(): only the column names and types are the receipt; the values read the clock." +columns = [{ name = "n", type = "NUMBER(10,2)" }] +rows = [ + ["1.00"], +] +query = "SELECT CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP() FROM input" +order = "ordered" +expected_divergence = "current_time" +tags = ["boundary"] From c0514f7ca5fdb781b1ffb63f148bd8b89a894550 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:19:21 +0000 Subject: [PATCH 2/9] cases: the names of unaliased DISTINCT aggregates and row expressions SUM, MAX and AVG over DISTINCT in one query, and COALESCE, CAST of text to INTEGER and lower(g ) in another, since one select list cannot hold both without GROUP BY; each item as written, with a no-rows twin for each. --- cases/relational/unaliased_round_current.toml | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cases/relational/unaliased_round_current.toml b/cases/relational/unaliased_round_current.toml index e860448d0..575b8ccb5 100644 --- a/cases/relational/unaliased_round_current.toml +++ b/cases/relational/unaliased_round_current.toml @@ -40,3 +40,53 @@ query = "SELECT CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP() FROM input" order = "ordered" expected_divergence = "current_time" tags = ["boundary"] + +[[case]] +id = "unaliased_distinct_aggregates" +description = "Unaliased SUM, MAX and AVG over DISTINCT, over a duplicate and a NULL. The aggregates are one query and the row expressions another, since one select list cannot hold both without GROUP BY; each item is written as given." +columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }] +rows = [ + ["1.50", "'12'"], + ["1.50", "'12'"], + ["NULL", "NULL"], + ["-2.25", "'-3'"], +] +query = "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM input" +order = "ordered" +tags = ["null", "duplicates"] + +[[case]] +id = "unaliased_distinct_aggregates_no_rows" +description = "The unaliased DISTINCT aggregates over no rows still name their columns." +columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }] +rows = [ + ["1.00", "'1'"], +] +query = "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM input WHERE n > 5" +order = "ordered" +tags = ["empty-input"] + +[[case]] +id = "unaliased_row_expressions" +description = "Unaliased COALESCE with a column and with literals only, CAST of numeric text to INTEGER, and lower with a space before its closing parenthesis, over a duplicate and a NULL." +columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }] +rows = [ + ["1.50", "'12'"], + ["1.50", "'12'"], + ["NULL", "NULL"], + ["-2.25", "'-3'"], +] +query = "SELECT COALESCE(n, 0, 1), COALESCE(1, 2), CAST(g AS INTEGER), lower(g ) FROM input" +order = "bag" +tags = ["null", "duplicates"] + +[[case]] +id = "unaliased_row_expressions_no_rows" +description = "The unaliased row expressions over no rows still name their columns." +columns = [{ name = "n", type = "NUMBER(10,2)" }, { name = "g", type = "VARCHAR" }] +rows = [ + ["1.00", "'1'"], +] +query = "SELECT COALESCE(n, 0, 1), COALESCE(1, 2), CAST(g AS INTEGER), lower(g ) FROM input WHERE n > 5" +order = "bag" +tags = ["empty-input"] From abeda9ac6fd487dd20643dc7a6eb25d870217f4f Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:20:04 +0000 Subject: [PATCH 3/9] evidence: Snowflake's names for unaliased ROUND and current-time items Recorded from test/unaliased-round-current 5778f8de (3 cases, utc-v1, no failures). Snowflake upper-cases the item text and keeps its spacing: ROUND(N, 1), ROUND(N), ROUND( N, 0 ), CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP(). --- ...5e04fb961d475e7dc4dfdc0dbca414e800e9.arrow | Bin 0 -> 1298 bytes ...55e04fb961d475e7dc4dfdc0dbca414e800e9.json | 79 ++++++++++++++++++ ...59d72d08fbfb0144e3bffc8ea04a8a57d0a4.arrow | Bin 0 -> 418 bytes ...459d72d08fbfb0144e3bffc8ea04a8a57d0a4.json | 70 ++++++++++++++++ ...7441518034208c7d0655f694b839e2bf1788.arrow | Bin 0 -> 2018 bytes ...c7441518034208c7d0655f694b839e2bf1788.json | 79 ++++++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 evidence/results/08a45c3252e8e18e403cbaa753d55e04fb961d475e7dc4dfdc0dbca414e800e9.arrow create mode 100644 evidence/results/08a45c3252e8e18e403cbaa753d55e04fb961d475e7dc4dfdc0dbca414e800e9.json create mode 100644 evidence/results/1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4.arrow create mode 100644 evidence/results/1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4.json create mode 100644 evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.arrow create mode 100644 evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.json diff --git a/evidence/results/08a45c3252e8e18e403cbaa753d55e04fb961d475e7dc4dfdc0dbca414e800e9.arrow b/evidence/results/08a45c3252e8e18e403cbaa753d55e04fb961d475e7dc4dfdc0dbca414e800e9.arrow new file mode 100644 index 0000000000000000000000000000000000000000..abe7996347929254611171ab4e08583c28d21eaa GIT binary patch literal 1298 zcmeHHJxc>Y5S=sUxx_;b4RWBRg@Q$d6gC!Tyuu=WTsCMYqGFVY0jq#zN`I99z}C`6 zu*@$|-`l-C*2=Vk4`y#>cXsB@&hBMJad@&N@&&vFB8d=ni6xR*QP6-Oge|ZX#;_bk zYOoUQxx}|Y9izg&Ipdv_v4T2dZbCu`u>@)NiXuPIXL`SrcXie|THnB4X0=%uo&LF;s0IW@t8aIci)A=4~M11d41R*HnUDgy`Tz@6aPggao#i5-8xILesw zd>^DY_K=itX|1S6D|JLz705-_v4*lrHQCiqq!;}NYwO>YGd9=e`8HjSQ*Zgxb*j$9 zNn7fTTBoOS_m^>?9&ob_UKpqS(9;v~*4dPtW%HZX?vHRAS0yJK$!4-Q{FsC&{`Wl# I^Ns#D9~$&3e*gdg literal 0 HcmV?d00001 diff --git a/evidence/results/1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4.json b/evidence/results/1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4.json new file mode 100644 index 000000000..abc57b935 --- /dev/null +++ b/evidence/results/1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4.json @@ -0,0 +1,70 @@ +{ + "arrow_sha256": "7345db83a3987b347fdc6fc2c78c28b308f9cabd7ec485c00eaf86fe3edf8304", + "case_hash": "1af29e9c87ebceb9155ea873c3d459d72d08fbfb0144e3bffc8ea04a8a57d0a4", + "columns": [ + { + "internal_size": null, + "name": "ROUND(N, 1)", + "nullable": false, + "precision": 11, + "scale": 1, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "ROUND(N)", + "nullable": false, + "precision": 11, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + } + ], + "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": "d36e211fc3fed8074451f2d41339da299eeeeceb73885ff1d1ec59e72a80f902", + "recorded_at": "2026-11-06T10:15:37.599612+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 0, + "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/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.arrow b/evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.arrow new file mode 100644 index 0000000000000000000000000000000000000000..820504ea96297e0ca7fb554c570d61dbddbd7201 GIT binary patch literal 2018 zcmeHIK}$kG5FXR?Wv`Hs@X+BMBoaI*bm*vHVGtH#)YZgN5R>$f&`;>tp+kp$M!z8X z5uT%Cf1qC9w{I7gmzLL{f!W#F8E0p|nR%A$^}~~*h!1k2A}Jvy#g(`$ih~^}5`!+# z9IP?DiojhS7DO5mo{Btw`aYZ&(%5@AqxS}wv;wXl9@i>cUM=qxyd3m6_yzn3z5zGD zc`#$$efU0e;U1xsZ3*(c+i&(7<3Styr;yJM&Y17nc+@^<_b)~rkx9s>pnY1M=9Tdg zY8bi%Q?v<5G5_%AaJ;;7Gn9@pOzZ>EmB+dw_q*jrtmd1Xh zbx+Gd$bKENQ{`Nmx0*P==?NW&b81sqeF@}naoY#*(Vq9YBq8f2k ztyEA0ymA}ornbB*!^eZ(17X$Y|NF%rm^F9*-!IG_dX0TXoY+eR@L&CY6)*>7_22#l DY`!PQ literal 0 HcmV?d00001 diff --git a/evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.json b/evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.json new file mode 100644 index 000000000..2ce0e1ee9 --- /dev/null +++ b/evidence/results/e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788.json @@ -0,0 +1,79 @@ +{ + "arrow_sha256": "4e90f2d758188dc2495839edefc3b350719abb76ed00c4105023e5a6c19308a1", + "case_hash": "e45ba0c883892fe6d1f132128c0c7441518034208c7d0655f694b839e2bf1788", + "columns": [ + { + "internal_size": null, + "name": "ROUND(N, 1)", + "nullable": true, + "precision": 11, + "scale": 1, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "ROUND(N)", + "nullable": true, + "precision": 11, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "ROUND( N, 0 )", + "nullable": true, + "precision": 11, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + } + ], + "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": "fa6821ae91be62a7a7239183aa56b6eef96d0eef6b7fecd358c16be53188347f", + "recorded_at": "2026-11-06T10:15:37.599612+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 4, + "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 977b7440c2320d76a17a8c6cb7b407a6c4f360f9 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:32:32 +0000 Subject: [PATCH 4/9] evidence: Snowflake's names for unaliased DISTINCT aggregates and row expressions Recorded from test/unaliased-round-current c0514f7c (4 cases, utc-v1, no failures): SUM(DISTINCT N), MAX(DISTINCT N), AVG(DISTINCT N), COALESCE(N, 0, 1), COALESCE(1, 2), CAST(G AS INTEGER), LOWER(G ). --- ...0dc604892b2443238307bdb53d0f571fe26c.arrow | Bin 0 -> 674 bytes ...b0dc604892b2443238307bdb53d0f571fe26c.json | 88 ++++++++++++++++++ ...5e9e96f2b6fb94602d2673c193686be33db3.arrow | Bin 0 -> 1402 bytes ...85e9e96f2b6fb94602d2673c193686be33db3.json | 79 ++++++++++++++++ ...78a55fbd62d94b2db1a51bee058a5767f90b.arrow | Bin 0 -> 2818 bytes ...778a55fbd62d94b2db1a51bee058a5767f90b.json | 88 ++++++++++++++++++ ...dd4a075b86ce9c00f8f4fd95e7f226732dc0.arrow | Bin 0 -> 1426 bytes ...8dd4a075b86ce9c00f8f4fd95e7f226732dc0.json | 79 ++++++++++++++++ 8 files changed, 334 insertions(+) create mode 100644 evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.arrow create mode 100644 evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.json create mode 100644 evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.arrow create mode 100644 evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.json create mode 100644 evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.arrow create mode 100644 evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.json create mode 100644 evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.arrow create mode 100644 evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.json diff --git a/evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.arrow b/evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.arrow new file mode 100644 index 0000000000000000000000000000000000000000..5fc2bb60a616efec5376672f3a4d161df4bf8925 GIT binary patch literal 674 zcmeHFF$%&!5S&PiQADCxT3lhFkiuA5dW1uTkU$bh<3lWbg3s{@7XC_S*9d9+feX92 zy`7otE@#WKXcFK>+j&3)u@S}y&_~D<&GVUqS;*+OQBJ^vexdKQ0-UKsLSJ-Dd&duk z^sH!HxjLA&-uueGeegZOFZG^*HlwGustLE1IpwuGxRS)H;^FOCe|ETrx%4A8p{Iq- zT$Q?25}Pe~_YN<@oHGO%=u}ZVv!^}8viRKEZK literal 0 HcmV?d00001 diff --git a/evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.json b/evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.json new file mode 100644 index 000000000..57b4dc8d3 --- /dev/null +++ b/evidence/results/03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c.json @@ -0,0 +1,88 @@ +{ + "arrow_sha256": "261e58fbaa07677713c9cb5fb326e3b02de3c82241f71950914c9aa3f3796db1", + "case_hash": "03a4a84eb27f2f213174ea73ca1b0dc604892b2443238307bdb53d0f571fe26c", + "columns": [ + { + "internal_size": null, + "name": "COALESCE(N, 0, 1)", + "nullable": false, + "precision": 10, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "COALESCE(1, 2)", + "nullable": false, + "precision": 1, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "CAST(G AS INTEGER)", + "nullable": false, + "precision": 38, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": 134217728, + "name": "LOWER(G )", + "nullable": false, + "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": "2ac39cda67c648de58cfdcd77a9d876c15950427554c30e1b0beb30004789363", + "recorded_at": "2026-11-06T10:29:57.611560+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 0, + "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/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.arrow b/evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.arrow new file mode 100644 index 0000000000000000000000000000000000000000..59e95046c92940a70d3e823d70eeaeac9afcc515 GIT binary patch literal 1402 zcmeHH!Ab&A6usuClMN&xVbH=wgh2}pYcE+LsDV!9)@0HFQ&BfUixz!Dv~1ZA1bu>w zKBT2zke%~p2A@In0d?TMckg@m-Fx4;Z>CVIonPfd=76~(k`N*;mP92YCNv;qz!q30 zM#CRP!myq~B4tPly2u7*6DMQJKOsr15TRHSV+wwZu+1f49a6YF%M?p>yHq{4)73qk z6`4YwA$JfPvJWvJFQ4-{>!XfPYHt?a9D_{;v7pI+8o$ZA7joW2fx&+T)JM zO!Hx!EQCI-PIDOeB*BNC>k#`9$f(uqGEZLHXPyH%u$IBlX}Ke}--F(O5AV%-dG@r_ zI4NRYgLM0C7ok+u_wd{DeWuXK7jvf}JWG(PN};jD)f;?wvCjPDT4$c;gE$#zdYt-2 zWhst{Zn3NEO%M;E=O3_HKViwH{~|2aTL~Mg=j6^q9JUvST(Wnpf&2zwZ-0VUZ}9#W zUfo%0t3LDp-_>9EuCjOs1@+JU0$|0<*Z=?k literal 0 HcmV?d00001 diff --git a/evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.json b/evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.json new file mode 100644 index 000000000..dbcc84dce --- /dev/null +++ b/evidence/results/1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3.json @@ -0,0 +1,79 @@ +{ + "arrow_sha256": "3167a8fc8b27f58e1797eacbca6add084b26b8e8f531218469c8fe6837c7fd69", + "case_hash": "1751713bd19a381c5e374dc282685e9e96f2b6fb94602d2673c193686be33db3", + "columns": [ + { + "internal_size": null, + "name": "SUM(DISTINCT N)", + "nullable": true, + "precision": 22, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "MAX(DISTINCT N)", + "nullable": true, + "precision": 10, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "AVG(DISTINCT N)", + "nullable": true, + "precision": 25, + "scale": 5, + "type_code": 0, + "type_name": "FIXED" + } + ], + "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": "081e444da48e148bd71ec3025d989f434804f35538bfadd3888e363beb070d94", + "recorded_at": "2026-11-06T10:29:57.611560+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/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.arrow b/evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.arrow new file mode 100644 index 0000000000000000000000000000000000000000..5382dbee46498b633688104374f22c7894fa9872 GIT binary patch literal 2818 zcmeHJy>1gh5FRII#~2G46rmK6xB?*~8OdiOl7hmPIYJ~WM)pZbkpg3x*plT$NeUuS zQc$Ewg(xW~DO^zS1c(Pn!DCRm0FLjw+kwkDmh%7}Yi?$CW@mT5*&kP%&BnGP;)8mY zNKQx@$x2F+l713Utsf_K3MF!Qee@_o*QI?{*-X8%|HVSC`69CV@YB8IscL!VQASaR0Z zO6$-$@0TaX-FkPpcia32?evfO z`$Ons#1I3{jqJHQT!LH&5BB%^NENKY0t)nt+yYK%SW5xcIax*vDY3*ncMX;{n?Ur{ zsx1#tR$KPwmgjD`O@K;Fzk@%5-+(!&VtV>}Hm0LU4A+3k$T)^vbp|MM?f1ZnrA%Nc3(RL5ELVeAksFY;Mz!v? zYHr>s*rgdbo=4_BZEucaF*srqdvXVD2^{;evT90i*FHg6&%ypd1+wA zGYd*kavxIauH<}aQtG~9!%gr%47Un)v0yuZ5q-0$On7s!Hmo%XQ}TH~a9*UW+(!L) z<7DvzEn307@s)Z&8Q5Vi?zPWg#V+)R>sCE3E2crE`qd84pvW`hU_D6BLm?F-&x}<6 z2V?6xM#kn|RAO$;A8k{eQvI4YZoiKPLyy}rx7;DX5lVmr#**MM@Nb|5W zYpQG1bvq@e^q?HpnK7(Y7S&D#70#LQE#Y0A2eUsX_}VPIXn^=%e>g$&H)#F_@jaiv dK^NZ+4`+k-e)Rv*{0*A^2A$!asRsXj{{s1_m^J_a literal 0 HcmV?d00001 diff --git a/evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.json b/evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.json new file mode 100644 index 000000000..e28a1e04c --- /dev/null +++ b/evidence/results/337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b.json @@ -0,0 +1,88 @@ +{ + "arrow_sha256": "ea71f02f9f2089868f312d773d4447f76b2ac6753d467d4b79f599dedf0048b8", + "case_hash": "337b2509d2c7ab371940e49d02b778a55fbd62d94b2db1a51bee058a5767f90b", + "columns": [ + { + "internal_size": null, + "name": "COALESCE(N, 0, 1)", + "nullable": false, + "precision": 10, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "COALESCE(1, 2)", + "nullable": false, + "precision": 1, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "CAST(G AS INTEGER)", + "nullable": true, + "precision": 38, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": 134217728, + "name": "LOWER(G )", + "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": "d372a413c8acce29934a82ca7fe445a0daa1c1f993c109e548f4da8723445c13", + "recorded_at": "2026-11-06T10:29:57.611560+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 4, + "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/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.arrow b/evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.arrow new file mode 100644 index 0000000000000000000000000000000000000000..765c458c74b315d3dc4379b6aa755f200e2f4885 GIT binary patch literal 1426 zcmeHH%}N4M6h7vtlMN&xVbH=wgh2}pi?&1dBdCE+<@wWy>C+M{v=D zwDbv_zHeqOjzZAB4xD?=x##De`<**u*6X(q1rZmdZ-}IXloUha5)&O7D5Aj@SUOfM zSVf|+ULztEa0a@_4y2CEgcqNZGg-bajdY#{%UDCPM!A)lOnt7lDzHuTR>46Y-EGu}qzaXPUpcE2;}ip)bX ztdj>brrm9gR7@IpgwHyKKL#GRTRomr4EcG^2?E&5aAdcgu`}pHZz6{GX1~n6DmO1n zkXzv1pyQyF*YykH_JWuhbmGOcvtZ_uh*i~Wu3+_n*hB2|d}19&p82Xy4qCWQa(3*2_$uOT6{&rmN%H$u z?`WBq^}I`eKCo5(`I?_5-=*^ZRxW}%>*A;V^*Ch{H|o5P0rcWP)qutChf^qGJ@7QY a#|(!Dn)^bQc>aHHf9ZS6;~kjZzxW%%)UPuD literal 0 HcmV?d00001 diff --git a/evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.json b/evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.json new file mode 100644 index 000000000..8740f3889 --- /dev/null +++ b/evidence/results/c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0.json @@ -0,0 +1,79 @@ +{ + "arrow_sha256": "1324d64781cde97814e1b890605b500a7c0c4b8fbdbfe859fd267f02c1e12e39", + "case_hash": "c6a1f49638c1bc38bfe62156c5c8dd4a075b86ce9c00f8f4fd95e7f226732dc0", + "columns": [ + { + "internal_size": null, + "name": "SUM(DISTINCT N)", + "nullable": true, + "precision": 22, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "MAX(DISTINCT N)", + "nullable": true, + "precision": 10, + "scale": 2, + "type_code": 0, + "type_name": "FIXED" + }, + { + "internal_size": null, + "name": "AVG(DISTINCT N)", + "nullable": true, + "precision": 25, + "scale": 5, + "type_code": 0, + "type_name": "FIXED" + } + ], + "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": "7f69572c9d26a20edf37c2e0203592591bb666431de392db3ee6fe5a31d74a00", + "recorded_at": "2026-11-06T10:29:57.611560+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 f4615b676e3c1516aba236b9d6acdc21ea9b0347 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:01:21 +0000 Subject: [PATCH 5/9] feat: name unaliased ROUND and current-time items as Snowflake receipts show ROUND of a column with an optional integer scale, CURRENT_DATE, CURRENT_DATE() and CURRENT_TIMESTAMP() now take Snowflake's recorded names. CURRENT_DATE and CURRENT_TIMESTAMP convert to DuckDB calls, so a select item aliased as the keyword no longer captures them. The recorded DISTINCT aggregate and row-expression names are pinned too. --- .../src/diagnostics/constants.rs | 5 +- .../sqlquack-duckdb/src/temporal/constants.rs | 4 ++ .../expressions/temporal_functions.rs | 5 +- .../_helpers/queries/output_names.rs | 12 +++- .../src/conversion/constants.rs | 5 ++ .../sqlquack/tests/cli/test_output_names.rs | 57 +++++++++++++++++-- .../docs/docs/diagnostics/reference.mdx | 2 +- 7 files changed, 80 insertions(+), 10 deletions(-) diff --git a/crates/sqlquack-core/src/diagnostics/constants.rs b/crates/sqlquack-core/src/diagnostics/constants.rs index d150544c6..3ce92755f 100644 --- a/crates/sqlquack-core/src/diagnostics/constants.rs +++ b/crates/sqlquack-core/src/diagnostics/constants.rs @@ -175,8 +175,9 @@ pub const CODES: &[CodeEntry] = &[ a converted query must return the same column names. SQLQuack emits Snowflake's name \ only for the shapes recorded results verify, named by their text upper-cased as \ written: literals, columns and COUNT, SUM, MAX, AVG, LOWER, UPPER and COALESCE of \ - columns, a column plus, minus, times, divided by or concatenated with a literal, and \ - a column cast to INTEGER, written with single spaces and at most one line break. \ + columns, ROUND of a column, a column plus, minus, times, divided by or concatenated \ + with a literal, a column cast to INTEGER, and CURRENT_DATE, CURRENT_DATE() or \ + CURRENT_TIMESTAMP(), written with single spaces and at most one line break. \ Other items, such as paths, CASE, nested calls or other functions, are refused \ rather than converted with a different column name. An alias fixes the name in both \ engines.", diff --git a/crates/sqlquack-duckdb/src/temporal/constants.rs b/crates/sqlquack-duckdb/src/temporal/constants.rs index 8e20055df..8380f8848 100644 --- a/crates/sqlquack-duckdb/src/temporal/constants.rs +++ b/crates/sqlquack-duckdb/src/temporal/constants.rs @@ -166,3 +166,7 @@ pub const DATE_FROM_PARTS: &str = "duckdb.date_from_parts"; pub const TIMESTAMP_FROM_PARTS: &str = "duckdb.timestamp_from_parts"; /// The dates whose every instant DuckDB's nanosecond timestamps hold. pub const NANOSECOND_DATE_RANGE: (&str, &str) = ("1677-09-22", "2262-04-10"); +/// DuckDB's current date as a call, which a select item's alias `CURRENT_DATE` cannot capture. +pub const DUCKDB_CURRENT_DATE: &str = "current_date()"; +/// DuckDB's current timestamp as a call, which an alias `CURRENT_TIMESTAMP` cannot capture. +pub const DUCKDB_CURRENT_TIMESTAMP: &str = "get_current_timestamp()"; diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs index 3cee47429..02be537ff 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs @@ -68,6 +68,7 @@ use crate::conversion::constants::{ UTC_OFFSET_ARITHMETIC, UTC_ZONE, ZONE_COLUMNS, ZONED_PARTS, }; use sqlquack_duckdb::raises::main::raised::raised; +use sqlquack_duckdb::temporal::constants::{DUCKDB_CURRENT_DATE, DUCKDB_CURRENT_TIMESTAMP}; use sqlquack_policy::policy::constants::{ CURRENT_TIME, DATE_TEXT_SPELLING, SESSION_TIMEZONE_UTC, TIME_ZONE_SPELLING, TIMESTAMP_RANGE, }; @@ -1156,9 +1157,9 @@ pub(crate) fn current_time( diverge(site.context, CURRENT_TIME, construct)?; diverge(site.context, SESSION_TIMEZONE_UTC, construct)?; Ok(match name { - "CURRENT_DATE" => Emitted::new("current_date".to_owned(), SnowflakeType::Date), + "CURRENT_DATE" => Emitted::new(DUCKDB_CURRENT_DATE.to_owned(), SnowflakeType::Date), _ => Emitted::new( - "current_timestamp".to_owned(), + DUCKDB_CURRENT_TIMESTAMP.to_owned(), SnowflakeType::TimestampLtz { precision: 9 }, ), }) diff --git a/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs b/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs index b4904c54e..0df3b3ca3 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs @@ -7,7 +7,7 @@ use crate::conversion::_helpers::support::tree::{ }; use crate::conversion::constants::{ OUTPUT_NAMED_AGGREGATES, OUTPUT_NAMED_ARITHMETIC, OUTPUT_NAMED_CAST_ENDINGS, - OUTPUT_NAMED_FUNCTIONS, + OUTPUT_NAMED_CURRENT_TIME, OUTPUT_NAMED_FUNCTIONS, OUTPUT_NAMED_ROUND, }; /// Whether an unaliased item written as `text` has a shape whose name receipts record. @@ -36,6 +36,16 @@ pub(crate) fn recorded_name_shape(item: &Value, text: &str) -> bool { || (named(&["COUNT"]) && matches!(node_kind(argument), Some(("star", _)))) }) } + "function" if arguments.is_empty() => { + only(&["name", "args", "no_parens"]) + && OUTPUT_NAMED_CURRENT_TIME.contains(&text.to_ascii_uppercase().as_str()) + } + "function" if named(&[OUTPUT_NAMED_ROUND]) => { + only(&["name", "args"]) + && arguments.get(2).is_none() + && arguments.first().is_some_and(plain_column) + && arguments.iter().skip(1).all(integer_literal) + } "function" => { only(&["name", "args"]) && named(OUTPUT_NAMED_FUNCTIONS) diff --git a/crates/sqlquack-rules/src/conversion/constants.rs b/crates/sqlquack-rules/src/conversion/constants.rs index 006eb3e0f..f23198c03 100644 --- a/crates/sqlquack-rules/src/conversion/constants.rs +++ b/crates/sqlquack-rules/src/conversion/constants.rs @@ -1990,3 +1990,8 @@ pub(crate) const OUTPUT_NAMED_FUNCTIONS: &[&str] = &["LOWER", "UPPER", "COALESCE pub(crate) const OUTPUT_NAMED_ARITHMETIC: &[&str] = &["add", "sub", "mul", "div"]; /// The cast target spellings whose unaliased column names receipts record. pub(crate) const OUTPUT_NAMED_CAST_ENDINGS: &[&str] = &[" AS INTEGER)", "::INTEGER"]; +/// ROUND, whose unaliased column names receipts record for a column and an integer scale. +pub(crate) const OUTPUT_NAMED_ROUND: &str = "ROUND"; +/// The current-time items, as written upper-cased, whose unaliased column names receipts record. +pub(crate) const OUTPUT_NAMED_CURRENT_TIME: &[&str] = + &["CURRENT_DATE()", "CURRENT_DATE", "CURRENT_TIMESTAMP()"]; diff --git a/crates/sqlquack/tests/cli/test_output_names.rs b/crates/sqlquack/tests/cli/test_output_names.rs index 766fbb11c..a25edd822 100644 --- a/crates/sqlquack/tests/cli/test_output_names.rs +++ b/crates/sqlquack/tests/cli/test_output_names.rs @@ -23,6 +23,48 @@ fn given_unaliased_items_when_converted_and_run_on_duckdb_then_they_take_snowfla flags: &[], expected_names: vec!["N\n + 1".to_owned()], }, + ColumnNameTestCase { + description: "ROUND and the current-time items, named as recorded", + sql: "SELECT ROUND(n, 1), ROUND(n), round( n, 0 ), current_date(), CURRENT_DATE, \ + CURRENT_TIMESTAMP() FROM (SELECT 1.25 AS n) AS input", + flags: &[], + expected_names: vec![ + "ROUND(N, 1)".to_owned(), + "ROUND(N)".to_owned(), + "ROUND( N, 0 )".to_owned(), + "CURRENT_DATE()".to_owned(), + "CURRENT_DATE".to_owned(), + "CURRENT_TIMESTAMP()".to_owned(), + ], + }, + ColumnNameTestCase { + description: "an alias spelled as the current-time keyword runs on DuckDB", + sql: "SELECT CURRENT_DATE() AS current_date, CURRENT_TIMESTAMP() AS current_timestamp", + flags: &[], + expected_names: vec!["CURRENT_DATE".to_owned(), "CURRENT_TIMESTAMP".to_owned()], + }, + ColumnNameTestCase { + description: "DISTINCT aggregates and row expressions, named as recorded", + sql: "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM (SELECT 1.25 AS n) AS input", + flags: &[], + expected_names: vec![ + "SUM(DISTINCT N)".to_owned(), + "MAX(DISTINCT N)".to_owned(), + "AVG(DISTINCT N)".to_owned(), + ], + }, + ColumnNameTestCase { + description: "COALESCE of three and of numbers, a text cast and a spaced call, as recorded", + sql: "SELECT COALESCE(n, 0, 1), COALESCE(1, 2), CAST(g AS INTEGER), lower(g ) \ + FROM (SELECT 1.25 AS n, '7' AS g) AS input", + flags: &[], + expected_names: vec![ + "COALESCE(N, 0, 1)".to_owned(), + "COALESCE(1, 2)".to_owned(), + "CAST(G AS INTEGER)".to_owned(), + "LOWER(G )".to_owned(), + ], + }, ColumnNameTestCase { description: "numbers keep their written form, upper-cased", sql: "SELECT 1.50, 1e3, -2, NULL, false", @@ -232,15 +274,22 @@ fn given_unaliased_items_no_receipt_names_when_converting_then_each_refuses_with expected_outcome: convert_outcome(1, false), }, ConvertTestCase { - description: "ROUND, which no receipt names yet refuses with SQ0009", - sql: "SELECT ROUND(n, 1) FROM input", + description: "ROUND of an expression, which no receipt names, refuses with SQ0009", + sql: "SELECT ROUND(n + 1, 1) FROM input", + flags: &["--schema", UNALIASED_SCHEMA], + mention: UNVERIFIED_NAME, + expected_outcome: convert_outcome(1, false), + }, + ConvertTestCase { + description: "CURRENT_TIMESTAMP without parentheses, which no receipt names, refuses with SQ0009", + sql: "SELECT CURRENT_TIMESTAMP FROM input", flags: &["--schema", UNALIASED_SCHEMA], mention: UNVERIFIED_NAME, expected_outcome: convert_outcome(1, false), }, ConvertTestCase { - description: "CURRENT_DATE(), which no receipt names yet refuses with SQ0009", - sql: "SELECT CURRENT_DATE() FROM input", + description: "CURRENT_DATE( ) with a space, which no receipt names, refuses with SQ0009", + sql: "SELECT CURRENT_DATE( ) FROM input", flags: &["--schema", UNALIASED_SCHEMA], mention: UNVERIFIED_NAME, expected_outcome: convert_outcome(1, false), diff --git a/website/src/content/docs/docs/diagnostics/reference.mdx b/website/src/content/docs/docs/diagnostics/reference.mdx index 9b0f820af..3b0814f90 100644 --- a/website/src/content/docs/docs/diagnostics/reference.mdx +++ b/website/src/content/docs/docs/diagnostics/reference.mdx @@ -140,7 +140,7 @@ echo "SELECT HASH(1) AS h" | sqlquack convert --builtin-policy **this column has no alias, and SQLQuack has not verified the name Snowflake gives it** (`unaliased_output_name`) -Snowflake names an unaliased select item after the expression's text, and a converted query must return the same column names. SQLQuack emits Snowflake's name only for the shapes recorded results verify, named by their text upper-cased as written: literals, columns and COUNT, SUM, MAX, AVG, LOWER, UPPER and COALESCE of columns, a column plus, minus, times, divided by or concatenated with a literal, and a column cast to INTEGER, written with single spaces and at most one line break. Other items, such as paths, CASE, nested calls or other functions, are refused rather than converted with a different column name. An alias fixes the name in both engines. +Snowflake names an unaliased select item after the expression's text, and a converted query must return the same column names. SQLQuack emits Snowflake's name only for the shapes recorded results verify, named by their text upper-cased as written: literals, columns and COUNT, SUM, MAX, AVG, LOWER, UPPER and COALESCE of columns, ROUND of a column, a column plus, minus, times, divided by or concatenated with a literal, a column cast to INTEGER, and CURRENT_DATE, CURRENT_DATE() or CURRENT_TIMESTAMP(), written with single spaces and at most one line break. Other items, such as paths, CASE, nested calls or other functions, are refused rather than converted with a different column name. An alias fixes the name in both engines. **Help:** add an alias, e.g. `AS one` From 2bbf41f1bb7bb9a1274dd95f4eac06a0fd2688da Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:32:45 +0000 Subject: [PATCH 6/9] fix: refuse AVG(DISTINCT) of a NUMBER, which Snowflake types differently Snowflake types AVG(DISTINCT n) of NUMBER(10,2) as NUMBER(25,5), three more decimal places rather than six, and no recording shows how it rounds. The current-time names case becomes typing evidence, since its values read the clock. --- cases/relational/unaliased_round_current.toml | 4 +-- .../sqlquack-rules/src/catalogue/constants.rs | 2 +- .../conversion/_helpers/expressions/calls.rs | 8 +++++ .../sqlquack/tests/cli/test_output_names.rs | 29 ++++++++++++++----- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/cases/relational/unaliased_round_current.toml b/cases/relational/unaliased_round_current.toml index 575b8ccb5..652671265 100644 --- a/cases/relational/unaliased_round_current.toml +++ b/cases/relational/unaliased_round_current.toml @@ -31,14 +31,14 @@ tags = ["empty-input"] [[case]] id = "unaliased_current_time" -description = "Unaliased CURRENT_DATE(), CURRENT_DATE and CURRENT_TIMESTAMP(): only the column names and types are the receipt; the values read the clock." +description = "Unaliased CURRENT_DATE(), CURRENT_DATE and CURRENT_TIMESTAMP(): only the column names and types are the receipt; the values read the clock, so it is typing evidence." +evidence = "typing" columns = [{ name = "n", type = "NUMBER(10,2)" }] rows = [ ["1.00"], ] query = "SELECT CURRENT_DATE(), CURRENT_DATE, CURRENT_TIMESTAMP() FROM input" order = "ordered" -expected_divergence = "current_time" tags = ["boundary"] [[case]] diff --git a/crates/sqlquack-rules/src/catalogue/constants.rs b/crates/sqlquack-rules/src/catalogue/constants.rs index 956582135..3496a47a8 100644 --- a/crates/sqlquack-rules/src/catalogue/constants.rs +++ b/crates/sqlquack-rules/src/catalogue/constants.rs @@ -1250,7 +1250,7 @@ pub const RULES: &[RuleSpec] = &[ RuleSpec { id: AVERAGE, constructs: &["fn:AVG"], - conditions: "A NUMBER of scale at most 6 and precision at most 26.", + conditions: "A NUMBER of scale at most 6 and precision at most 26. AVG(DISTINCT) of a NUMBER only over a window, since Snowflake types the aggregate with 3 more decimal places and no recording shows how it rounds.", failures: &[FailureMode { kind: ErrorKind::NumericOverflow, snowflake: &[NUMBER_OUT_OF_RANGE], diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs index ea2be5126..dfbff2740 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs @@ -488,6 +488,14 @@ impl AggregateCall<'_> { let _rule = log.rule(AVERAGE); let refuse = |reason: String| outside_conditions(node_construct("avg"), reason); let argument = self.argument("avg")?; + if !self.framed + && flag(self.payload, "distinct") + && matches!(argument.data_type, SnowflakeType::Number { .. }) + { + return Err(refuse( + "AVG(DISTINCT) of a NUMBER, which Snowflake types with 3 more decimal places, not 6, and no recording shows how it rounds".to_owned(), + )); + } let total = sum_type(argument.data_type, self.site.context, "avg").map_err(refuse)?; let data_type = match self.framed { true => framed_average(argument.data_type, log), diff --git a/crates/sqlquack/tests/cli/test_output_names.rs b/crates/sqlquack/tests/cli/test_output_names.rs index a25edd822..cc64a44f0 100644 --- a/crates/sqlquack/tests/cli/test_output_names.rs +++ b/crates/sqlquack/tests/cli/test_output_names.rs @@ -44,14 +44,10 @@ fn given_unaliased_items_when_converted_and_run_on_duckdb_then_they_take_snowfla expected_names: vec!["CURRENT_DATE".to_owned(), "CURRENT_TIMESTAMP".to_owned()], }, ColumnNameTestCase { - description: "DISTINCT aggregates and row expressions, named as recorded", - sql: "SELECT SUM(DISTINCT n), MAX(DISTINCT n), AVG(DISTINCT n) FROM (SELECT 1.25 AS n) AS input", + description: "DISTINCT aggregates, named as recorded", + sql: "SELECT SUM(DISTINCT n), MAX(DISTINCT n) FROM (SELECT 1.25 AS n) AS input", flags: &[], - expected_names: vec![ - "SUM(DISTINCT N)".to_owned(), - "MAX(DISTINCT N)".to_owned(), - "AVG(DISTINCT N)".to_owned(), - ], + expected_names: vec!["SUM(DISTINCT N)".to_owned(), "MAX(DISTINCT N)".to_owned()], }, ColumnNameTestCase { description: "COALESCE of three and of numbers, a text cast and a spaced call, as recorded", @@ -88,6 +84,25 @@ fn given_unaliased_items_when_converted_and_run_on_duckdb_then_they_take_snowfla } } +#[test] +fn given_avg_distinct_of_a_number_when_converting_then_it_refuses() { + let test_cases = [ConvertTestCase { + description: "AVG(DISTINCT) of a NUMBER, which Snowflake types NUMBER(25,5) for NUMBER(10,2), refuses", + sql: "SELECT AVG(DISTINCT n) AS a FROM (SELECT 1.25 AS n) AS input", + flags: &[], + mention: "AVG(DISTINCT) of a NUMBER", + expected_outcome: convert_outcome(1, false), + }]; + for test_case in test_cases { + assert_eq!( + run_convert(test_case.sql, test_case.flags, test_case.mention), + test_case.expected_outcome, + "{}", + test_case.description + ); + } +} + #[test] fn given_unaliased_items_no_receipt_names_when_converting_then_each_refuses_with_sq0009() { let test_cases = [ From 3472c26423bb2d68ad23de24ccf2417c35bf131d Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:15:47 +0000 Subject: [PATCH 7/9] chore(coverage): record the unaliased ROUND, current-time and DISTINCT aggregate statements --- coverage/translation/results-baseline.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coverage/translation/results-baseline.json b/coverage/translation/results-baseline.json index c891a50dd..6168cc9bc 100644 --- a/coverage/translation/results-baseline.json +++ b/coverage/translation/results-baseline.json @@ -6718,6 +6718,13 @@ "relational/unaliased_output_names.toml#unaliased_group_count": "matching", "relational/unaliased_output_names.toml#unaliased_literals": "matching", "relational/unaliased_output_names.toml#unaliased_over_no_rows": "matching", + "relational/unaliased_round_current.toml#unaliased_current_time": "shape_only", + "relational/unaliased_round_current.toml#unaliased_distinct_aggregates": "refused", + "relational/unaliased_round_current.toml#unaliased_distinct_aggregates_no_rows": "refused", + "relational/unaliased_round_current.toml#unaliased_round": "matching", + "relational/unaliased_round_current.toml#unaliased_round_no_rows": "matching", + "relational/unaliased_round_current.toml#unaliased_row_expressions": "matching", + "relational/unaliased_round_current.toml#unaliased_row_expressions_no_rows": "matching", "relational/union_by_name.toml#union_all_by_name_reordered": "matching", "relational/union_by_name.toml#union_by_name_case_insensitive": "matching", "relational/union_by_name.toml#union_by_name_chain": "matching", From 264c7987d024b316f4702ce3a69ed37113a6be52 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:56:53 +0000 Subject: [PATCH 8/9] docs(rules): say AVG(DISTINCT) of a NUMBER is refused --- crates/sqlquack-rules/src/catalogue/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sqlquack-rules/src/catalogue/constants.rs b/crates/sqlquack-rules/src/catalogue/constants.rs index 3496a47a8..1e8afaf39 100644 --- a/crates/sqlquack-rules/src/catalogue/constants.rs +++ b/crates/sqlquack-rules/src/catalogue/constants.rs @@ -1250,7 +1250,7 @@ pub const RULES: &[RuleSpec] = &[ RuleSpec { id: AVERAGE, constructs: &["fn:AVG"], - conditions: "A NUMBER of scale at most 6 and precision at most 26. AVG(DISTINCT) of a NUMBER only over a window, since Snowflake types the aggregate with 3 more decimal places and no recording shows how it rounds.", + conditions: "A NUMBER of scale at most 6 and precision at most 26. AVG(DISTINCT) of a NUMBER is refused, since Snowflake types it with 3 more decimal places and no recording shows how it rounds.", failures: &[FailureMode { kind: ErrorKind::NumericOverflow, snowflake: &[NUMBER_OUT_OF_RANGE], From cce77acdccde175294c96287ff2daa643dd862fd Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:13:24 +0000 Subject: [PATCH 9/9] refactor: match the ROUND and current-time names where they are read With main merged, the conversion constants file reached 2,005 lines, over the 2,000-line limit. The names sit in output_names.rs as match literals, as calls.rs writes them. --- .../src/conversion/_helpers/queries/output_names.rs | 9 ++++++--- crates/sqlquack-rules/src/conversion/constants.rs | 5 ----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs b/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs index 0df3b3ca3..03e9577e7 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/queries/output_names.rs @@ -7,7 +7,7 @@ use crate::conversion::_helpers::support::tree::{ }; use crate::conversion::constants::{ OUTPUT_NAMED_AGGREGATES, OUTPUT_NAMED_ARITHMETIC, OUTPUT_NAMED_CAST_ENDINGS, - OUTPUT_NAMED_CURRENT_TIME, OUTPUT_NAMED_FUNCTIONS, OUTPUT_NAMED_ROUND, + OUTPUT_NAMED_FUNCTIONS, }; /// Whether an unaliased item written as `text` has a shape whose name receipts record. @@ -38,9 +38,12 @@ pub(crate) fn recorded_name_shape(item: &Value, text: &str) -> bool { } "function" if arguments.is_empty() => { only(&["name", "args", "no_parens"]) - && OUTPUT_NAMED_CURRENT_TIME.contains(&text.to_ascii_uppercase().as_str()) + && matches!( + text.to_ascii_uppercase().as_str(), + "CURRENT_DATE()" | "CURRENT_DATE" | "CURRENT_TIMESTAMP()" + ) } - "function" if named(&[OUTPUT_NAMED_ROUND]) => { + "function" if named(&["ROUND"]) => { only(&["name", "args"]) && arguments.get(2).is_none() && arguments.first().is_some_and(plain_column) diff --git a/crates/sqlquack-rules/src/conversion/constants.rs b/crates/sqlquack-rules/src/conversion/constants.rs index 412af428e..c0a81f28c 100644 --- a/crates/sqlquack-rules/src/conversion/constants.rs +++ b/crates/sqlquack-rules/src/conversion/constants.rs @@ -1998,8 +1998,3 @@ pub(crate) const OUTPUT_NAMED_FUNCTIONS: &[&str] = &["LOWER", "UPPER", "COALESCE pub(crate) const OUTPUT_NAMED_ARITHMETIC: &[&str] = &["add", "sub", "mul", "div"]; /// The cast target spellings whose unaliased column names receipts record. pub(crate) const OUTPUT_NAMED_CAST_ENDINGS: &[&str] = &[" AS INTEGER)", "::INTEGER"]; -/// ROUND, whose unaliased column names receipts record for a column and an integer scale. -pub(crate) const OUTPUT_NAMED_ROUND: &str = "ROUND"; -/// The current-time items, as written upper-cased, whose unaliased column names receipts record. -pub(crate) const OUTPUT_NAMED_CURRENT_TIME: &[&str] = - &["CURRENT_DATE()", "CURRENT_DATE", "CURRENT_TIMESTAMP()"];