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 df23e38ff..2c2546b76 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":"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":"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/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/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/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/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": [] +} diff --git a/coverage/syntax/reviews/auditor-expressions.toml b/coverage/syntax/reviews/auditor-expressions.toml index 44243322e..1da3231d0 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-07T04:58: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" 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..86e17f59a 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.typed_literal && 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..4aa860283 100644 --- a/crates/sqlquack-core/src/refusals/tests/test_refusals.rs +++ b/crates/sqlquack-core/src/refusals/tests/test_refusals.rs @@ -121,6 +121,20 @@ 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 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"], + }, 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-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..e1fbc436a 100644 --- a/crates/sqlquack-syntax/src/expressions/operators/models.rs +++ b/crates/sqlquack-syntax/src/expressions/operators/models.rs @@ -86,6 +86,10 @@ 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 '...'`; read by the Snowflake refusals only. + #[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 +106,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/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/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 4940fbea1..8c5801767 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, @@ -949,6 +955,12 @@ impl Parser { } fn parse_primary_datetime_literal(&mut self) -> Result { + 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); + } + } let token = self.advance()?; let original_text = token.text.to_string(); if self.check(TokenType::String) { @@ -1034,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, @@ -1161,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, @@ -1188,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, @@ -1211,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, @@ -1315,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, @@ -1475,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 55bfc3698..bc4053d65 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,11 @@ impl Parser { let Some(end) = self.named_type_literal_end() else { return Ok(None); }; + 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() .map(|token| match token.token_type { @@ -153,6 +158,7 @@ impl Parser { }, trailing_comments: Vec::new(), double_colon_syntax: false, + typed_literal: true, format: None, default: None, inferred_type: None, @@ -161,6 +167,18 @@ impl Parser { )))) } + /// 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::expressions::types::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/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs index 6fb860578..4383a3780 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 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: true, + }, + AcceptanceTestCase { + 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: true, + }, + 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)' ;", 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/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, []) 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 +```