From 2541bfd0b48faf3cb88235841662ab113ce7c658 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:12:28 +0000 Subject: [PATCH 1/9] Refuse a timestamp type name other than TIMESTAMP before a string Snowflake takes only DATE, TIME and TIMESTAMP as typed literals. It rejects TIMESTAMP_NTZ, TIMESTAMP_LTZ, TIMESTAMP_TZ, their spellings without underscores and DATETIME before a string with a syntax error (errno 1003), and with a precision as an unsupported literal (errno 2121), but the named-type literal rule read each as a cast that then converted. The parser now refuses them at the string, pointing to '...':: or the TO_ function. The date-time constant form gains a negative neighbour for each, still to capture. --- coverage/syntax/cases/expressions.jsonl | 9 ++ coverage/syntax/limits.toml | 86 +++++++++++++++++++ coverage/syntax/obligations/expressions.toml | 72 ++++++++++++++++ .../parser/_helpers/expressions/primary.rs | 6 ++ .../src/parser/_helpers/types/data_types.rs | 15 ++++ .../sqlquack-syntax/src/parser/constants.rs | 11 +++ .../tests/parser/test_snowflake_recorded.rs | 66 ++++++++++++++ 7 files changed, 265 insertions(+) diff --git a/coverage/syntax/cases/expressions.jsonl b/coverage/syntax/cases/expressions.jsonl index df23e38ff..7a7c286df 100644 --- a/coverage/syntax/cases/expressions.jsonl +++ b/coverage/syntax/cases/expressions.jsonl @@ -56,11 +56,20 @@ {"capture":"execute","case_id":"sf.expressions.commands-model-function.model-version-or-alias.kec6e75a2820d","expect":"accept","obligations":["sf.expressions.commands-model-function.model-version-or-alias.o023"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT MODEL ( m1 , v1 )!\"SELECT\" ( 1 ) FROM t1 ;","sql_sha256":"ec6e75a2820de5b6f23ce1b22e835119ef4f07ee312c041ac2c1e402f7ba12e3","stage":"semantic"} {"capture":"execute","case_id":"sf.expressions.commands-model-function.model-version-or-alias.kec9073852d8f","expect":"reject","obligations":["sf.expressions.commands-model-function.model-version-or-alias.o019"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT MODEL ( SELECT , v1 )!predict ( 1 ) FROM t1 ;","sql_sha256":"ec9073852d8fddcf064cf0b7b6a5780be8be50912b23502fc52f8f6993675b00"} {"capture":"execute","case_id":"sf.expressions.commands-model-function.model-version-or-alias.kfa76fb256113","expect":"reject","obligations":["sf.expressions.commands-model-function.model-version-or-alias.o028"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT MODEL ( m1 , v1 )!predict 1 ) FROM t1 ;","sql_sha256":"fa76fb25611301afa1737408ad93d63ed2b6a1ee4ecad1f8a4d2e1a0b842af43"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k1a7982b7fb16","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o010"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_LTZ '2026-01-02 03:04:05' ;","sql_sha256":"1a7982b7fb162e7e1b9bed4697be078aa7e87e01fc6c916cc4340f2b62a5ff3d"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k45cf89eb2789","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o001","sf.expressions.data-types-datetime.date-time-constant.o002","sf.expressions.data-types-datetime.date-time-constant.o005"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATE '2026-01-01' ;","sql_sha256":"45cf89eb278932964361b3f4be3fbd806c5c2929b9978ba4abfdb4c632c68d30"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k4f7f4f66d4b6","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o006"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATE m1 ;","sql_sha256":"4f7f4f66d4b6e0387d2ff68d6366e031522c5b8dfcdecb66bec229bf079c60ee","stage":"semantic"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k5ee293529f1e","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o014"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMPTZ '2026-01-02 03:04:05' ;","sql_sha256":"5ee293529f1e0d950714db52afe68643c1932954fb352248b646860b14154b43"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k624d77c24c5c","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o012"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMPNTZ '2026-01-02 03:04:05' ;","sql_sha256":"624d77c24c5c78d9f76e7f5e9abc8334b1b11c008dafd006422a1c8d5ea43666"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k69fcf5144724","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o011"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_TZ '2026-01-02 03:04:05' ;","sql_sha256":"69fcf514472412f23dfd971799b928236fa3618f86a7a8b604e7de1e5d939a86"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k6cb397c64afd","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o003"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIME '2026-01-01' ;","sql_sha256":"6cb397c64afdf4c04273d895883d32343a4adfb58a7f99b016e10cb5728fa1f1","stage":"runtime"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k8a36862cebb9","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o009"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_NTZ '2026-01-02 03:04:05' ;","sql_sha256":"8a36862cebb9e2362f3742fed6479b8e95bf7e1d9cb0ff5fc2fa6c2b9a6f6c4e"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k8b2b51653394","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o008"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATE ;","sql_sha256":"8b2b5165339438e2b5d8525374f49aeca053fb2e1a17f63f41693700b8e3729c","stage":"semantic"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kaf2e829caa1c","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o013"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMPLTZ '2026-01-02 03:04:05' ;","sql_sha256":"af2e829caa1c08836aeff567645c775cafe64e3889241fe93b64eae9c9228220"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kd5a5920217a9","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o004"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP '2026-01-01' ;","sql_sha256":"d5a5920217a99e21c7cac1cbd42b0b07e8e95102f23b7431f800ef677ac82b41"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kdf44dc074801","expect":"observe","obligations":["sf.expressions.data-types-datetime.date-time-constant.o016"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kf2531b90a1d4","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o015"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATETIME '2026-01-02 03:04:05' ;","sql_sha256":"f2531b90a1d45e1eefb1b10bd7904efccc0ebaf57b352e1176c960fdc52afe8d"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kfadd3f560bcd","expect":"observe","obligations":["sf.expressions.data-types-datetime.date-time-constant.o017"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date.k59a97475f0c8","expect":"accept","obligations":["sf.expressions.data-types-datetime.date.o001"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS DATE ) ;","sql_sha256":"59a97475f0c8b89764fe8721799bc7d86ac03d73a68936308a28bbf53bc4558b"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.datetime.k2fd95373b7e2","expect":"accept","obligations":["sf.expressions.data-types-datetime.datetime.o001"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS DATETIME ) ;","sql_sha256":"2fd95373b7e21051e0882240cf98b8bd7a71c503ddb7defdb1e68b48ca76a941"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.k0110f52d73ae","expect":"reject","obligations":["sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.o070"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS INTERVAL DAY TO SECOND ( 3 ) ;","sql_sha256":"0110f52d73ae53e7c4b41b54703550937b2521160a9274dd6e4513b53fb3633a"} diff --git a/coverage/syntax/limits.toml b/coverage/syntax/limits.toml index 46c0c3fad..739bf5155 100644 --- a/coverage/syntax/limits.toml +++ b/coverage/syntax/limits.toml @@ -82865,3 +82865,89 @@ sql = "DESC TRANSACTION SELECT ;" expect = "observe" context = "raw" +# Date and time constants: only DATE, TIME and TIMESTAMP take a string. The other timestamp names and +# DATETIME are recorded refused before one (errno 1003), and with a precision as unsupported literals +# (errno 2121), so those are observed. + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMP_NTZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMP_NTZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMP_LTZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMP_LTZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMP_TZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMP_TZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMPNTZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMPNTZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMPLTZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMPLTZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMPTZ in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMPTZ '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "DATETIME in place of DATE, TIME or TIMESTAMP" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT DATETIME '2026-01-02 03:04:05' ;" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMP_NTZ(3) in place of DATE, TIME or TIMESTAMP, observed" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' ;" +expect = "observe" +context = "raw" + +[[limit]] +form = "sf.expressions.data-types-datetime.date-time-constant" +kind = "negative_neighbour" +target = "{ DATE | TIME | TIMESTAMP }" +note = "TIMESTAMP_TZ(3) in place of DATE, TIME or TIMESTAMP, observed" +anchor = "doc.data-types-datetime / Date and time constants" +sql = "SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' ;" +expect = "observe" +context = "raw" diff --git a/coverage/syntax/obligations/expressions.toml b/coverage/syntax/obligations/expressions.toml index 7ed83c688..72f47f1c8 100644 --- a/coverage/syntax/obligations/expressions.toml +++ b/coverage/syntax/obligations/expressions.toml @@ -484,6 +484,78 @@ target = "''" anchor = "doc.data-types-datetime / Date and time constants" note = "omit '', observed" +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o009" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMP_NTZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o010" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMP_LTZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o011" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMP_TZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o012" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMPNTZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o013" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMPLTZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o014" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMPTZ in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o015" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "DATETIME in place of DATE, TIME or TIMESTAMP" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o016" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMP_NTZ(3) in place of DATE, TIME or TIMESTAMP, observed" + +[[obligation]] +id = "sf.expressions.data-types-datetime.date-time-constant.o017" +kind = "negative_neighbour" +required = true +target = "{ DATE | TIME | TIMESTAMP }" +anchor = "doc.data-types-datetime / Date and time constants" +note = "TIMESTAMP_TZ(3) in place of DATE, TIME or TIMESTAMP, observed" + [[obligation]] id = "sf.expressions.data-types-datetime.datetime.o001" kind = "positive" diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs index 4940fbea1..f0eabf6b2 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs @@ -949,6 +949,12 @@ impl Parser { } fn parse_primary_datetime_literal(&mut self) -> Result { + if self.is_snowflake() && self.check_next(TokenType::String) { + let string = self.current + 1; + if let Some(error) = self.untyped_literal_error(string) { + return Err(error); + } + } let token = self.advance()?; let original_text = token.text.to_string(); if self.check(TokenType::String) { diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs index 55bfc3698..885e2f7db 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs @@ -132,6 +132,9 @@ impl Parser { let Some(end) = self.named_type_literal_end() else { return Ok(None); }; + if let Some(error) = self.untyped_literal_error(end) { + return Err(error); + } let parts: Vec = self.tokens[self.current..end] .iter() .map(|token| match token.token_type { @@ -161,6 +164,18 @@ impl Parser { )))) } + /// Snowflake's refusal of a timestamp type name before the string at `end`, placed at the string. + pub(crate) fn untyped_literal_error(&mut self, end: usize) -> Option { + let name = self.peek_text().to_ascii_uppercase(); + let (_, function) = crate::parser::constants::SNOWFLAKE_UNTYPED_LITERAL_TYPES + .iter() + .find(|(listed, _)| *listed == name)?; + self.current = end; + Some(self.parse_error(format!( + "Snowflake has no {name} literal; cast the string instead, as '...'::{name} or {function}('...')" + ))) + } + /// The position of the string after a type name and its non-empty balanced parameters, or none. fn named_type_literal_end(&self) -> Option { let mut end = self.current + 1; diff --git a/crates/sqlquack-syntax/src/parser/constants.rs b/crates/sqlquack-syntax/src/parser/constants.rs index a304c98d2..5eaaf1ad5 100644 --- a/crates/sqlquack-syntax/src/parser/constants.rs +++ b/crates/sqlquack-syntax/src/parser/constants.rs @@ -251,6 +251,17 @@ pub(crate) const SQLITE_RANGE_FOLLOWERS: [crate::tokens::models::TokenType; 9] = /// Snowflake types written before a string as a typed literal, beyond the shared ones. pub(crate) const SNOWFLAKE_LITERAL_TYPES: &[&str] = &["IPADDRESS"]; +/// Type names Snowflake refuses before a string, each with the conversion function to suggest. +pub(crate) const SNOWFLAKE_UNTYPED_LITERAL_TYPES: &[(&str, &str)] = &[ + ("TIMESTAMP_NTZ", "TO_TIMESTAMP_NTZ"), + ("TIMESTAMPNTZ", "TO_TIMESTAMP_NTZ"), + ("TIMESTAMP_LTZ", "TO_TIMESTAMP_LTZ"), + ("TIMESTAMPLTZ", "TO_TIMESTAMP_LTZ"), + ("TIMESTAMP_TZ", "TO_TIMESTAMP_TZ"), + ("TIMESTAMPTZ", "TO_TIMESTAMP_TZ"), + ("DATETIME", "TO_TIMESTAMP_NTZ"), +]; + /// Reserved words Snowflake is recorded refusing as an unquoted later part of a dotted name. pub(crate) const SNOWFLAKE_REFUSED_NAME_PARTS: &[&str] = &["SELECT"]; diff --git a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs index 6fb860578..648671b2b 100644 --- a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs @@ -590,6 +590,72 @@ fn given_recorded_snowflake_cases_when_parsing_then_acceptance_matches_snowflake dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "TIMESTAMP_NTZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMP_NTZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMP_LTZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMP_LTZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMP_TZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMP_TZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMPNTZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMPNTZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMPLTZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMPLTZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMPTZ before a string is no literal (recorded errno 1003)", + sql: "SELECT TIMESTAMPTZ '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "DATETIME before a string is no literal (recorded errno 1003)", + sql: "SELECT DATETIME '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMP_NTZ(3) before a string is no literal (recorded errno 2121)", + sql: "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMP_TZ(3) before a string is no literal (recorded errno 2121)", + sql: "SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TIMESTAMP before a string is a literal (recorded)", + sql: "SELECT TIMESTAMP '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "DATE before a string is a literal (recorded)", + sql: "SELECT DATE '2026-01-02 03:04:05' AS v FROM input", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, AcceptanceTestCase { description: "a typed literal's parameters are not empty (recorded errno 1003)", sql: "SELECT PERIOD ( ) '[2026-01-01, 2026-01-02)' ;", From a1ecdbed1170407a418697baf6c2d747da53df76 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:23:42 +0000 Subject: [PATCH 2/9] Import 171: typed timestamp literal spellings, 9 receipts tslit-ex-00, the nine date-time-constant neighbours built at 2541bfd0: seven syntax errors (1003/42000) and the two precision forms as compilation errors after parsing (2121/42601). Inventory equal, no changes, 0.0 credits. --- .../syntax/receipts/import-20261104-171.jsonl | 9 +++ .../receipts/import-20261104-171.report.json | 75 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 coverage/syntax/receipts/import-20261104-171.jsonl create mode 100644 coverage/syntax/receipts/import-20261104-171.report.json diff --git a/coverage/syntax/receipts/import-20261104-171.jsonl b/coverage/syntax/receipts/import-20261104-171.jsonl new file mode 100644 index 000000000..39a650129 --- /dev/null +++ b/coverage/syntax/receipts/import-20261104-171.jsonl @@ -0,0 +1,9 @@ +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k1a7982b7fb16","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-b92a-0004-01620021a2ae","raw_record_sha256":"1f31e28755e8e13ad1d34269c44e1e964106d091b49d6142a9ab8ee234af58e8","recorded_at":"2026-11-07T04:20:14.572514+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"1a7982b7fb162e7e1b9bed4697be078aa7e87e01fc6c916cc4340f2b62a5ff3d","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k5ee293529f1e","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-ba02-0004-016200219a72","raw_record_sha256":"5e28d94c2df4930ca00b73f64ab5f1a5d21769fc998409c6f5c536c53ea539d5","recorded_at":"2026-11-07T04:20:16.259824+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"5ee293529f1e0d950714db52afe68643c1932954fb352248b646860b14154b43","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k624d77c24c5c","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-ba02-0004-016200219a76","raw_record_sha256":"50a7e690bc43c19eac8a5983e68297992a989ad50146d9cb2b0976e86c4e111e","recorded_at":"2026-11-07T04:20:17.963461+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"624d77c24c5c78d9f76e7f5e9abc8334b1b11c008dafd006422a1c8d5ea43666","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k69fcf5144724","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-ba02-0004-016200219a7a","raw_record_sha256":"560ea6cb61b2be35b79b05155e8bf1d84b9a178ef8f7812417229c1f0f0ac073","recorded_at":"2026-11-07T04:20:19.207794+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"69fcf514472412f23dfd971799b928236fa3618f86a7a8b604e7de1e5d939a86","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k8a36862cebb9","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-b92a-0004-01620021a2be","raw_record_sha256":"df03ec63b5185bdb5862e266108b4be98d8d9904e2fad5f4959ac9f5328fed8e","recorded_at":"2026-11-07T04:20:20.717434+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"8a36862cebb9e2362f3742fed6479b8e95bf7e1d9cb0ff5fc2fa6c2b9a6f6c4e","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kaf2e829caa1c","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-ba02-0004-016200219a82","raw_record_sha256":"2ea76ca3b085a672cb226b07109ca3141904ae825ab6a5df720f28109191ab24","recorded_at":"2026-11-07T04:20:21.924422+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"af2e829caa1c08836aeff567645c775cafe64e3889241fe93b64eae9c9228220","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kdf44dc074801","errno":2121,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"semantic_error","phase_basis":"errno 2121 compilation error after parsing","query_id":"01c83e84-0004-b92a-0004-01620021a2c6","raw_record_sha256":"d8951d4ea853fe4d777e1f095d3755843d2af729b72b2fbcea0d286bfcb8cd54","reason_category":"other","reason_category_basis":"errno 2121 has no more specific reason","recorded_at":"2026-11-07T04:20:23.115276+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19","sqlstate":"42601","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kf2531b90a1d4","errno":1003,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"syntax_error","phase_basis":"errno 1003 SQL compilation syntax error","query_id":"01c83e84-0004-ba02-0004-016200219a8a","raw_record_sha256":"93b872099d4679fafaef599e3ebe434b4b6b9f421ef3ead798944d3e6b342852","recorded_at":"2026-11-07T04:20:24.279327+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"f2531b90a1d45e1eefb1b10bd7904efccc0ebaf57b352e1176c960fdc52afe8d","sqlstate":"42000","warehouse_version":"10.40.102"} +{"capture_method":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kfadd3f560bcd","errno":2121,"format":"sqlquack-syntax-receipt/1","outcome":"error","packet_sha256":"a5b666e0373f52b6e4e820743751bb2d1f878d7bac10e49ed5275299f07df013","phase":"semantic_error","phase_basis":"errno 2121 compilation error after parsing","query_id":"01c83e84-0004-ba02-0004-016200219a8e","raw_record_sha256":"28621a99a52c888306d5832302832a5ee5764c0de9f3f41b81d86556c844cb34","reason_category":"other","reason_category_basis":"errno 2121 has no more specific reason","recorded_at":"2026-11-07T04:20:25.498444+00:00","run_audit_sha256":"ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88","run_id":"tslit-ex-00","session_parameters":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql_sha256":"fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6","sqlstate":"42601","warehouse_version":"10.40.102"} diff --git a/coverage/syntax/receipts/import-20261104-171.report.json b/coverage/syntax/receipts/import-20261104-171.report.json new file mode 100644 index 000000000..aba2969d4 --- /dev/null +++ b/coverage/syntax/receipts/import-20261104-171.report.json @@ -0,0 +1,75 @@ +{ + "format": "sqlquack-syntax-receipt/1", + "inputs": [ + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMP_LTZ '2026-01-02 03:04:05' ;", + "sql_sha256": "1a7982b7fb162e7e1b9bed4697be078aa7e87e01fc6c916cc4340f2b62a5ff3d" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMPTZ '2026-01-02 03:04:05' ;", + "sql_sha256": "5ee293529f1e0d950714db52afe68643c1932954fb352248b646860b14154b43" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMPNTZ '2026-01-02 03:04:05' ;", + "sql_sha256": "624d77c24c5c78d9f76e7f5e9abc8334b1b11c008dafd006422a1c8d5ea43666" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMP_TZ '2026-01-02 03:04:05' ;", + "sql_sha256": "69fcf514472412f23dfd971799b928236fa3618f86a7a8b604e7de1e5d939a86" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMP_NTZ '2026-01-02 03:04:05' ;", + "sql_sha256": "8a36862cebb9e2362f3742fed6479b8e95bf7e1d9cb0ff5fc2fa6c2b9a6f6c4e" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMPLTZ '2026-01-02 03:04:05' ;", + "sql_sha256": "af2e829caa1c08836aeff567645c775cafe64e3889241fe93b64eae9c9228220" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' ;", + "sql_sha256": "df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT DATETIME '2026-01-02 03:04:05' ;", + "sql_sha256": "f2531b90a1d45e1eefb1b10bd7904efccc0ebaf57b352e1176c960fdc52afe8d" + }, + { + "setup": [], + "setup_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "sql": "SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' ;", + "sql_sha256": "fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6" + } + ], + "receipts": 9, + "receipts_file_sha256": "aa3ff207e91b57c010069068a11f43c09eff8d2ba04ac077684358a684fffd44", + "reconciled_receipts": 0, + "refusals": [], + "refused": 0, + "runs": [ + { + "audit_sha256": "ca80939dbf9194731cc6b35d3a712018d63d477dcf8700137f84f766c5cc2f88", + "receipts": 9, + "refused_rows": 0, + "run_id": "tslit-ex-00", + "status": "imported" + } + ], + "superseding": 0, + "withdrawn": [] +} From 45691acb97b5632574dd60038fa0cfd06a445320 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:32:43 +0000 Subject: [PATCH 3/9] Parse a timestamp type with a precision before a string and refuse it after parsing Snowflake parses TIMESTAMP_NTZ(3) '...' and TIMESTAMP_TZ(3) '...' and rejects them at compile time (2121, "Unsupported data type literal"), while the seven bare spellings are syntax errors (1003). The parser now refuses only the bare spellings; a listed type with parameters before a string parses as a cast, and the refusal pass rejects it as SQ0211 unsupported_type_literal with the same advice to write '...'::TIMESTAMP_NTZ or call TO_TIMESTAMP_NTZ. The two precision cases settle as accept with a semantic stage from import 171's receipts. --- README.md | 4 +-- coverage/syntax/cases/expressions.jsonl | 4 +-- coverage/syntax/marks/expressions.jsonl | 2 ++ .../sqlquack-core/src/diagnostics/codes.lock | 1 + .../src/diagnostics/constants.rs | 14 +++++++++ .../src/refusals/_helpers/rejections.rs | 29 ++++++++++++++++++- .../src/refusals/tests/test_refusals.rs | 10 +++++++ .../src/expressions/types/constants.rs | 12 ++++++++ .../src/expressions/types/mod.rs | 1 + .../src/parser/_helpers/types/data_types.rs | 10 ++++--- .../sqlquack-syntax/src/parser/constants.rs | 11 ------- .../tests/parser/test_snowflake_recorded.rs | 8 ++--- crates/sqlquack/tests/cli/test_check.rs | 8 +++++ .../docs/docs/diagnostics/reference.mdx | 15 ++++++++++ 14 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 crates/sqlquack-syntax/src/expressions/types/constants.rs diff --git a/README.md b/README.md index 5e694a305..4ea7b942b 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ guard's former `E_GUARD_*` name), `message`, `span` (character `start` and `end` listed with an explanation each in `crates/sqlquack-core/src/diagnostics/constants.rs`, which `sqlquack explain ` prints; a code is never reused or renumbered. SQL that Snowflake parses but then rejects, such as `INTERSECT ALL`, `FOR UPDATE WAIT`, `QUALIFY` without a window function, -or a `* ILIKE` `REPLACE` or `RENAME` of a column the pattern removes, is refused with its own -`SQ02xx` code instead of being converted. +a `* ILIKE` `REPLACE` or `RENAME` of a column the pattern removes, or `TIMESTAMP_NTZ(3) '...'`, is +refused with its own `SQ02xx` code instead of being converted. A `VALUES` list of more than 1,000 rows is refused for now. `--session NAME=VALUE` passes the caller's Snowflake session parameters (`function` takes it too): diff --git a/coverage/syntax/cases/expressions.jsonl b/coverage/syntax/cases/expressions.jsonl index 7a7c286df..2c2546b76 100644 --- a/coverage/syntax/cases/expressions.jsonl +++ b/coverage/syntax/cases/expressions.jsonl @@ -67,9 +67,9 @@ {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.k8b2b51653394","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o008"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATE ;","sql_sha256":"8b2b5165339438e2b5d8525374f49aeca053fb2e1a17f63f41693700b8e3729c","stage":"semantic"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kaf2e829caa1c","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o013"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMPLTZ '2026-01-02 03:04:05' ;","sql_sha256":"af2e829caa1c08836aeff567645c775cafe64e3889241fe93b64eae9c9228220"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kd5a5920217a9","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o004"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP '2026-01-01' ;","sql_sha256":"d5a5920217a99e21c7cac1cbd42b0b07e8e95102f23b7431f800ef677ac82b41"} -{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kdf44dc074801","expect":"observe","obligations":["sf.expressions.data-types-datetime.date-time-constant.o016"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kdf44dc074801","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o016"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19","stage":"semantic"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kf2531b90a1d4","expect":"reject","obligations":["sf.expressions.data-types-datetime.date-time-constant.o015"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT DATETIME '2026-01-02 03:04:05' ;","sql_sha256":"f2531b90a1d45e1eefb1b10bd7904efccc0ebaf57b352e1176c960fdc52afe8d"} -{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kfadd3f560bcd","expect":"observe","obligations":["sf.expressions.data-types-datetime.date-time-constant.o017"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6"} +{"capture":"execute","case_id":"sf.expressions.data-types-datetime.date-time-constant.kfadd3f560bcd","expect":"accept","obligations":["sf.expressions.data-types-datetime.date-time-constant.o017"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' ;","sql_sha256":"fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6","stage":"semantic"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.date.k59a97475f0c8","expect":"accept","obligations":["sf.expressions.data-types-datetime.date.o001"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS DATE ) ;","sql_sha256":"59a97475f0c8b89764fe8721799bc7d86ac03d73a68936308a28bbf53bc4558b"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.datetime.k2fd95373b7e2","expect":"accept","obligations":["sf.expressions.data-types-datetime.datetime.o001"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS DATETIME ) ;","sql_sha256":"2fd95373b7e21051e0882240cf98b8bd7a71c503ddb7defdb1e68b48ca76a941"} {"capture":"execute","case_id":"sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.k0110f52d73ae","expect":"reject","obligations":["sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.o070"],"session":{"ABORT_DETACHED_QUERY":true,"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS":30,"STATEMENT_TIMEOUT_IN_SECONDS":30,"TIMEZONE":"UTC","USE_CACHED_RESULT":false},"session_sha256":"c2b87f85a8229ee648b5f718b90d87093a916ed0842c86f29bfccd5c4484ad7d","setup":[],"setup_sha256":"4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","sql":"SELECT CAST ( NULL AS INTERVAL DAY TO SECOND ( 3 ) ;","sql_sha256":"0110f52d73ae53e7c4b41b54703550937b2521160a9274dd6e4513b53fb3633a"} diff --git a/coverage/syntax/marks/expressions.jsonl b/coverage/syntax/marks/expressions.jsonl index 76eff0bd6..4c3b8daab 100644 --- a/coverage/syntax/marks/expressions.jsonl +++ b/coverage/syntax/marks/expressions.jsonl @@ -7,6 +7,8 @@ {"case_id":"sf.expressions.commands-model-function.model-version-or-alias.k4e8d393ab28f","mark":"settled accept (D133)","obligations":["sf.expressions.commands-model-function.model-version-or-alias.o027"],"sql_sha256":"4e8d393ab28f75265e63c31704cb8b0001489279385189a21afca86ef4e536a7"} {"case_id":"sf.expressions.data-types-datetime.date-time-constant.k4f7f4f66d4b6","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.date-time-constant.o006"],"sql_sha256":"4f7f4f66d4b6e0387d2ff68d6366e031522c5b8dfcdecb66bec229bf079c60ee"} {"case_id":"sf.expressions.data-types-datetime.date-time-constant.k8b2b51653394","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.date-time-constant.o008"],"sql_sha256":"8b2b5165339438e2b5d8525374f49aeca053fb2e1a17f63f41693700b8e3729c"} +{"case_id":"sf.expressions.data-types-datetime.date-time-constant.kdf44dc074801","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.date-time-constant.o016"],"sql_sha256":"df44dc074801f38a70409c7bfd032ebc31ab4b48ab58c4ed0cbd5dc3ee583d19"} +{"case_id":"sf.expressions.data-types-datetime.date-time-constant.kfadd3f560bcd","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.date-time-constant.o017"],"sql_sha256":"fadd3f560bcd9345070e94a8684624818dc601cf8a9561861c5ba30443b001b6"} {"case_id":"sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.k80bd5c6cb355","mark":"documented (D113)","obligations":["sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.o089","sf.expressions.data-types-datetime.interval-yearmonthqualifier-daytimequalifier.o090"],"sql_sha256":"80bd5c6cb355a1ade71485591afa56dc706af0a75a52f7df37c18af0389577ba"} {"case_id":"sf.expressions.data-types-datetime.interval.interval-constants.k4afa2b15ceda","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.interval.interval-constants.o009"],"sql_sha256":"4afa2b15ceda38e24b46d1068390d50019c9f89ff05ee6fca618b0087f0a5797"} {"case_id":"sf.expressions.data-types-datetime.interval.k0a322c8617c9","mark":"settled accept (D133)","obligations":["sf.expressions.data-types-datetime.interval.o087"],"sql_sha256":"0a322c8617c95c3819d5eb985d8ba0d662e20160907649a541e3ded96e31aa78"} diff --git a/crates/sqlquack-core/src/diagnostics/codes.lock b/crates/sqlquack-core/src/diagnostics/codes.lock index 876146d43..995c361ad 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 +SQ0211 unsupported_type_literal diff --git a/crates/sqlquack-core/src/diagnostics/constants.rs b/crates/sqlquack-core/src/diagnostics/constants.rs index 8b37bb245..b09d5a4f9 100644 --- a/crates/sqlquack-core/src/diagnostics/constants.rs +++ b/crates/sqlquack-core/src/diagnostics/constants.rs @@ -311,4 +311,18 @@ 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: "SQ0211", + name: "unsupported_type_literal", + alias: None, + title: "Snowflake has no literal of this timestamp type", + help: Some("cast the string instead, e.g. `'2026-01-02'::TIMESTAMP_NTZ(3)`"), + explanation: "Only DATE, TIME and TIMESTAMP take a string as a typed literal in \ + Snowflake. A TIMESTAMP_NTZ, TIMESTAMP_LTZ or TIMESTAMP_TZ type with a precision \ + before a string parses, but Snowflake rejects it as an unsupported data type \ + literal (Snowflake error 2121). Without a precision, the same spelling is a syntax \ + error. Cast the string with `::` or CAST, or call TO_TIMESTAMP_NTZ, \ + TO_TIMESTAMP_LTZ or TO_TIMESTAMP_TZ.", + example: r#"echo "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v" | sqlquack check"#, + }, ]; diff --git a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs index d45f7486d..f2bf15b24 100644 --- a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs +++ b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs @@ -1,6 +1,9 @@ //! The constructs Snowflake parses but then rejects, recognised one node at a time. -use sqlquack_syntax::expressions::names::models::Star; +use sqlquack_syntax::expressions::names::models::{Literal, Star}; +use sqlquack_syntax::expressions::operators::models::Cast; +use sqlquack_syntax::expressions::types::constants::SNOWFLAKE_UNTYPED_LITERAL_TYPES; +use sqlquack_syntax::expressions::types::models::DataType; use sqlquack_syntax::{Expression, ExpressionWalk}; /// The registry name of the rejection a node causes, with the reason, if it causes one. @@ -33,10 +36,34 @@ pub(crate) fn rejection(node: &Expression) -> Option<(&'static str, String)> { format!("{name} does not match the star's ILIKE pattern, so REPLACE or RENAME names a column the pattern removed"), ) }), + Expression::Cast(cast) => untyped_literal(cast), _ => None, } } +/// A string written after a timestamp type name with parameters, such as `TIMESTAMP_NTZ(3) '...'`. +fn untyped_literal(cast: &Cast) -> Option<(&'static str, String)> { + let Expression::Literal(literal) = &cast.this else { + return None; + }; + let DataType::Custom { name } = &cast.to else { + return None; + }; + let written: String = name.split_whitespace().collect(); + let (base, _) = written.split_once('(')?; + let (_, function) = SNOWFLAKE_UNTYPED_LITERAL_TYPES + .iter() + .find(|(listed, _)| listed.eq_ignore_ascii_case(base))?; + (!cast.double_colon_syntax && matches!(literal.as_ref(), Literal::String(_))).then(|| { + ( + "unsupported_type_literal", + format!( + "Snowflake has no {written} literal; cast the string instead, as '...'::{written} or {function}('...')" + ), + ) + }) +} + /// A REPLACE or RENAME name the ILIKE pattern removes ignoring case; backslashes and non-ASCII skip. fn filtered_out_name(star: &Star) -> Option<&str> { let pattern = star diff --git a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs index 2ac00ac8f..cb4f06e91 100644 --- a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs +++ b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs @@ -121,6 +121,16 @@ fn given_sql_snowflake_parses_then_rejects_when_refusing_then_each_rejection_is_ expected_rejections: vec![], expected_codes: vec![], }, + RejectionTestCase { + description: "a timestamp type with a precision before a string is rejected; a cast or TIMESTAMP literal is not", + sql: "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v; \ + SELECT timestamp_tz(3) '2026-01-02 03:04:05' AS v; \ + SELECT '2026-01-02 03:04:05'::TIMESTAMP_NTZ(3) AS v; \ + SELECT CAST('2026-01-02' AS TIMESTAMP_TZ(3)) AS v; \ + SELECT TIMESTAMP '2026-01-02 03:04:05' AS v; SELECT v1 'm' AS v", + expected_rejections: vec!["unsupported_type_literal", "unsupported_type_literal"], + expected_codes: vec!["SQ0211", "SQ0211"], + }, RejectionTestCase { description: "a rejection nested in a CTE is found", sql: "WITH both_ids AS (SELECT 1 AS id EXCEPT ALL SELECT 2) SELECT id FROM both_ids", diff --git a/crates/sqlquack-syntax/src/expressions/types/constants.rs b/crates/sqlquack-syntax/src/expressions/types/constants.rs new file mode 100644 index 000000000..b775b1ea3 --- /dev/null +++ b/crates/sqlquack-syntax/src/expressions/types/constants.rs @@ -0,0 +1,12 @@ +//! Type names with fixed meanings in Snowflake SQL. + +/// Type names Snowflake refuses before a string, each with the conversion function to suggest. +pub const SNOWFLAKE_UNTYPED_LITERAL_TYPES: &[(&str, &str)] = &[ + ("TIMESTAMP_NTZ", "TO_TIMESTAMP_NTZ"), + ("TIMESTAMPNTZ", "TO_TIMESTAMP_NTZ"), + ("TIMESTAMP_LTZ", "TO_TIMESTAMP_LTZ"), + ("TIMESTAMPLTZ", "TO_TIMESTAMP_LTZ"), + ("TIMESTAMP_TZ", "TO_TIMESTAMP_TZ"), + ("TIMESTAMPTZ", "TO_TIMESTAMP_TZ"), + ("DATETIME", "TO_TIMESTAMP_NTZ"), +]; diff --git a/crates/sqlquack-syntax/src/expressions/types/mod.rs b/crates/sqlquack-syntax/src/expressions/types/mod.rs index 320d2d1cc..e79286c07 100644 --- a/crates/sqlquack-syntax/src/expressions/types/mod.rs +++ b/crates/sqlquack-syntax/src/expressions/types/mod.rs @@ -1,4 +1,5 @@ //! Data types and the interval, array and struct nodes. +pub mod constants; pub(crate) mod main; pub mod models; diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs index 885e2f7db..051b2231c 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs @@ -132,8 +132,10 @@ impl Parser { let Some(end) = self.named_type_literal_end() else { return Ok(None); }; - if let Some(error) = self.untyped_literal_error(end) { - return Err(error); + if end == self.current + 1 { + if let Some(error) = self.untyped_literal_error(end) { + return Err(error); + } } let parts: Vec = self.tokens[self.current..end] .iter() @@ -164,10 +166,10 @@ impl Parser { )))) } - /// Snowflake's refusal of a timestamp type name before the string at `end`, placed at the string. + /// Snowflake's syntax refusal of a timestamp type name right before the string at `end`, at it. pub(crate) fn untyped_literal_error(&mut self, end: usize) -> Option { let name = self.peek_text().to_ascii_uppercase(); - let (_, function) = crate::parser::constants::SNOWFLAKE_UNTYPED_LITERAL_TYPES + let (_, function) = crate::expressions::types::constants::SNOWFLAKE_UNTYPED_LITERAL_TYPES .iter() .find(|(listed, _)| *listed == name)?; self.current = end; diff --git a/crates/sqlquack-syntax/src/parser/constants.rs b/crates/sqlquack-syntax/src/parser/constants.rs index 5eaaf1ad5..a304c98d2 100644 --- a/crates/sqlquack-syntax/src/parser/constants.rs +++ b/crates/sqlquack-syntax/src/parser/constants.rs @@ -251,17 +251,6 @@ pub(crate) const SQLITE_RANGE_FOLLOWERS: [crate::tokens::models::TokenType; 9] = /// Snowflake types written before a string as a typed literal, beyond the shared ones. pub(crate) const SNOWFLAKE_LITERAL_TYPES: &[&str] = &["IPADDRESS"]; -/// Type names Snowflake refuses before a string, each with the conversion function to suggest. -pub(crate) const SNOWFLAKE_UNTYPED_LITERAL_TYPES: &[(&str, &str)] = &[ - ("TIMESTAMP_NTZ", "TO_TIMESTAMP_NTZ"), - ("TIMESTAMPNTZ", "TO_TIMESTAMP_NTZ"), - ("TIMESTAMP_LTZ", "TO_TIMESTAMP_LTZ"), - ("TIMESTAMPLTZ", "TO_TIMESTAMP_LTZ"), - ("TIMESTAMP_TZ", "TO_TIMESTAMP_TZ"), - ("TIMESTAMPTZ", "TO_TIMESTAMP_TZ"), - ("DATETIME", "TO_TIMESTAMP_NTZ"), -]; - /// Reserved words Snowflake is recorded refusing as an unquoted later part of a dotted name. pub(crate) const SNOWFLAKE_REFUSED_NAME_PARTS: &[&str] = &["SELECT"]; diff --git a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs index 648671b2b..4383a3780 100644 --- a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs @@ -633,16 +633,16 @@ fn given_recorded_snowflake_cases_when_parsing_then_acceptance_matches_snowflake expected_accepted: false, }, AcceptanceTestCase { - description: "TIMESTAMP_NTZ(3) before a string is no literal (recorded errno 2121)", + description: "TIMESTAMP_NTZ(3) before a string parses, as Snowflake rejects it later (recorded errno 2121)", sql: "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v FROM input", dialect: Dialect::Snowflake, - expected_accepted: false, + expected_accepted: true, }, AcceptanceTestCase { - description: "TIMESTAMP_TZ(3) before a string is no literal (recorded errno 2121)", + description: "TIMESTAMP_TZ(3) before a string parses, as Snowflake rejects it later (recorded errno 2121)", sql: "SELECT TIMESTAMP_TZ(3) '2026-01-02 03:04:05' AS v FROM input", dialect: Dialect::Snowflake, - expected_accepted: false, + expected_accepted: true, }, AcceptanceTestCase { description: "TIMESTAMP before a string is a literal (recorded)", diff --git a/crates/sqlquack/tests/cli/test_check.rs b/crates/sqlquack/tests/cli/test_check.rs index 1b43e592a..58eece974 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 timestamp type with a precision before a string is SQ0211 and exits 1", + arguments: &["check"], + sql: "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v", + expected_exit_code: Some(1), + expected_stdout_prefix: "", + expected_stderr_mention: "error[SQ0211]", + }, 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 77106dbc6..b1efe72eb 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 | +| [`SQ0211`](#sq0211) | `unsupported_type_literal` | Snowflake has no literal of this timestamp type | ## 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 ``` + +### SQ0211 + +**Snowflake has no literal of this timestamp type** (`unsupported_type_literal`) + +Only DATE, TIME and TIMESTAMP take a string as a typed literal in Snowflake. A TIMESTAMP_NTZ, TIMESTAMP_LTZ or TIMESTAMP_TZ type with a precision before a string parses, but Snowflake rejects it as an unsupported data type literal (Snowflake error 2121). Without a precision, the same spelling is a syntax error. Cast the string with `::` or CAST, or call TO_TIMESTAMP_NTZ, TO_TIMESTAMP_LTZ or TO_TIMESTAMP_TZ. + +**Help:** cast the string instead, e.g. `'2026-01-02'::TIMESTAMP_NTZ(3)` + +**Example:** + +```sh +echo "SELECT TIMESTAMP_NTZ(3) '2026-01-02 03:04:05' AS v" | sqlquack check +``` From 32ba12e8c2426d24b81694b27d5230233c2da104 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:40:04 +0000 Subject: [PATCH 4/9] Find a run-database receipt's setup by its digest alone The case builder looked a receipt's setup text up by the pair of its SQL hash and setup digest in the reports' inputs. The run that recorded the 13 IPADDRESS and UUID temporary table cases lists no inputs for their SQL, so the builder fell back to the held setup and rewrote their recorded setup. The same setup text is listed under the same digest by other reports, and the digest names the text, so the lookup now uses the digest alone. A plain rebuild of the expressions family now reproduces the committed case file. --- scripts/syntax_obligations/_helpers/cases.py | 6 +- .../scripts/syntax_obligations/_test_types.py | 7 +++ .../scripts/syntax_obligations/test_setups.py | 56 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/unit/scripts/syntax_obligations/test_setups.py diff --git a/scripts/syntax_obligations/_helpers/cases.py b/scripts/syntax_obligations/_helpers/cases.py index ea8807073..f22e62765 100644 --- a/scripts/syntax_obligations/_helpers/cases.py +++ b/scripts/syntax_obligations/_helpers/cases.py @@ -152,13 +152,13 @@ def _binding( def run_database_setups(directory: Path) -> tuple[dict[tuple[str, str], list[str]], list[str]]: """Each ddl case's setup from its latest counted receipt (D110a), and cases for review.""" - inputs: dict[tuple[str, str], list[str]] = {} + inputs: dict[str, list[str]] = {} counted_runs: set[str] = set() for path in directory.glob("*.report.json"): report: Row = json.loads(path.read_text()) for row in report.get("inputs", []): setup: list[str] = list(row["setup"]) - inputs[(str(row["sql_sha256"]), digest(canonical(setup)))] = setup + inputs[digest(canonical(setup))] = setup counted_runs |= { str(run["run_id"]) for run in report.get("runs", []) @@ -170,7 +170,7 @@ def run_database_setups(directory: Path) -> tuple[dict[tuple[str, str], list[str for line in path.read_text().splitlines(): receipt: Row = json.loads(line) key: tuple[str, str] = (str(receipt["case_id"]), str(receipt["sql_sha256"])) - setup = inputs.get((key[1], str(receipt.get("setup_sha256", ""))), []) + setup = inputs.get(str(receipt.get("setup_sha256", "")), []) if ( receipt.get("capture_method") != CASE_CAPTURE_RUN_DATABASE or MEASURE_KEY in receipt diff --git a/tests/unit/scripts/syntax_obligations/_test_types.py b/tests/unit/scripts/syntax_obligations/_test_types.py index 053b7b49b..ad135dcda 100644 --- a/tests/unit/scripts/syntax_obligations/_test_types.py +++ b/tests/unit/scripts/syntax_obligations/_test_types.py @@ -10,3 +10,10 @@ class ShortenedTargetTestCase: description: str grammar: str expected_last_tokens: frozenset[str] + + +@dataclass(frozen=True) +class RecordedSetupTestCase: + description: str + input_sql_sha256: str + expected_setups: dict[tuple[str, str], list[str]] diff --git a/tests/unit/scripts/syntax_obligations/test_setups.py b/tests/unit/scripts/syntax_obligations/test_setups.py new file mode 100644 index 000000000..680a9573b --- /dev/null +++ b/tests/unit/scripts/syntax_obligations/test_setups.py @@ -0,0 +1,56 @@ +"""Run-database setups read back from counted receipts.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scripts.syntax_obligations._helpers.cases import canonical, digest, run_database_setups +from tests.unit.scripts.syntax_obligations._test_types import RecordedSetupTestCase + +SETUP: list[str] = [ + "CREATE DATABASE COMMENT = 'sqlquack ddl run '", + "USE SCHEMA .CASES", +] +CASE_ID: str = "sf.expressions.data-types-uuid.k0a320b3136f" +SQL_SHA: str = "0a320b3136f" + "0" * 53 + + +@pytest.mark.parametrize( + "test_case", + ( + RecordedSetupTestCase( + description="the setup is listed as an input of the receipt's own SQL", + input_sql_sha256=SQL_SHA, + expected_setups={(CASE_ID, SQL_SHA): SETUP}, + ), + RecordedSetupTestCase( + description="the setup is listed only as an input of another SQL", + input_sql_sha256="f" * 64, + expected_setups={(CASE_ID, SQL_SHA): SETUP}, + ), + ), + ids=lambda case: case.description, +) +def test_given_a_counted_run_database_receipt_when_read_then_its_setup_is_found_by_digest( + test_case: RecordedSetupTestCase, tmp_path: Path +) -> None: + report: dict[str, object] = { + "inputs": [{"sql_sha256": test_case.input_sql_sha256, "setup": SETUP}], + "runs": [{"run_id": "run-1", "status": "imported"}], + } + receipt: dict[str, object] = { + "capture_method": "execute_ddl_in_run_database", + "case_id": CASE_ID, + "phase": "accepted", + "recorded_at": "2026-11-04T02:40:54+00:00", + "run_id": "run-1", + "setup_sha256": digest(canonical(SETUP)), + "sql_sha256": SQL_SHA, + } + (tmp_path / "import-1.report.json").write_text(json.dumps(report)) + (tmp_path / "import-1.jsonl").write_text(json.dumps(receipt) + "\n") + + assert run_database_setups(tmp_path) == (test_case.expected_setups, []) From bf97f064996229eb78b0c9ef4d7e53913a210dca Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:58:53 +0000 Subject: [PATCH 5/9] coverage(syntax): renew the auditor's date and time constant approval --- .../syntax/reviews/auditor-expressions.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/coverage/syntax/reviews/auditor-expressions.toml b/coverage/syntax/reviews/auditor-expressions.toml index 44243322e..f3a42eaca 100644 --- a/coverage/syntax/reviews/auditor-expressions.toml +++ b/coverage/syntax/reviews/auditor-expressions.toml @@ -436,3 +436,21 @@ note = "Read in full against the documentation page, the obligations, the cases [review.definitions] "sf.expressions.data-types-datetime.timestamp-variations" = "c5aee53a49abc97848292516688ff51bdbb71c9b3eab90e5e17be1a239ba2c52" "sf.expressions.identifier-literal" = "6a6e2aa9d390636159e63ade723676774c8aa4776bf415e1a16fdff49e946740" + +[[review]] +id = "auditor-expressions-016" +reviewer_lane = "auditor" +family = "expressions" +parser_tree = "90d1828438bc1814b7b71e8aeda823626d48b9f0" +reviewed_utc = "2026-11-07T05:05:00Z" +verdict = "approved" +forms = [ + "sf.expressions.data-types-datetime.date-time-constant", +] +note = "Re-read after o009-o017 were added: the nine negative neighbours, their cases and their import-171 receipts (seven 1003, two 2121), against the parse trees and refusals of the parser at 32ba12e8c242; every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.expressions.data-types-datetime.date-time-constant" = "c8570c5f675d9f63f8af68ba7b683a31451d1d5c88f0e7c7f2d02a81c2f6d7b5" + +[review.definitions] +"sf.expressions.data-types-datetime.date-time-constant" = "31d338a953d80790742befe3292530218a691cf8a112a56d8fd37ed9c7650f64" From 615a5c2a2037b2d92c0c65a295d8ac342efabfbf Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 05:00:35 +0000 Subject: [PATCH 6/9] coverage(syntax): record the renewed approval's actual review time --- coverage/syntax/reviews/auditor-expressions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/syntax/reviews/auditor-expressions.toml b/coverage/syntax/reviews/auditor-expressions.toml index f3a42eaca..1da3231d0 100644 --- a/coverage/syntax/reviews/auditor-expressions.toml +++ b/coverage/syntax/reviews/auditor-expressions.toml @@ -442,7 +442,7 @@ id = "auditor-expressions-016" reviewer_lane = "auditor" family = "expressions" parser_tree = "90d1828438bc1814b7b71e8aeda823626d48b9f0" -reviewed_utc = "2026-11-07T05:05:00Z" +reviewed_utc = "2026-11-07T04:58:00Z" verdict = "approved" forms = [ "sf.expressions.data-types-datetime.date-time-constant", From a21da4d654ca1c60cee651a303876ac0e573dcf7 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:10:31 +0000 Subject: [PATCH 7/9] Refuse only typed literals as unsupported type literals, not casts SQ0211 matched any CAST to a listed timestamp type with a precision, so CAST('...' AS DATETIME(3)), CAST('...' AS TIMESTAMPNTZ(3)) and the TIMESTAMP_LTZ casts were refused although they are ordinary casts. A cast now records whether it was written as a typed literal, TYPE '...', and only those are refused. The flag is left out of the serialized tree, so parse trees and coverage fingerprints are unchanged. --- .../src/refusals/_helpers/rejections.rs | 2 +- .../src/refusals/tests/test_refusals.rs | 4 ++ .../src/parse/_helpers/tree/nodes.rs | 1 + .../src/expressions/operators/models.rs | 22 +++++++++ .../_helpers/expressions/comparisons.rs | 3 ++ .../parser/_helpers/expressions/grouping.rs | 1 + .../parser/_helpers/expressions/primary.rs | 12 +++++ .../src/parser/_helpers/functions/generic.rs | 45 +++---------------- .../src/parser/_helpers/functions/registry.rs | 1 + .../_helpers/functions/registry_functions.rs | 1 + .../src/parser/_helpers/functions/typed.rs | 1 + .../parser/_helpers/statements/sessions.rs | 1 + .../src/parser/_helpers/support/names.rs | 1 + .../src/parser/_helpers/types/casts.rs | 3 ++ .../src/parser/_helpers/types/data_types.rs | 1 + 15 files changed, 58 insertions(+), 41 deletions(-) diff --git a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs index f2bf15b24..86e17f59a 100644 --- a/crates/sqlquack-core/src/refusals/_helpers/rejections.rs +++ b/crates/sqlquack-core/src/refusals/_helpers/rejections.rs @@ -54,7 +54,7 @@ fn untyped_literal(cast: &Cast) -> Option<(&'static str, String)> { let (_, function) = SNOWFLAKE_UNTYPED_LITERAL_TYPES .iter() .find(|(listed, _)| listed.eq_ignore_ascii_case(base))?; - (!cast.double_colon_syntax && matches!(literal.as_ref(), Literal::String(_))).then(|| { + (cast.typed_literal && matches!(literal.as_ref(), Literal::String(_))).then(|| { ( "unsupported_type_literal", format!( diff --git a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs index cb4f06e91..4aa860283 100644 --- a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs +++ b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs @@ -127,6 +127,10 @@ fn given_sql_snowflake_parses_then_rejects_when_refusing_then_each_rejection_is_ SELECT timestamp_tz(3) '2026-01-02 03:04:05' AS v; \ SELECT '2026-01-02 03:04:05'::TIMESTAMP_NTZ(3) AS v; \ SELECT CAST('2026-01-02' AS TIMESTAMP_TZ(3)) AS v; \ + SELECT CAST('2026-01-02' AS DATETIME(3)) AS v; \ + SELECT CAST('2026-01-02' AS TIMESTAMPNTZ(3)) AS v; \ + SELECT CAST('2026-01-02' AS TIMESTAMP_LTZ(3)) AS v; \ + SELECT TRY_CAST('2026-01-02' AS TIMESTAMPLTZ(3)) AS v; \ SELECT TIMESTAMP '2026-01-02 03:04:05' AS v; SELECT v1 'm' AS v", expected_rejections: vec!["unsupported_type_literal", "unsupported_type_literal"], expected_codes: vec!["SQ0211", "SQ0211"], diff --git a/crates/sqlquack-syntax-postgres/src/parse/_helpers/tree/nodes.rs b/crates/sqlquack-syntax-postgres/src/parse/_helpers/tree/nodes.rs index 460112da9..6a6e2e7b7 100644 --- a/crates/sqlquack-syntax-postgres/src/parse/_helpers/tree/nodes.rs +++ b/crates/sqlquack-syntax-postgres/src/parse/_helpers/tree/nodes.rs @@ -79,6 +79,7 @@ pub(crate) fn cast(this: Expression, to: DataType, double_colon_syntax: bool) -> to, trailing_comments: Vec::new(), double_colon_syntax, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/expressions/operators/models.rs b/crates/sqlquack-syntax/src/expressions/operators/models.rs index 73df99eac..93a267a04 100644 --- a/crates/sqlquack-syntax/src/expressions/operators/models.rs +++ b/crates/sqlquack-syntax/src/expressions/operators/models.rs @@ -86,6 +86,11 @@ pub struct Cast { /// Whether PostgreSQL `::` syntax was used (true) vs CAST() function (false) #[serde(default)] pub double_colon_syntax: bool, + /// SQLQuack: written as a typed literal, `TYPE '...'`, rather than as `CAST` or `::`; read by + /// the Snowflake refusals only, so left out of the serialized tree. + #[serde(skip)] + #[ast(skip)] + pub typed_literal: bool, /// FORMAT clause for BigQuery: CAST(x AS STRING FORMAT 'format_string') #[serde(skip_serializing_if = "Option::is_none", default)] pub format: Option>, @@ -102,6 +107,23 @@ pub struct Cast { pub span: SourceSpan, } +impl Cast { + /// A cast of `this` to `to`, written with `::` when `double_colon_syntax`, else as CAST. + pub fn new(this: Expression, to: DataType, double_colon_syntax: bool) -> Self { + Self { + this, + to, + trailing_comments: Vec::new(), + double_colon_syntax, + typed_literal: false, + format: None, + default: None, + inferred_type: None, + span: SourceSpan::default(), + } + } +} + #[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CollationExpr { pub this: Expression, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs index 72018d73b..1b3abec15 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs @@ -1662,6 +1662,7 @@ impl Parser { to: self.normalize_dialect_data_type(data_type), trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1761,6 +1762,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1958,6 +1960,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: true, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/grouping.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/grouping.rs index 6cec95e18..fc2816b2d 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/grouping.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/grouping.rs @@ -1234,6 +1234,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs index f0eabf6b2..e8db83907 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs @@ -365,6 +365,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: true, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -725,6 +726,7 @@ impl Parser { to: DataType::Date, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -776,6 +778,7 @@ impl Parser { }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -807,6 +810,7 @@ impl Parser { }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -873,6 +877,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -929,6 +934,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1040,6 +1046,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1167,6 +1174,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1194,6 +1202,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1217,6 +1226,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1321,6 +1331,7 @@ impl Parser { to: DataType::Binary { length: None }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -1481,6 +1492,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs index 0a061e367..243be56d3 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs @@ -735,6 +735,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, @@ -936,53 +937,17 @@ impl Parser { self.skip(); let data_type = self.parse_data_type_for_cast()?; - expr = Expression::Cast(Box::new(Cast { - this: expr, - to: data_type, - trailing_comments: Vec::new(), - double_colon_syntax: true, - format: None, - default: None, - inferred_type: None, - span: SourceSpan::default(), - })); + expr = Expression::Cast(Box::new(Cast::new(expr, data_type, true))); } } else if self.match_token(TokenType::ColonGt) { let data_type = self.parse_data_type_for_cast()?; - expr = Expression::Cast(Box::new(Cast { - this: expr, - to: data_type, - trailing_comments: Vec::new(), - double_colon_syntax: false, - format: None, - default: None, - inferred_type: None, - span: SourceSpan::default(), - })); + expr = Expression::Cast(Box::new(Cast::new(expr, data_type, false))); } else if self.match_token(TokenType::NColonGt) { let data_type = self.parse_data_type_for_cast()?; - expr = Expression::TryCast(Box::new(Cast { - this: expr, - to: data_type, - trailing_comments: Vec::new(), - double_colon_syntax: false, - format: None, - default: None, - inferred_type: None, - span: SourceSpan::default(), - })); + expr = Expression::TryCast(Box::new(Cast::new(expr, data_type, false))); } else if self.match_token(TokenType::QDColon) { let data_type = self.parse_data_type_for_cast()?; - expr = Expression::TryCast(Box::new(Cast { - this: expr, - to: data_type, - trailing_comments: Vec::new(), - double_colon_syntax: true, - format: None, - default: None, - inferred_type: None, - span: SourceSpan::default(), - })); + expr = Expression::TryCast(Box::new(Cast::new(expr, data_type, true))); } else if self.check(TokenType::Arrow) && !matches!( self.config.dialect, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/registry.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/registry.rs index 7469fabdb..3083965ea 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/registry.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/registry.rs @@ -723,6 +723,7 @@ impl Parser { to, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/registry_functions.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/registry_functions.rs index 42e896886..1edbd7716 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/registry_functions.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/registry_functions.rs @@ -1076,6 +1076,7 @@ impl Parser { }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/typed.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/typed.rs index ece629793..05a8a28cc 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/typed.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/typed.rs @@ -221,6 +221,7 @@ impl Parser { to: DataType::Text, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/statements/sessions.rs b/crates/sqlquack-syntax/src/parser/_helpers/statements/sessions.rs index ecf3d33c1..ba35564ab 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/statements/sessions.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/statements/sessions.rs @@ -929,6 +929,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: true, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/support/names.rs b/crates/sqlquack-syntax/src/parser/_helpers/support/names.rs index 2ebce7430..e71b3f81b 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/support/names.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/support/names.rs @@ -1127,6 +1127,7 @@ impl Parser { to: data_type, trailing_comments: Vec::new(), double_colon_syntax: true, + typed_literal: false, format: None, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs index a1a4703d2..ec3d47859 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs @@ -165,6 +165,7 @@ impl Parser { to: data_type, trailing_comments, double_colon_syntax: false, + typed_literal: false, format, default, inferred_type: None, @@ -194,6 +195,7 @@ impl Parser { to: data_type, trailing_comments, double_colon_syntax: false, + typed_literal: false, format, default: None, inferred_type: None, @@ -223,6 +225,7 @@ impl Parser { to: data_type, trailing_comments, double_colon_syntax: false, + typed_literal: false, format, default: None, inferred_type: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs index 051b2231c..bc4053d65 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs @@ -158,6 +158,7 @@ impl Parser { }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: true, format: None, default: None, inferred_type: None, From 62b33fb98deb6b4d085d96bd86aa02cc7e49a69a Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:52:02 +0000 Subject: [PATCH 8/9] Refuse DATETIME before a string only in the faithful Snowflake modes The DATETIME '...' syntax refusal checked only for the Snowflake dialect, so the polyglot-matching modes refused it too, where polyglot reads it as a datetime literal; polyglot's exact corpus check failed on test_bigquery_date_timezone_to_duckdb_476. It now uses the same gate as the other Snowflake syntax refusals. --- .../sqlquack-syntax/src/parser/_helpers/expressions/primary.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs index e8db83907..8c5801767 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs @@ -955,7 +955,7 @@ impl Parser { } fn parse_primary_datetime_literal(&mut self) -> Result { - if self.is_snowflake() && self.check_next(TokenType::String) { + if self.applies_snowflake_context_rules() && self.check_next(TokenType::String) { let string = self.current + 1; if let Some(error) = self.untyped_literal_error(string) { return Err(error); From 053d0fa6f6571978d842503697e6566b92987b52 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:10:56 +0000 Subject: [PATCH 9/9] Keep the typed-literal flag's doc comment to one line for fensu --- crates/sqlquack-syntax/src/expressions/operators/models.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/sqlquack-syntax/src/expressions/operators/models.rs b/crates/sqlquack-syntax/src/expressions/operators/models.rs index 93a267a04..e1fbc436a 100644 --- a/crates/sqlquack-syntax/src/expressions/operators/models.rs +++ b/crates/sqlquack-syntax/src/expressions/operators/models.rs @@ -86,8 +86,7 @@ pub struct Cast { /// Whether PostgreSQL `::` syntax was used (true) vs CAST() function (false) #[serde(default)] pub double_colon_syntax: bool, - /// SQLQuack: written as a typed literal, `TYPE '...'`, rather than as `CAST` or `::`; read by - /// the Snowflake refusals only, so left out of the serialized tree. + /// SQLQuack: written as a typed literal, `TYPE '...'`; read by the Snowflake refusals only. #[serde(skip)] #[ast(skip)] pub typed_literal: bool,