diff --git a/cases/translation/functions/cast.cast-as.toml b/cases/translation/functions/cast.cast-as.toml index b03f6cec5..b21db451d 100644 --- a/cases/translation/functions/cast.cast-as.toml +++ b/cases/translation/functions/cast.cast-as.toml @@ -60,3 +60,85 @@ rows = [ query = "SELECT CAST(x AS NUMBER) AS r FROM input ORDER BY ordinal" order = "ordered" tags = ["null"] + +[[case]] +id = "t006_text_to_binary" +description = "Hex text in upper, lower and mixed case cast to BINARY under BINARY_INPUT_FORMAT HEX. Catches: the text's UTF-8 bytes taken instead of its hex digits, or case-sensitive hex" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'ABCD'"], + ["'00ff10'"], + ["'4a4B'"], +] +query = "SELECT CAST(v AS BINARY) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t007_binary_empty_and_null" +description = "Empty text and NULL cast to BINARY. Catches: empty text read as NULL, or NULL as empty" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["''"], + ["NULL"], +] +query = "SELECT CAST(v AS BINARY) AS r, CAST(v AS BINARY) IS NULL AS n FROM input ORDER BY ordinal" +order = "ordered" +tags = ["boundary"] + +[[case]] +id = "t008_binary_odd_length" +description = "Hex text of odd length cast to BINARY; no page says what an odd digit count does, so the receipt decides. Catches: a padded or truncated byte" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'ABC'"], +] +query = "SELECT CAST(v AS BINARY) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["error"] + +[[case]] +id = "t009_binary_invalid_digit" +description = "Text with a non-hex character cast to BINARY. Catches: the character skipped or read as 0" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'0G'"], +] +query = "SELECT CAST(v AS BINARY) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["error"] + +[[case]] +id = "t010_binary_length" +description = "Hex text of two and three bytes cast to BINARY(2), with its length; Snowflake raises for the three-byte value, failing the query. Catches: the excess bytes silently truncated, or the declared length ignored" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'ABCD'"], + ["'ABCDEF'"], +] +query = "SELECT CAST(v AS BINARY(2)) AS r, LENGTH(CAST(v AS BINARY(2))) AS l FROM input ORDER BY ordinal" +order = "ordered" +tags = ["error"] + +[[case]] +id = "t011_binary_to_binary_fits" +description = "A BINARY value cast to a BINARY(n) it fits, with its length. Catches: the bytes changed or re-read as hex text" +columns = [{ name = "b", type = "BINARY" }] +rows = [ + ["'AABB'"], + ["NULL"], +] +query = "SELECT CAST(b AS BINARY(2)) AS r, LENGTH(CAST(b AS BINARY(2))) AS l FROM input ORDER BY ordinal" +order = "ordered" +tags = ["boundary"] + +[[case]] +id = "t012_binary_to_shorter_binary" +description = "A three-byte BINARY value cast to BINARY(2); a BINARY value raises 100145 here, exactly as the cast of hex text does (t010). Catches: the excess byte silently truncated" +columns = [{ name = "b", type = "BINARY" }] +rows = [ + ["'AABBCC'"], +] +query = "SELECT CAST(b AS BINARY(2)) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["error"] diff --git a/cases/translation/functions/cast.double-colon.toml b/cases/translation/functions/cast.double-colon.toml index 89abddedb..70cca7bcb 100644 --- a/cases/translation/functions/cast.double-colon.toml +++ b/cases/translation/functions/cast.double-colon.toml @@ -60,3 +60,38 @@ rows = [ query = "SELECT x::NUMBER AS r FROM input ORDER BY ordinal" order = "ordered" tags = ["null"] + +[[case]] +id = "t006_text_to_binary" +description = "Hex text cast to BINARY with ::, beside NULL. Catches: :: read differently from CAST" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'00ff10'"], + ["NULL"], +] +query = "SELECT v::BINARY AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] + +[[case]] +id = "t007_binary_to_binary_fits" +description = "A BINARY value cast with :: to a BINARY(n) it fits, with its length, beside NULL. Catches: :: read differently from CAST, or the bytes re-read as hex text" +columns = [{ name = "b", type = "BINARY" }] +rows = [ + ["'AABB'"], + ["NULL"], +] +query = "SELECT b::BINARY(2) AS r, LENGTH(b::BINARY(2)) AS l FROM input ORDER BY ordinal" +order = "ordered" +tags = ["boundary"] + +[[case]] +id = "t008_binary_to_shorter_binary" +description = "A three-byte BINARY value cast with :: to BINARY(2); :: raises 100145 here, exactly as CAST does (cast-as t012). Catches: the excess byte silently truncated" +columns = [{ name = "b", type = "BINARY" }] +rows = [ + ["'AABBCC'"], +] +query = "SELECT b::BINARY(2) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["error"] diff --git a/cases/translation/functions/try-cast.toml b/cases/translation/functions/try-cast.toml index 439d6c2ca..5ce60779a 100644 --- a/cases/translation/functions/try-cast.toml +++ b/cases/translation/functions/try-cast.toml @@ -58,3 +58,17 @@ rows = [ query = "SELECT TRY_CAST(v AS NUMBER) AS r FROM input ORDER BY ordinal" order = "ordered" tags = ["null"] + +[[case]] +id = "t006_text_to_binary" +description = "TRY_CAST of valid hex, odd-length hex, a non-hex character and NULL to BINARY. Catches: an error instead of NULL, or a partial value" +columns = [{ name = "v", type = "VARCHAR" }] +rows = [ + ["'4a4B'"], + ["'ABC'"], + ["'0G'"], + ["NULL"], +] +query = "SELECT TRY_CAST(v AS BINARY) AS r FROM input ORDER BY ordinal" +order = "ordered" +tags = ["type-combination"] diff --git a/coverage/translation/obligations/functions.toml b/coverage/translation/obligations/functions.toml index e622cc5e4..11d4262d2 100644 --- a/coverage/translation/obligations/functions.toml +++ b/coverage/translation/obligations/functions.toml @@ -5692,6 +5692,84 @@ reading = "the page: converts the expression to the target type; a scale too sma cases = ["cases/translation/functions/cast.cast-as.toml#t005_null_source_expr"] required = true +[[obligation]] +id = "sf.functions.cast.cast-as.t006" +form = "sf.functions.cast.cast-as" +kind = "type_combination" +target = "VARCHAR to BINARY" +note = "Hex text in upper, lower and mixed case cast to BINARY under BINARY_INPUT_FORMAT HEX." +catches = "the text's UTF-8 bytes taken instead of its hex digits, or the hex read case-sensitively" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.cast-as.toml#t006_text_to_binary"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t007" +form = "sf.functions.cast.cast-as" +kind = "boundary" +subkind = "empty_string" +target = "empty_string" +note = "Empty text and NULL cast to BINARY." +catches = "empty text read as NULL, or NULL as an empty value" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.cast-as.toml#t007_binary_empty_and_null"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t008" +form = "sf.functions.cast.cast-as" +kind = "error_parity" +target = "error" +note = "Hex text of odd length cast to BINARY. No page says what an odd digit count does; each byte being two digits, the receipt shows Snowflake raises (100115)." +catches = "a padded or truncated byte returned instead of the error" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.cast-as.toml#t008_binary_odd_length"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t009" +form = "sf.functions.cast.cast-as" +kind = "error_parity" +target = "error" +note = "Text with a non-hex character cast to BINARY, which Snowflake raises (100115)." +catches = "the character skipped or read as 0" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.cast-as.toml#t009_binary_invalid_digit"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t010" +form = "sf.functions.cast.cast-as" +kind = "error_parity" +target = "error" +note = "Two- and three-byte hex text cast to BINARY(2); the three-byte value raises (100145) and fails the query." +catches = "the excess bytes silently truncated, or the declared length ignored" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the text data types page: BINARY(N) declares the maximum number of bytes; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.cast-as.toml#t010_binary_length"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t011" +form = "sf.functions.cast.cast-as" +kind = "type_combination" +target = "BINARY to BINARY(n)" +note = "A two-byte BINARY value and NULL cast to BINARY(2), which it exactly fills, with the length." +catches = "the bytes changed or re-read as hex text, the length not 2, or the NULL lost" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the text data types page: BINARY(N) declares the maximum number of bytes" +cases = ["cases/translation/functions/cast.cast-as.toml#t011_binary_to_binary_fits"] +required = true + +[[obligation]] +id = "sf.functions.cast.cast-as.t012" +form = "sf.functions.cast.cast-as" +kind = "error_parity" +target = "error" +note = "A three-byte BINARY value cast to BINARY(2); the receipt shows Snowflake raises 100145 (22000), as it does for hex text (t010)." +catches = "the excess byte silently truncated instead of the error" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the text data types page: BINARY(N) declares the maximum number of bytes" +cases = ["cases/translation/functions/cast.cast-as.toml#t012_binary_to_shorter_binary"] +required = true + [[obligation]] id = "sf.functions.cast.double-colon.t001" form = "sf.functions.cast.double-colon" @@ -5748,6 +5826,39 @@ reading = "the page: converts the expression to the target type; a scale too sma cases = ["cases/translation/functions/cast.double-colon.toml#t005_null_source_expr"] required = true +[[obligation]] +id = "sf.functions.cast.double-colon.t006" +form = "sf.functions.cast.double-colon" +kind = "type_combination" +target = "VARCHAR to BINARY" +note = "Hex text cast to BINARY with ::, beside NULL." +catches = ":: read differently from CAST, or the NULL not kept" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/cast.double-colon.toml#t006_text_to_binary"] +required = true + +[[obligation]] +id = "sf.functions.cast.double-colon.t007" +form = "sf.functions.cast.double-colon" +kind = "type_combination" +target = "BINARY to BINARY(n)" +note = "A two-byte BINARY value and NULL cast with :: to BINARY(2), which it exactly fills, with the length." +catches = ":: read differently from CAST, the bytes re-read as hex text, the length not 2, or the NULL lost" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the text data types page: BINARY(N) declares the maximum number of bytes" +cases = ["cases/translation/functions/cast.double-colon.toml#t007_binary_to_binary_fits"] +required = true + +[[obligation]] +id = "sf.functions.cast.double-colon.t008" +form = "sf.functions.cast.double-colon" +kind = "error_parity" +target = "error" +note = "A three-byte BINARY value cast with :: to BINARY(2); the receipt shows Snowflake raises 100145 (22000), as CAST does (cast-as t012)." +catches = "the excess byte silently truncated instead of the error" +reading = "the page: converts the expression to the target type; a scale too small to hold the value rounds it; a precision too small raises an error; the text data types page: BINARY(N) declares the maximum number of bytes" +cases = ["cases/translation/functions/cast.double-colon.toml#t008_binary_to_shorter_binary"] +required = true + [[obligation]] id = "sf.functions.cbrt.t001" form = "sf.functions.cbrt" @@ -30934,6 +31045,17 @@ reading = "the page: like CAST, but NULL where the conversion fails; only string cases = ["cases/translation/functions/try-cast.toml#t005_null_source_string_expr"] required = true +[[obligation]] +id = "sf.functions.try-cast.t006" +form = "sf.functions.try-cast" +kind = "type_combination" +target = "VARCHAR to BINARY" +note = "Valid hex, odd-length hex, a non-hex character and NULL under TRY_CAST to BINARY. The page does not list BINARY among the targets; the receipt shows Snowflake accepts it and returns NULL for the two invalid values." +catches = "an error raised instead of NULL, or a partial value" +reading = "the page: like CAST, but NULL where the conversion fails; only string expressions are accepted, to VARCHAR, NUMBER, DOUBLE, BOOLEAN, DATE, TIME or a timestamp type; the binary input and output page: under BINARY_INPUT_FORMAT HEX (the default) each byte is two hex digits, 0 to 9 and A to F, and text to binary is case-insensitive" +cases = ["cases/translation/functions/try-cast.toml#t006_text_to_binary"] +required = true + [[obligation]] id = "sf.functions.try-decrypt.t001" form = "sf.functions.try-decrypt" diff --git a/coverage/translation/results-baseline.json b/coverage/translation/results-baseline.json index 1f2b8cd77..d9e3b88b8 100644 --- a/coverage/translation/results-baseline.json +++ b/coverage/translation/results-baseline.json @@ -9048,11 +9048,17 @@ "translation/functions/cast.cast-as.toml#t003_text": "matching", "translation/functions/cast.cast-as.toml#t004_precision": "matching_error", "translation/functions/cast.cast-as.toml#t005_null_source_expr": "matching", + "translation/functions/cast.cast-as.toml#t006_text_to_binary": "matching", + "translation/functions/cast.cast-as.toml#t007_binary_empty_and_null": "matching", + "translation/functions/cast.cast-as.toml#t008_binary_odd_length": "matching_error", + "translation/functions/cast.cast-as.toml#t009_binary_invalid_digit": "matching_error", + "translation/functions/cast.cast-as.toml#t010_binary_length": "matching_error", "translation/functions/cast.double-colon.toml#t001_scale": "matching", "translation/functions/cast.double-colon.toml#t002_float_to_integer": "matching", "translation/functions/cast.double-colon.toml#t003_text": "matching", "translation/functions/cast.double-colon.toml#t004_precision": "matching_error", "translation/functions/cast.double-colon.toml#t005_null_source_expr": "matching", + "translation/functions/cast.double-colon.toml#t006_text_to_binary": "matching", "translation/functions/cbrt.toml#t001_float": "refused", "translation/functions/cbrt.toml#t002_fixed": "refused", "translation/functions/cbrt.toml#t003_decfloat": "refused", @@ -9068,7 +9074,7 @@ "translation/functions/ceil.toml#t008_negatives": "refused", "translation/functions/charindex.toml#t001_varchar": "matching", "translation/functions/charindex.toml#t002_start_pos": "refused", - "translation/functions/charindex.toml#t003_binary": "refused", + "translation/functions/charindex.toml#t003_binary": "matching", "translation/functions/charindex.toml#t004_null_expr1": "matching", "translation/functions/charindex.toml#t005_null_expr2": "matching", "translation/functions/charindex.toml#t006_empty_needle": "matching", @@ -9114,16 +9120,16 @@ "translation/functions/compress.toml#t007_unknown_method": "refused", "translation/functions/concat-ws.toml#t001_varchar": "matching", "translation/functions/concat-ws.toml#t002_single": "matching", - "translation/functions/concat-ws.toml#t003_binary": "refused", + "translation/functions/concat-ws.toml#t003_binary": "matching", "translation/functions/concat-ws.toml#t004_null_separator": "matching", "translation/functions/concat-ws.toml#t005_null_expression": "matching", "translation/functions/concat-ws.toml#t006_empty": "matching", "translation/functions/concat.concat-subscript-dot.toml#t001_varchar": "matching", - "translation/functions/concat.concat-subscript-dot.toml#t002_binary": "refused", + "translation/functions/concat.concat-subscript-dot.toml#t002_binary": "matching", "translation/functions/concat.concat-subscript-dot.toml#t003_null_expr": "matching", "translation/functions/concat.concat-subscript-dot.toml#t004_empty": "matching", "translation/functions/concat.concat.toml#t001_varchar": "matching", - "translation/functions/concat.concat.toml#t002_binary": "refused", + "translation/functions/concat.concat.toml#t002_binary": "matching", "translation/functions/concat.concat.toml#t003_null_expr": "matching", "translation/functions/concat.concat.toml#t004_empty": "matching", "translation/functions/concat.concat.toml#t005_single": "matching", @@ -9836,16 +9842,16 @@ "translation/functions/least.toml#t004_date": "matching", "translation/functions/least.toml#t005_null_expr": "matching", "translation/functions/left.toml#t001_varchar": "matching", - "translation/functions/left.toml#t002_binary": "refused", + "translation/functions/left.toml#t002_binary": "matching", "translation/functions/left.toml#t003_null_string_expr": "matching", "translation/functions/left.toml#t004_null_length_expr": "matching", "translation/functions/left.toml#t005_negative": "matching", "translation/functions/length.len.toml#t001_varchar": "matching", - "translation/functions/length.len.toml#t002_binary": "refused", + "translation/functions/length.len.toml#t002_binary": "matching", "translation/functions/length.len.toml#t003_null_expression": "matching", "translation/functions/length.len.toml#t004_empty": "matching", "translation/functions/length.length.toml#t001_varchar": "matching", - "translation/functions/length.length.toml#t002_binary": "refused", + "translation/functions/length.length.toml#t002_binary": "matching", "translation/functions/length.length.toml#t003_null_expression": "matching", "translation/functions/length.length.toml#t004_empty": "matching", "translation/functions/like-all.toml#t001_patterns": "refused", @@ -9922,7 +9928,7 @@ "translation/functions/lpad.toml#t001_varchar": "matching", "translation/functions/lpad.toml#t002_truncate": "matching", "translation/functions/lpad.toml#t003_default_pad": "matching", - "translation/functions/lpad.toml#t004_binary": "refused", + "translation/functions/lpad.toml#t004_binary": "matching", "translation/functions/lpad.toml#t005_null_base": "matching", "translation/functions/lpad.toml#t006_null_length_expr": "matching", "translation/functions/lpad.toml#t007_null_pad": "matching", @@ -10129,10 +10135,10 @@ "translation/functions/object-pick.toml#t001_keys": "refused", "translation/functions/object-pick.toml#t002_array": "refused", "translation/functions/object-pick.toml#t003_null_object": "refused", - "translation/functions/octet-length.toml#t001_varchar": "refused", - "translation/functions/octet-length.toml#t002_binary": "refused", - "translation/functions/octet-length.toml#t003_null_string_or_binary": "refused", - "translation/functions/octet-length.toml#t004_empty": "refused", + "translation/functions/octet-length.toml#t001_varchar": "matching", + "translation/functions/octet-length.toml#t002_binary": "matching", + "translation/functions/octet-length.toml#t003_null_string_or_binary": "matching", + "translation/functions/octet-length.toml#t004_empty": "matching", "translation/functions/parse-ip.toml#t001_inet": "refused", "translation/functions/parse-ip.toml#t002_cidr": "refused", "translation/functions/parse-ip.toml#t003_permissive": "refused", @@ -10216,13 +10222,13 @@ "translation/functions/period-succeeds.toml#t002_null_period1": "refused", "translation/functions/pi.toml#t001_value": "refused", "translation/functions/position.position-in.toml#t001_varchar": "matching", - "translation/functions/position.position-in.toml#t002_binary": "refused", + "translation/functions/position.position-in.toml#t002_binary": "matching", "translation/functions/position.position-in.toml#t003_null_expr1": "matching", "translation/functions/position.position-in.toml#t004_null_expr2": "matching", "translation/functions/position.position-in.toml#t005_empty_needle": "matching", "translation/functions/position.position.toml#t001_varchar": "matching", "translation/functions/position.position.toml#t002_start_pos": "refused", - "translation/functions/position.position.toml#t003_binary": "refused", + "translation/functions/position.position.toml#t003_binary": "matching", "translation/functions/position.position.toml#t004_null_expr1": "matching", "translation/functions/position.position.toml#t005_null_expr2": "matching", "translation/functions/position.position.toml#t006_empty_needle": "matching", @@ -10465,7 +10471,7 @@ "translation/functions/reverse.toml#t003_null_subject": "refused", "translation/functions/reverse.toml#t004_empty": "refused", "translation/functions/right.toml#t001_varchar": "matching", - "translation/functions/right.toml#t002_binary": "refused", + "translation/functions/right.toml#t002_binary": "matching", "translation/functions/right.toml#t003_null_string_expr": "matching", "translation/functions/right.toml#t004_null_length_expr": "matching", "translation/functions/right.toml#t005_negative": "matching", @@ -10517,7 +10523,7 @@ "translation/functions/rpad.toml#t001_varchar": "matching", "translation/functions/rpad.toml#t002_truncate": "matching", "translation/functions/rpad.toml#t003_default_pad": "matching", - "translation/functions/rpad.toml#t004_binary": "refused", + "translation/functions/rpad.toml#t004_binary": "matching", "translation/functions/rpad.toml#t005_null_base": "matching", "translation/functions/rpad.toml#t006_null_length_expr": "matching", "translation/functions/rpad.toml#t007_null_pad": "matching", @@ -10997,7 +11003,7 @@ "translation/functions/strtok.toml#t007_out_of_range": "refused", "translation/functions/substr.substr.toml#t001_varchar": "matching", "translation/functions/substr.substr.toml#t002_no_length": "matching", - "translation/functions/substr.substr.toml#t003_binary": "refused", + "translation/functions/substr.substr.toml#t003_binary": "matching", "translation/functions/substr.substr.toml#t004_start_zero": "matching", "translation/functions/substr.substr.toml#t005_null_base_expr": "matching", "translation/functions/substr.substr.toml#t006_null_start_expr": "matching", @@ -11006,7 +11012,7 @@ "translation/functions/substr.substr.toml#t009_past_end": "matching", "translation/functions/substr.substring.toml#t001_varchar": "matching", "translation/functions/substr.substring.toml#t002_no_length": "matching", - "translation/functions/substr.substring.toml#t003_binary": "refused", + "translation/functions/substr.substring.toml#t003_binary": "matching", "translation/functions/substr.substring.toml#t004_start_zero": "matching", "translation/functions/substr.substring.toml#t005_null_base_expr": "matching", "translation/functions/substr.substring.toml#t006_null_start_expr": "matching", @@ -11340,6 +11346,7 @@ "translation/functions/try-cast.toml#t003_precision": "matching", "translation/functions/try-cast.toml#t004_number_input": "refused", "translation/functions/try-cast.toml#t005_null_source_string_expr": "matching", + "translation/functions/try-cast.toml#t006_text_to_binary": "matching", "translation/functions/try-decrypt-raw.toml#t001_round_trip": "refused", "translation/functions/try-decrypt-raw.toml#t002_wrong_aad": "refused", "translation/functions/try-decrypt-raw.toml#t003_null_value_to_decrypt": "refused", @@ -12952,7 +12959,7 @@ "sf.expressions.data-types-structured.array": "unmeasured", "sf.expressions.data-types-structured.map": "unmeasured", "sf.expressions.data-types-structured.object": "unmeasured", - "sf.expressions.data-types-text.binary": "unmeasured", + "sf.expressions.data-types-text.binary": "matching", "sf.expressions.data-types-text.char": "unmeasured", "sf.expressions.data-types-text.dollar-quoted-string-constant": "matching", "sf.expressions.data-types-text.single-quoted-string-constant": "refused", @@ -13467,7 +13474,7 @@ "sf.functions.embed-text-1024-snowflake-cortex": "not_translatable", "sf.functions.embed-text-snowflake-cortex": "not_translatable", "sf.functions.encrypt": "refused", - "sf.functions.encrypt-raw": "unmeasured", + "sf.functions.encrypt-raw": "refused", "sf.functions.endswith": "matching", "sf.functions.entity-sentiment-snowflake-cortex": "not_translatable", "sf.functions.equal-null": "refused", @@ -13757,7 +13764,7 @@ "sf.functions.object-insert": "refused", "sf.functions.object-keys": "refused", "sf.functions.object-pick": "refused", - "sf.functions.octet-length": "refused", + "sf.functions.octet-length": "matching", "sf.functions.online-feature-table-refresh-history": "not_translatable", "sf.functions.parse-document-snowflake-cortex": "not_translatable", "sf.functions.parse-ip": "refused", diff --git a/crates/sqlquack-core/src/failures/constants.rs b/crates/sqlquack-core/src/failures/constants.rs index d990bb16b..6bb412435 100644 --- a/crates/sqlquack-core/src/failures/constants.rs +++ b/crates/sqlquack-core/src/failures/constants.rs @@ -78,6 +78,11 @@ pub const STRING_TOO_LONG: SnowflakeCode = SnowflakeCode { errno: 100_078, sqlstate: "22000", }; +/// Bytes longer than a BINARY(n) target, which Snowflake does not truncate. +pub const BINARY_TOO_LONG: SnowflakeCode = SnowflakeCode { + errno: 100_145, + sqlstate: "22000", +}; /// A function's array argument is not an array. pub const NOT_AN_ARRAY_ARGUMENT: SnowflakeCode = SnowflakeCode { @@ -167,6 +172,7 @@ pub const SNOWFLAKE_ERRORS: &[(SnowflakeCode, ErrorKind)] = &[ (JSON_NOT_PARSED, ErrorKind::JsonParse), (VARIANT_NOT_CAST, ErrorKind::VariantCast), (STRING_TOO_LONG, ErrorKind::StringTooLong), + (BINARY_TOO_LONG, ErrorKind::StringTooLong), (NOT_AN_ARRAY_ARGUMENT, ErrorKind::InvalidArgument), (OBJECT_KEY_EXISTS, ErrorKind::InvalidArgument), (INVALID_CHARACTER_CODE, ErrorKind::InvalidArgument), diff --git a/crates/sqlquack-duckdb/src/blocks/_helpers/types_map.rs b/crates/sqlquack-duckdb/src/blocks/_helpers/types_map.rs index e08ae20af..95b3975ed 100644 --- a/crates/sqlquack-duckdb/src/blocks/_helpers/types_map.rs +++ b/crates/sqlquack-duckdb/src/blocks/_helpers/types_map.rs @@ -11,6 +11,7 @@ pub(crate) fn duckdb_type(data_type: SnowflakeType, log: &ProvenanceLog) -> Resu SnowflakeType::Number { precision, scale } => Ok(format!("DECIMAL({precision},{scale})")), SnowflakeType::Float => Ok("DOUBLE".to_owned()), SnowflakeType::Varchar => Ok("VARCHAR".to_owned()), + SnowflakeType::Binary => Ok("BLOB".to_owned()), SnowflakeType::Boolean => Ok("BOOLEAN".to_owned()), SnowflakeType::Date => Ok("DATE".to_owned()), SnowflakeType::Time { precision } if precision <= NANOSECOND_DIGITS => { diff --git a/crates/sqlquack-duckdb/src/text/constants.rs b/crates/sqlquack-duckdb/src/text/constants.rs index be7e8feca..4a9232bac 100644 --- a/crates/sqlquack-duckdb/src/text/constants.rs +++ b/crates/sqlquack-duckdb/src/text/constants.rs @@ -21,6 +21,14 @@ pub const CODE_POINT_TEXT: &str = "duckdb.code_point_text"; /// HEX_ENCODE's hexadecimal digits of the UTF-8 bytes: `duckdb.hex_text`. pub const HEX_TEXT: &str = "duckdb.hex_text"; +/// Raising an error for bytes longer than a BINARY(n) target: `duckdb.binary_length_check`. +pub const BINARY_LENGTH_CHECK: &str = "duckdb.binary_length_check"; +/// Hexadecimal text read as bytes, as under BINARY_INPUT_FORMAT HEX: `duckdb.hex_bytes`. +pub const HEX_BYTES: &str = "duckdb.hex_bytes"; +/// What a cast of text that is not hex digits to BINARY raises with. +pub const HEX_BYTES_MESSAGE: &str = "The following string is not a legal hex-encoded value"; +/// What a cast of bytes longer than its BINARY(n) target raises with. +pub const BINARY_TOO_LONG_MESSAGE: &str = "Binary value is too long and would be truncated"; /// TRY_HEX_DECODE_STRING's text from hexadecimal digits: `duckdb.hex_decoded_text`. pub const HEX_DECODED_TEXT: &str = "duckdb.hex_decoded_text"; diff --git a/crates/sqlquack-duckdb/src/text/main/binary_length_check.rs b/crates/sqlquack-duckdb/src/text/main/binary_length_check.rs new file mode 100644 index 000000000..26ae7a7c8 --- /dev/null +++ b/crates/sqlquack-duckdb/src/text/main/binary_length_check.rs @@ -0,0 +1,19 @@ +//! `binary_length_check`: raises an error when bytes are longer than `max_length`, as a BINARY(n) cast does. + +use sqlquack_core::failures::constants::BINARY_TOO_LONG; +use sqlquack_core::failures::main::tagged_message::tagged_message; +use sqlquack_core::provenance::models::ProvenanceLog; + +use crate::raises::main::raised::raised; +use crate::text::constants::{BINARY_LENGTH_CHECK, BINARY_TOO_LONG_MESSAGE}; + +/// Raises an error when bytes are longer than `max_length`, as a BINARY(n) cast does. +pub fn binary_length_check(sql: &str, max_length: u32, log: &ProvenanceLog) -> String { + log.building_block(BINARY_LENGTH_CHECK); + format!( + "list_transform([{sql}], lambda binary_value: CASE \ + WHEN octet_length(binary_value) > {max_length} THEN CAST({} AS BLOB) \ + ELSE binary_value END)[1]", + raised(&tagged_message(BINARY_TOO_LONG, BINARY_TOO_LONG_MESSAGE)) + ) +} diff --git a/crates/sqlquack-duckdb/src/text/main/hex_bytes.rs b/crates/sqlquack-duckdb/src/text/main/hex_bytes.rs new file mode 100644 index 000000000..b68a86e14 --- /dev/null +++ b/crates/sqlquack-duckdb/src/text/main/hex_bytes.rs @@ -0,0 +1,22 @@ +//! `hex_bytes`: text of whole pairs of hexadecimal digits to BINARY, as under BINARY_INPUT_FORMAT HEX. + +use sqlquack_core::failures::constants::HEX_DECODE_FAILED; +use sqlquack_core::failures::main::tagged_message::tagged_message; +use sqlquack_core::provenance::models::ProvenanceLog; + +use crate::raises::main::raised::raised; +use crate::text::constants::{HEX_BYTES, HEX_BYTES_MESSAGE, HEX_PAIRS_PATTERN}; + +/// The bytes hex digits of either case spell; other text raises 100115, or is NULL when `trying`. +pub fn hex_bytes(sql: &str, trying: bool, log: &ProvenanceLog) -> String { + log.building_block(HEX_BYTES); + let failure = match trying { + true => "NULL".to_owned(), + false => raised(&tagged_message(HEX_DECODE_FAILED, HEX_BYTES_MESSAGE)), + }; + format!( + "list_transform([{sql}], lambda hex_text: CASE WHEN hex_text IS NULL THEN NULL \ + WHEN regexp_full_match(hex_text, '{HEX_PAIRS_PATTERN}') THEN unhex(hex_text) \ + ELSE CAST({failure} AS BLOB) END)[1]" + ) +} diff --git a/crates/sqlquack-duckdb/src/text/main/mod.rs b/crates/sqlquack-duckdb/src/text/main/mod.rs index 9f11271e8..b632a2b82 100644 --- a/crates/sqlquack-duckdb/src/text/main/mod.rs +++ b/crates/sqlquack-duckdb/src/text/main/mod.rs @@ -1,6 +1,7 @@ //! One building block per module. pub mod base64_bytes; +pub mod binary_length_check; pub mod bytes_substring; pub mod bytes_text; pub mod character_edit_distance; @@ -10,6 +11,7 @@ pub mod compared_number_range; pub mod compared_number_text; pub mod folded_text; pub mod guarded_pattern; +pub mod hex_bytes; pub mod hex_decoded_text; pub mod hex_text; pub mod json_parse_url; diff --git a/crates/sqlquack-rules/src/catalogue/constants.rs b/crates/sqlquack-rules/src/catalogue/constants.rs index 1d6212b15..2f04350aa 100644 --- a/crates/sqlquack-rules/src/catalogue/constants.rs +++ b/crates/sqlquack-rules/src/catalogue/constants.rs @@ -2,13 +2,13 @@ use crate::catalogue::models::NotExpressibleDeclaration; use sqlquack_core::failures::constants::{ - BOOLEAN_NOT_RECOGNIZED, CHARACTER_CODE_FAILURES, DATE_FORMAT_MISMATCH, DATE_NOT_RECOGNIZED, - DIVISION_BY_ZERO, DIVISION_FAILURES, FLATTEN_FAILURES, HEX_DECODE_FAILURES, - INVALID_UTF8_FAILURES, JSON_NOT_PARSED, KEY_EXISTS_FAILURES, LOGARITHM_FAILURES, - NOT_AN_ARRAY_ARGUMENT, NOT_AN_ARRAY_FAILURES, NUMBER_NOT_RECOGNIZED, NUMBER_OUT_OF_RANGE, - NUMBER_TEXT_OUT_OF_RANGE, OBJECT_KEY_EXISTS, RANGE_STEP_FAILURES, STRING_TOO_LONG, - TIME_TEXT_FAILURES, TIMESTAMP_FORMAT_MISMATCH, TIMESTAMP_NOT_RECOGNIZED, VARIANT_NOT_CAST, - ZONE_FAILURES, + BINARY_TOO_LONG, BOOLEAN_NOT_RECOGNIZED, CHARACTER_CODE_FAILURES, DATE_FORMAT_MISMATCH, + DATE_NOT_RECOGNIZED, DIVISION_BY_ZERO, DIVISION_FAILURES, FLATTEN_FAILURES, HEX_DECODE_FAILED, + HEX_DECODE_FAILURES, INVALID_UTF8_FAILURES, JSON_NOT_PARSED, KEY_EXISTS_FAILURES, + LOGARITHM_FAILURES, NOT_AN_ARRAY_ARGUMENT, NOT_AN_ARRAY_FAILURES, NUMBER_NOT_RECOGNIZED, + NUMBER_OUT_OF_RANGE, NUMBER_TEXT_OUT_OF_RANGE, OBJECT_KEY_EXISTS, RANGE_STEP_FAILURES, + STRING_TOO_LONG, TIME_TEXT_FAILURES, TIMESTAMP_FORMAT_MISMATCH, TIMESTAMP_NOT_RECOGNIZED, + VARIANT_NOT_CAST, ZONE_FAILURES, }; use sqlquack_core::failures::models::{ErrorKind, FailureMode}; use sqlquack_snowflake::typing::constants::VARIANT_COMMON_TYPE; @@ -21,16 +21,17 @@ use crate::conversion::constants::{ ARRAY_CONSTRUCT_COMPACT_RULE, ARRAY_CONSTRUCT_RULE, ARRAY_CONTAINS_RULE, ARRAY_EXCEPT_RULE, ARRAY_EXTREME_RULE, ARRAY_FLATTEN_RULE, ARRAY_POSITION_RULE, ARRAY_RANGE_RULE, ARRAY_REVERSE_RULE, ARRAY_SET_RULE, ARRAY_SIZE_RULE, ARRAY_SLICE_RULE, ARRAY_SORT_RULE, - ARRAY_TO_STRING_RULE, ASOF_JOIN, AVERAGE, BASE64_BINARY, BETWEEN, BITWISE, CASE, CASE_MAPPING, - CAST_BOOLEAN, CAST_NUMERIC, CAST_PARSED_TEXT, CAST_SAME_TYPE, CAST_TEMPORAL, CAST_TEXT_LITERAL, - CAST_TEXT_TIME, CAST_TEXT_TIMESTAMP, CAST_TO_TEXT, CAST_UTC_ZONED, COALESCE, COLUMN, COMMENT, - COMMON_TABLE, COMPARISON, COMPUTED_SUBSTR_LENGTH, CONCAT, CONCAT_WS, CONSTANT_LITERAL, COUNT, - CURRENT_TIME_RULE, DATE_ARITHMETIC, DATE_DAY_ARITHMETIC, DATE_FORMAT_RULE, - DATE_LITERAL_COMPARISON, DATE_PART_RULE, DAY_NAME_RULE, DECIMAL_ARITHMETIC, DECIMAL_DIVISION, - DECIMAL_EPOCH_RULE, DECIMAL_MODULO, DECODE_RULE, DISTINCT, DIV0_RULE, EDIT_DISTANCE, EXISTS, - FLATTEN_RULE, FLOAT_ARITHMETIC, FLOAT_MODULO, FLOAT_TO_DOUBLE, FRACTIONAL_DATE_AMOUNT, - FROM_PARTS, FROM_RELATION, FROM_SUBQUERY, GENERATOR_RULE, GROUP_BY, GROUP_BY_ALL_RULE, - HASH_RULE, HAVING, HIERARCHY_RULE, IFF, ILIKE_FOLDING, IN_LIST, IN_SUBQUERY, + ARRAY_TO_STRING_RULE, ASOF_JOIN, AVERAGE, BASE64_BINARY, BETWEEN, BITWISE, BYTE_CONCAT, + BYTE_LENGTH, BYTE_PAD, BYTE_SEARCH, BYTE_SLICE, CASE, CASE_MAPPING, CAST_BOOLEAN, + CAST_HEX_BINARY, CAST_NUMERIC, CAST_PARSED_TEXT, CAST_SAME_TYPE, CAST_TEMPORAL, + CAST_TEXT_LITERAL, CAST_TEXT_TIME, CAST_TEXT_TIMESTAMP, CAST_TO_TEXT, CAST_UTC_ZONED, COALESCE, + COLUMN, COMMENT, COMMON_TABLE, COMPARISON, COMPUTED_SUBSTR_LENGTH, CONCAT, CONCAT_WS, + CONSTANT_LITERAL, COUNT, CURRENT_TIME_RULE, DATE_ARITHMETIC, DATE_DAY_ARITHMETIC, + DATE_FORMAT_RULE, DATE_LITERAL_COMPARISON, DATE_PART_RULE, DAY_NAME_RULE, DECIMAL_ARITHMETIC, + DECIMAL_DIVISION, DECIMAL_EPOCH_RULE, DECIMAL_MODULO, DECODE_RULE, DISTINCT, DIV0_RULE, + EDIT_DISTANCE, EXISTS, FLATTEN_RULE, FLOAT_ARITHMETIC, FLOAT_MODULO, FLOAT_TO_DOUBLE, + FRACTIONAL_DATE_AMOUNT, FROM_PARTS, FROM_RELATION, FROM_SUBQUERY, GENERATOR_RULE, GROUP_BY, + GROUP_BY_ALL_RULE, HASH_RULE, HAVING, HIERARCHY_RULE, IFF, ILIKE_FOLDING, IN_LIST, IN_SUBQUERY, INTERVAL_ARITHMETIC, IS_NULL, JARO_WINKLER, JOIN_CROSS, JOIN_ON, JOIN_USING, LAMBDA_RULE, LAST_DAY_RULE, LATERAL_SUBQUERY, LEAST_GREATEST, LIKE, LIMIT, LISTAGG_RULE, LOGARITHM, LOGIC, MEDIAN_RULE, MIN_MAX, MIN_MAX_FLOAT, MODE_RULE, NAMED_AGGREGATES, NEGATE, NOT_IN_SUBQUERY, @@ -43,7 +44,7 @@ use crate::conversion::constants::{ REGEXP_POSIX_SPACE, ROUNDING, SCALAR_SUBQUERY, SELECT, SET_OPERATION, SIGN_RULE, SPLIT_RULE, SPLIT_TO_TABLE_RULE, STAR, SUM, SUM_FLOAT, SYSTEM_TYPE_NAME, TEMPORAL_TEXT_FORMAT, TEXT_AFFIX, TEXT_BRANCHES, TEXT_CHR, TEXT_CONTAINS, TEXT_HEX, TEXT_HEX_DECODE, TEXT_INITCAP, TEXT_MD5, - TEXT_NUMBER_COMPARISON, TEXT_PAD, TEXT_POSITION, TEXT_REPEAT, TEXT_REPLACE, + TEXT_NUMBER_COMPARISON, TEXT_OCTET_LENGTH, TEXT_PAD, TEXT_POSITION, TEXT_REPEAT, TEXT_REPLACE, TEXT_SEARCH_POSITION, TEXT_SHA2, TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TEXT_TRANSLATE, TIME_ARITHMETIC, TIME_CAST, TIME_ZONE_CONVERSION, TIMESTAMP_FORMAT_RULE, TIMESTAMP_TIME_RULE, TO_ARRAY_RULE, TO_BOOLEAN_RULE, TO_DATE_TEMPORAL, TO_TIMESTAMP_TEMPORAL, TO_VARIANT_RULE, TRIM, @@ -456,6 +457,21 @@ pub const RULES: &[RuleSpec] = &[ }, ], }, + RuleSpec { + id: CAST_HEX_BINARY, + constructs: &["node:cast", "node:try_cast"], + conditions: "VARCHAR to BINARY or BINARY(n) under the session profile's pinned BINARY_INPUT_FORMAT HEX: whole pairs of hexadecimal digits of either case, as recorded; empty text gives empty bytes and NULL gives NULL. An odd count of digits or any other character fails with error 100115, or is NULL under TRY_CAST. More bytes than BINARY(n) holds fail the query with error 100145 rather than being truncated, as recorded for text; a value already BINARY cast to BINARY(n) is checked the same way. TRY_CAST to BINARY(n) is refused, since no recording shows it.", + failures: &[ + FailureMode { + kind: ErrorKind::InvalidArgument, + snowflake: &[HEX_DECODE_FAILED], + }, + FailureMode { + kind: ErrorKind::StringTooLong, + snowflake: &[BINARY_TOO_LONG], + }, + ], + }, RuleSpec { id: CAST_TEXT_LITERAL, constructs: &["node:cast", "node:try_cast"], @@ -1196,10 +1212,10 @@ pub const RULES: &[RuleSpec] = &[ RuleSpec { id: CAST_SAME_TYPE, constructs: &["node:cast", "node:try_cast"], - conditions: "The operand already has the target type, or is NULL. TRY_CAST of a NUMBER to its own NUMBER type is the value, as recorded; to another NUMBER type Snowflake rejects it (error 1065), which refuses.", + conditions: "The operand already has the target type, or is NULL. TRY_CAST of a NUMBER to its own NUMBER type is the value, as recorded; to another NUMBER type Snowflake rejects it (error 1065), which refuses. A BINARY value cast to a shorter BINARY(n) fails with error 100145, as text does with 100078.", failures: &[FailureMode { kind: ErrorKind::StringTooLong, - snowflake: &[STRING_TOO_LONG], + snowflake: &[STRING_TOO_LONG, BINARY_TOO_LONG], }], }, RuleSpec { @@ -1232,6 +1248,48 @@ pub const RULES: &[RuleSpec] = &[ conditions: "VARCHAR, NUMBER, FLOAT, BOOLEAN or DATE operands; NUMBER, BOOLEAN and DATE are formatted as Snowflake does, FLOAT as its ::VARCHAR text, recorded over many rows; JSON-held VARIANTs as ::VARCHAR writes them, relying on json_fidelity. Text constants concatenate to a text constant.", failures: &[], }, + RuleSpec { + id: BYTE_LENGTH, + constructs: &["fn:LENGTH", "fn:LEN", "fn:OCTET_LENGTH"], + conditions: "A BINARY operand: its count of bytes, not of hex digits, typed NUMBER(18,0), as recorded; NULL for NULL.", + failures: &[], + }, + RuleSpec { + id: TEXT_OCTET_LENGTH, + constructs: &["fn:OCTET_LENGTH"], + conditions: "A VARCHAR operand: its count of UTF-8 bytes, typed NUMBER(18,0), as recorded for multi-byte and empty text; NULL for NULL.", + failures: &[], + }, + RuleSpec { + id: BYTE_SLICE, + constructs: &["fn:LEFT", "fn:RIGHT", "fn:SUBSTR", "fn:SUBSTRING"], + conditions: "A BINARY operand and NUMBER(p,0) positions and lengths, counted in bytes, as recorded: LEFT and RIGHT keep the first or last bytes, all of them for a longer length; SUBSTR starts at a byte position of 1 or more. A negative length or a start below 1, which no BINARY recording reaches, raises an error instead of a result; NULL arguments give NULL.", + failures: &[FailureMode { + kind: ErrorKind::NumericOverflow, + snowflake: &[NUMBER_OUT_OF_RANGE], + }], + }, + RuleSpec { + id: BYTE_PAD, + constructs: &["fn:LPAD", "fn:RPAD"], + conditions: "A BINARY operand and BINARY padding with an integer NUMBER length, counted in bytes: shorter bytes are padded with the padding repeated from its first byte, before them for LPAD and after them for RPAD, as recorded; longer bytes are cut to the length. A negative length or empty padding, which no BINARY recording reaches, raises an error; LPAD and RPAD of BINARY without padding are refused; NULL arguments give NULL.", + failures: &[FailureMode { + kind: ErrorKind::NumericOverflow, + snowflake: &[NUMBER_OUT_OF_RANGE], + }], + }, + RuleSpec { + id: BYTE_SEARCH, + constructs: &["fn:CHARINDEX", "fn:POSITION", "node:str_position"], + conditions: "Two BINARY operands, without a start position: the first byte position of one in the other counted from 1, matching whole bytes only, 0 when absent, typed NUMBER(9,0), as recorded; an empty search, which no BINARY recording reaches, raises an error; NULL operands give NULL.", + failures: &[], + }, + RuleSpec { + id: BYTE_CONCAT, + constructs: &["node:concat", "fn:CONCAT", "fn:CONCAT_WS"], + conditions: "BINARY operands only: their bytes in order, CONCAT_WS with the separator's bytes between parts, as recorded for CONCAT, || and CONCAT_WS; NULL when any operand is NULL. BINARY beside another type, and CONCAT_WS of a single BINARY part, are refused.", + failures: &[], + }, RuleSpec { id: COUNT, constructs: &["fn:COUNT"], diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/bytes.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/bytes.rs new file mode 100644 index 000000000..b533b9293 --- /dev/null +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/bytes.rs @@ -0,0 +1,211 @@ +//! LENGTH, LEFT, RIGHT, SUBSTR, LPAD, RPAD, POSITION and concatenation of BINARY, byte by byte. + +use serde_json::Value; +use sqlquack_core::classification::models::Construct; +use sqlquack_core::refusals::main::outside_conditions::outside_conditions; +use sqlquack_core::refusals::models::Refusal; +use sqlquack_duckdb::blocks::main::null_propagating_concat::null_propagating_concat; +use sqlquack_duckdb::raises::main::raised::raised; +use sqlquack_snowflake::typing::models::SnowflakeType; + +use crate::conversion::_helpers::expressions::scalar::convert_expression; +use crate::conversion::_helpers::expressions::strings::emitted_text_operand; +use crate::conversion::_helpers::expressions::text_functions::integer_operand; +use crate::conversion::_helpers::support::scope::Emitted; +use crate::conversion::_helpers::support::site::Site; +use crate::conversion::constants::{ + BYTE_CONCAT, BYTE_LENGTH, BYTE_PAD, BYTE_SEARCH, BYTE_SLICE, LENGTH_TYPE, TEXT_OCTET_LENGTH, + TEXT_POSITION_LIMIT, TEXT_SEARCH_POSITION_TYPE, UNRECORDED_BYTES, +}; + +/// The DuckDB type BINARY is held in. +const BLOB: &str = "BLOB"; + +/// LENGTH, LEN or OCTET_LENGTH of BINARY: its count of bytes, typed NUMBER(18,0). +pub(crate) fn byte_length(bytes: &Emitted, site: Site<'_>) -> Emitted { + let _rule = site.context.log.rule(BYTE_LENGTH); + Emitted::new( + format!("CAST(octet_length({}) AS DECIMAL(18,0))", bytes.sql), + LENGTH_TYPE, + ) +} + +/// OCTET_LENGTH: the count of bytes of BINARY, or of VARCHAR's UTF-8 bytes. +pub(crate) fn octet_length( + value: &Value, + construct: &Construct, + site: Site<'_>, +) -> Result { + let value = convert_expression(value, site)?; + if value.data_type == SnowflakeType::Binary { + return Ok(byte_length(&value, site)); + } + let _rule = site.context.log.rule(TEXT_OCTET_LENGTH); + let text = emitted_text_operand(value, construct, site)?; + Ok(Emitted::new( + format!("CAST(octet_length(encode({text})) AS DECIMAL(18,0))"), + LENGTH_TYPE, + )) +} + +/// LEFT or RIGHT of BINARY: its first or last `length` bytes; a negative length raises. +pub(crate) fn byte_left_right( + name: &str, + (bytes, length): (&Emitted, &Value), + construct: &Construct, + site: Site<'_>, +) -> Result { + let _rule = site.context.log.rule(BYTE_SLICE); + let length = integer_operand(length, construct, site)?; + Ok(Emitted::new( + format!( + "list_transform([struct_pack(b := {}, n := {length})], lambda p: CASE \ + WHEN p['n'] < 0 THEN {} \ + ELSE unhex({}(hex(p['b']), 2 * {})) END)[1]", + bytes.sql, + unrecorded("a negative length", BLOB), + name.to_ascii_lowercase(), + clamped("p['n']") + ), + SnowflakeType::Binary, + )) +} + +/// SUBSTR or SUBSTRING of BINARY from a byte position of 1 or more; other positions raise. +pub(crate) fn byte_substring( + (bytes, start, length): (&Emitted, &Value, Option<&Value>), + construct: &Construct, + site: Site<'_>, +) -> Result { + let _rule = site.context.log.rule(BYTE_SLICE); + let start = integer_operand(start, construct, site)?; + let (length, digits) = match length { + Some(length) => ( + integer_operand(length, construct, site)?, + format!(", 2 * {}", clamped("p['n']")), + ), + None => ("0".to_owned(), String::new()), + }; + Ok(Emitted::new( + format!( + "list_transform([struct_pack(b := {}, s := {start}, n := {length})], lambda p: CASE \ + WHEN p['s'] < 1 THEN {} WHEN p['n'] < 0 THEN {} \ + ELSE unhex(substring(hex(p['b']), 2 * {} - 1{digits})) \ + END)[1]", + bytes.sql, + unrecorded("a start position below 1", BLOB), + unrecorded("a negative length", BLOB), + clamped("p['s']"), + ), + SnowflakeType::Binary, + )) +} + +/// LPAD or RPAD of BINARY with BINARY padding, repeated from its first byte, or cut to the length. +pub(crate) fn byte_pad( + name: &str, + (bytes, length, filler): (&Emitted, &Value, Option<&Emitted>), + construct: &Construct, + site: Site<'_>, +) -> Result { + let _rule = site.context.log.rule(BYTE_PAD); + let Some(filler) = filler.filter(|filler| is_bytes(filler)) else { + return Err(outside_conditions( + construct.clone(), + format!("{name} of BINARY without BINARY padding, which no recording shows"), + )); + }; + let length = integer_operand(length, construct, site)?; + Ok(Emitted::new( + format!( + "list_transform([struct_pack(b := {}, n := {length}, f := {})], lambda p: CASE \ + WHEN p['n'] < 0 THEN {} WHEN octet_length(p['f']) = 0 THEN {} \ + ELSE unhex({}(hex(p['b']), CAST(2 * {} AS INTEGER), hex(p['f']))) \ + END)[1]", + bytes.sql, + filler.sql, + unrecorded("a negative length", BLOB), + unrecorded("empty padding", BLOB), + name.to_ascii_lowercase(), + clamped("p['n']") + ), + SnowflakeType::Binary, + )) +} + +/// The first byte position of BINARY `needle` in BINARY `haystack`, 0 when absent. +pub(crate) fn byte_position( + (needle, haystack): (&Emitted, &Emitted), + construct: &Construct, + site: Site<'_>, +) -> Result { + let _rule = site.context.log.rule(BYTE_SEARCH); + if !is_bytes(needle) || !is_bytes(haystack) { + return Err(mixed(construct)); + } + Ok(Emitted::new( + format!( + "list_transform([struct_pack(n := hex({}), h := hex({}))], lambda p: CASE \ + WHEN p['n'] IS NULL OR p['h'] IS NULL THEN NULL WHEN p['n'] = '' THEN {} \ + ELSE CAST(coalesce(list_position(list_transform(\ + range(0, (length(p['h']) - length(p['n'])) // 2 + 1), \ + lambda i: substring(p['h'], 2 * i + 1, length(p['n'])) = p['n']), true), 0) \ + AS DECIMAL(9,0)) END)[1]", + needle.sql, + haystack.sql, + unrecorded("an empty search", "DECIMAL(9,0)"), + ), + TEXT_SEARCH_POSITION_TYPE, + )) +} + +/// CONCAT, `||` or CONCAT_WS of BINARY parts: their bytes in order, NULL when any part is. +pub(crate) fn byte_concat( + parts: &[&Emitted], + construct: &Construct, + site: Site<'_>, +) -> Result { + let _rule = site.context.log.rule(BYTE_CONCAT); + if !parts.iter().all(|part| is_bytes(part)) { + return Err(mixed(construct)); + } + let parts: Vec = parts + .iter() + .map(|part| format!("CAST({} AS BLOB)", part.sql)) + .collect(); + Ok(Emitted::new( + null_propagating_concat(&parts, site.context.log), + SnowflakeType::Binary, + )) +} + +/// Whether `emitted` holds BINARY, or is an untyped NULL. +pub(crate) fn is_bytes(emitted: &Emitted) -> bool { + matches!( + emitted.data_type, + SnowflakeType::Binary | SnowflakeType::Null + ) +} + +fn mixed(construct: &Construct) -> Refusal { + outside_conditions( + construct.clone(), + "BINARY beside another type, which needs an implicit conversion no recording shows" + .to_owned(), + ) +} + +/// An error raised for BINARY values the recordings do not reach, rather than a guessed result. +fn unrecorded(what: &str, held: &str) -> String { + format!( + "CAST({} AS {held})", + raised(&format!("{UNRECORDED_BYTES}: {what}")) + ) +} + +/// A byte count or position cut to the largest Snowflake reads, NULL staying NULL. +fn clamped(count: &str) -> String { + format!( + "(CASE WHEN {count} > {TEXT_POSITION_LIMIT} THEN {TEXT_POSITION_LIMIT} ELSE {count} END)" + ) +} diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs index a10eb1062..83323f9cd 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs @@ -21,6 +21,7 @@ use sqlquack_snowflake::typing::main::sum::sum; use sqlquack_snowflake::typing::models::SnowflakeType; use crate::conversion::_helpers::expressions::arrays::array_function; +use crate::conversion::_helpers::expressions::bytes::octet_length; use crate::conversion::_helpers::expressions::casts::zoned_text; use crate::conversion::_helpers::expressions::casts::{ to_boolean_call, to_double_call, to_number_call, to_varchar_call, @@ -172,6 +173,7 @@ pub(crate) fn convert_function(payload: &Value, site: Site<'_>) -> Result>(), &construct, site) } ("CONCAT_WS", parts) => concatenate_with_separator(parts, &construct, site), + ("OCTET_LENGTH", [value]) => octet_length(value, &construct, site), ("CONTAINS", parts) => text_contains(parts, &construct, site), ("EDITDISTANCE", parts) => edit_distance(parts, &construct, site), ("JAROWINKLER_SIMILARITY", parts) => jaro_winkler(parts, &construct, site), diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/casts.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/casts.rs index d55811335..3807ff0b2 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/casts.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/casts.rs @@ -25,6 +25,8 @@ use sqlquack_duckdb::temporal::main::text_to_timestamp::text_to_timestamp; use sqlquack_duckdb::temporal::main::text_to_zoned::text_to_zoned; use sqlquack_duckdb::temporal::main::utc_to_zone::utc_to_zone; use sqlquack_duckdb::temporal::main::utc_wall_time::utc_wall_time; +use sqlquack_duckdb::text::main::binary_length_check::binary_length_check; +use sqlquack_duckdb::text::main::hex_bytes::hex_bytes; use sqlquack_duckdb::times::main::text_to_time::text_to_time; use sqlquack_duckdb::times::main::time_text::time_text; use sqlquack_duckdb::times::main::timestamp_time::timestamp_time; @@ -59,13 +61,13 @@ use crate::conversion::_helpers::support::tree::{ LITERAL_NUMBER, field, node_construct, node_kind, payload_object, require_understood, }; use crate::conversion::constants::{ - CAST_BOOLEAN, CAST_NUMERIC, CAST_PARSED_TEXT, CAST_SAME_TYPE, CAST_TEMPORAL, CAST_TEXT_LITERAL, - CAST_TEXT_TIME, CAST_TEXT_TIMESTAMP, CAST_TO_TEXT, CAST_UTC_ZONED, FLOAT_SCALE_FACET, - FLOAT_TO_DOUBLE, KEEP_NULL_CONSTRUCTOR, KIND_CAST_TARGETS, MICROSECOND_DIGITS, - NTZ_OUTPUT_PATTERN, OBJECT_CONSTRUCTOR, OBJECT_TYPE_NAME, TARGET_FACET, TEXT_TYPE_PAIR, - TEXT_TYPE_SPELLINGS, TIME_CAST, TIMESTAMP_TIME_RULE, TO_BOOLEAN_RULE, TO_DOUBLE_PAIR, - TO_NUMBER_PAIR, TO_VARCHAR_PAIR, TO_VARIANT_RULE, VARIANT_CAST, VARIANT_KIND_CAST, - VARIANT_NAME, VARIANT_TEXT_FUNCTION, VARIANT_TO_VARCHAR, + CAST_BOOLEAN, CAST_HEX_BINARY, CAST_NUMERIC, CAST_PARSED_TEXT, CAST_SAME_TYPE, CAST_TEMPORAL, + CAST_TEXT_LITERAL, CAST_TEXT_TIME, CAST_TEXT_TIMESTAMP, CAST_TO_TEXT, CAST_UTC_ZONED, + FLOAT_SCALE_FACET, FLOAT_TO_DOUBLE, KEEP_NULL_CONSTRUCTOR, KIND_CAST_TARGETS, + MICROSECOND_DIGITS, NTZ_OUTPUT_PATTERN, OBJECT_CONSTRUCTOR, OBJECT_TYPE_NAME, TARGET_FACET, + TEXT_TYPE_PAIR, TEXT_TYPE_SPELLINGS, TIME_CAST, TIMESTAMP_TIME_RULE, TO_BOOLEAN_RULE, + TO_DOUBLE_PAIR, TO_NUMBER_PAIR, TO_VARCHAR_PAIR, TO_VARIANT_RULE, VARIANT_CAST, + VARIANT_KIND_CAST, VARIANT_NAME, VARIANT_TEXT_FUNCTION, VARIANT_TO_VARCHAR, }; use sqlquack_policy::policy::constants::{ DATE_TEXT_SPELLING, SESSION_TIMEZONE_UTC, TIMESTAMP_RANGE, TIMESTAMP_TEXT_ROUNDING, @@ -294,6 +296,18 @@ fn cast_sql( let _rule = cast_rule(CAST_PARSED_TEXT, log); Ok(text_to_boolean(&operand.sql, text_failure(trying), log)) } + (SnowflakeType::Varchar, SnowflakeType::Binary, _) => { + let _rule = cast_rule(CAST_HEX_BINARY, log); + let bytes = hex_bytes(&operand.sql, trying, log); + match (target.max_length, trying) { + (None, _) => Ok(bytes), + (Some(max_length), false) => Ok(binary_length_check(&bytes, max_length, log)), + (Some(_), true) => Err( + "TRY_CAST to BINARY(n), whose result for longer bytes no recording shows" + .to_owned(), + ), + } + } (SnowflakeType::Varchar, SnowflakeType::Time { precision }, literal) => { let _rule = cast_rule(CAST_TEXT_TIME, log); time_submicro((operand.literal.as_ref(), precision), construct, context) @@ -457,12 +471,11 @@ fn nanosecond_range( } } -/// Text cut to a VARCHAR(n) target's length raises an error, as in Snowflake. +/// Text or bytes longer than a VARCHAR(n) or BINARY(n) target raise an error, as in Snowflake. fn bounded(sql: String, target: CastTarget, log: &ProvenanceLog) -> String { - match target.max_length { - Some(max_length) if target.data_type == SnowflakeType::Varchar => { - varchar_length_check(&sql, max_length, log) - } + match (target.max_length, target.data_type) { + (Some(max_length), SnowflakeType::Varchar) => varchar_length_check(&sql, max_length, log), + (Some(max_length), SnowflakeType::Binary) => binary_length_check(&sql, max_length, log), _ => sql, } } @@ -484,7 +497,7 @@ fn strict_cast( } _ if from == to => { let _rule = cast_rule(CAST_SAME_TYPE, log); - cast(&operand.sql, to, log) + Ok(bounded(cast(&operand.sql, to, log)?, target, log)) } (SnowflakeType::Number { .. }, SnowflakeType::Number { .. } | SnowflakeType::Float) | (SnowflakeType::Float, SnowflakeType::Number { scale: 0, .. }) => { @@ -791,6 +804,7 @@ pub(crate) fn data_type_name(data_type: &Value) -> Option { "text" => "VARCHAR".to_owned(), "char" => with_arguments("CHAR", &[number("length")]), "boolean" => "BOOLEAN".to_owned(), + "binary" | "var_binary" => with_arguments("BINARY", &[number("length")]), "date" => "DATE".to_owned(), "time" if !zoned => with_arguments("TIME", &[number("precision")]), "timestamp" if zoned => with_arguments("TIMESTAMP_TZ", &[number("precision")]), diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/mod.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/mod.rs index b12a65b7f..73245e0c8 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/mod.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod arrays; pub(crate) mod atoms; +pub(crate) mod bytes; pub(crate) mod calls; pub(crate) mod casts; pub(crate) mod conditionals; diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/strings.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/strings.rs index 2b88e4181..a30760417 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/strings.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/strings.rs @@ -30,6 +30,7 @@ use sqlquack_duckdb::text::main::hex_text::hex_text; use sqlquack_duckdb::text::main::title_case::title_case; use sqlquack_snowflake::typing::models::SnowflakeType; +use crate::conversion::_helpers::expressions::bytes::byte_concat; use crate::conversion::_helpers::expressions::scalar::convert_expression; use crate::conversion::_helpers::expressions::text_functions::integer_operand; use crate::conversion::_helpers::support::divergence::diverge; @@ -55,7 +56,15 @@ pub(crate) fn text_operand( construct: &Construct, site: Site<'_>, ) -> Result { - let emitted = convert_expression(node, site)?; + emitted_text_operand(convert_expression(node, site)?, construct, site) +} + +/// A converted VARCHAR (or NULL) operand's SQL. +pub(crate) fn emitted_text_operand( + emitted: Emitted, + construct: &Construct, + site: Site<'_>, +) -> Result { match emitted.data_type { SnowflakeType::Varchar | SnowflakeType::Null => Ok(emitted.sql), SnowflakeType::Date => Ok(format!("CAST({} AS VARCHAR)", emitted.sql)), @@ -255,18 +264,32 @@ pub(crate) fn concatenate_with_separator( construct: &Construct, site: Site<'_>, ) -> Result { - let _rule = site.context.log.rule(CONCAT_WS); let [separator, first, rest @ ..] = arguments else { return Err(outside_conditions( construct.clone(), "CONCAT_WS without a part to join".to_owned(), )); }; - let mut interleaved: Vec = vec![first.clone()]; + let separator = convert_expression(separator, site)?; + let mut interleaved: Vec = vec![convert_expression(first, site)?]; for part in rest { interleaved.push(separator.clone()); - interleaved.push(part.clone()); + interleaved.push(convert_expression(part, site)?); } + if separator.data_type == SnowflakeType::Binary + || interleaved + .iter() + .any(|part| part.data_type == SnowflakeType::Binary) + { + if rest.is_empty() { + return Err(outside_conditions( + construct.clone(), + "CONCAT_WS of a single BINARY part, which no recording shows".to_owned(), + )); + } + return byte_concat(&interleaved.iter().collect::>(), construct, site); + } + let _rule = site.context.log.rule(CONCAT_WS); if rest.is_empty() { interleaved.push(separator.clone()); let texts = operand_texts(&interleaved, construct, site)?; @@ -291,11 +314,17 @@ pub(crate) fn concatenate( construct: &Construct, site: Site<'_>, ) -> Result { - let _rule = site.context.log.rule(CONCAT); let emitted = parts .iter() .map(|part| convert_expression(part, site)) .collect::, Refusal>>()?; + if emitted + .iter() + .any(|part| part.data_type == SnowflakeType::Binary) + { + return byte_concat(&emitted.iter().collect::>(), construct, site); + } + let _rule = site.context.log.rule(CONCAT); let mut folded = String::new(); for part in &emitted { match &part.literal { @@ -328,17 +357,13 @@ fn text_constant(text: String, site: Site<'_>) -> Emitted { /// Each operand as text: VARCHAR as it is, others as Snowflake's `::VARCHAR` writes them. fn operand_texts( - parts: &[Value], + parts: &[Emitted], construct: &Construct, site: Site<'_>, ) -> Result, Refusal> { let mut texts: Vec = Vec::with_capacity(parts.len()); for part in parts { - texts.push(emitted_text( - &convert_expression(part, site)?, - construct, - site, - )?); + texts.push(emitted_text(part, construct, site)?); } Ok(texts) } diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs index cb25e49cd..2922f1e1c 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs @@ -62,12 +62,13 @@ use crate::conversion::constants::{ DATE_FORMAT_RULE, DATE_NAME, DATE_PART_RULE, DATE_PARTS, DAY_NAME_RULE, DECIMAL_EPOCH_RULE, FRACTIONAL_DATE_AMOUNT, FROM_PARTS, LAST_DAY_RULE, MAX_FRACTION_DIGITS_U8, MICROSECOND_DIGITS, MONTH_NAME_FIELDS, MONTH_NAME_FOLLOWERS, NULL_BRANCHES, NUMBER_TEXT_FORMAT, OFFSET_FREE_UNITS, - RECORDED_TIME_ZONES, SCALE_DIGITS, TEMPORAL_TEXT_FIELDS, TEMPORAL_TEXT_FORMAT, - TEMPORAL_TEXT_SEPARATORS, TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TIME_ARITHMETIC, - TIME_ZONE_CONVERSION, TIMESTAMP_FORMAT_RULE, TIMESTAMP_NTZ_NAME, TIMESTAMP_TIME_RULE, - TO_DATE_TEMPORAL, TO_TIMESTAMP_TEMPORAL, UTC_OFFSET_ARITHMETIC, UTC_ZONE, ZONE_COLUMNS, - ZONED_PARTS, + SCALE_DIGITS, TEMPORAL_TEXT_FIELDS, TEMPORAL_TEXT_FORMAT, TEMPORAL_TEXT_SEPARATORS, + TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TIME_ARITHMETIC, TIME_ZONE_CONVERSION, + TIMESTAMP_FORMAT_RULE, TIMESTAMP_NTZ_NAME, TIMESTAMP_TIME_RULE, TO_DATE_TEMPORAL, + TO_TIMESTAMP_TEMPORAL, UTC_OFFSET_ARITHMETIC, UTC_ZONE, ZONE_COLUMNS, ZONED_PARTS, }; +use crate::time_zones::constants::RECORDED_TIME_ZONES; +use crate::time_zones::main::recorded_zone::recorded_zone; use sqlquack_duckdb::raises::main::raised::raised; use sqlquack_duckdb::temporal::constants::{DUCKDB_CURRENT_DATE, DUCKDB_CURRENT_TIMESTAMP}; use sqlquack_policy::policy::constants::{ @@ -1110,7 +1111,7 @@ struct Zone { /// A literal zone name, or a VARCHAR expression naming one per row. fn zone_operand(node: &Value, construct: &Construct, site: Site<'_>) -> Result { if let Some(zone) = zone_literal(node, site) { - if !RECORDED_TIME_ZONES.contains(&zone.as_str()) { + if !recorded_zone(&zone) { return Err(outside_conditions( construct.clone(), format!( diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_functions.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_functions.rs index 9d86ed47d..ced962cc3 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_functions.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_functions.rs @@ -15,9 +15,12 @@ use sqlquack_duckdb::text::main::folded_text::folded_text; use sqlquack_duckdb::text::main::guarded_pattern::guarded_pattern; use sqlquack_snowflake::typing::models::SnowflakeType; +use crate::conversion::_helpers::expressions::bytes::{ + byte_left_right, byte_length, byte_pad, byte_substring, +}; use crate::conversion::_helpers::expressions::result_types::result_type; use crate::conversion::_helpers::expressions::scalar::{call_name, convert_expression}; -use crate::conversion::_helpers::expressions::strings::text_operand; +use crate::conversion::_helpers::expressions::strings::{emitted_text_operand, text_operand}; use crate::conversion::_helpers::support::divergence::diverge; use crate::conversion::_helpers::support::scope::{Emitted, Literal, resolve}; use crate::conversion::_helpers::support::site::Site; @@ -50,7 +53,6 @@ pub(crate) fn pad( construct: &Construct, site: Site<'_>, ) -> Result { - let _rule = site.context.log.rule(TEXT_PAD); let (text, length, filler) = match arguments { [text, length] => (text, length, None), [text, length, filler] => (text, length, Some(filler)), @@ -61,7 +63,16 @@ pub(crate) fn pad( )); } }; - let text = text_operand(text, construct, site)?; + let text = convert_expression(text, site)?; + if text.data_type == SnowflakeType::Binary { + let filler = match filler { + Some(filler) => Some(convert_expression(filler, site)?), + None => None, + }; + return byte_pad(name, (&text, length, filler.as_ref()), construct, site); + } + let _rule = site.context.log.rule(TEXT_PAD); + let text = emitted_text_operand(text, construct, site)?; let length = format!( "CAST({} AS INTEGER)", integer_operand(length, construct, site)? @@ -88,8 +99,12 @@ pub(crate) fn left_right( construct: &Construct, site: Site<'_>, ) -> Result { + let text = convert_expression(text, site)?; + if text.data_type == SnowflakeType::Binary { + return byte_left_right(name, (&text, length), construct, site); + } let _rule = site.context.log.rule(TEXT_POSITION); - let text = text_operand(text, construct, site)?; + let text = emitted_text_operand(text, construct, site)?; let length = integer_operand(length, construct, site)?; Ok(Emitted::new( format!( @@ -105,16 +120,22 @@ pub(crate) fn left_right( /// SUBSTR or SUBSTRING; start 0 counts as 1, and a negative length as 0. pub(crate) fn substring(payload: &Value, site: Site<'_>) -> Result { let construct = node_construct("substring"); - let _rule = site.context.log.rule(TEXT_POSITION); let payload = payload_object(payload, &construct)?; require_understood( payload, &["this", "start", "length", "from_for_syntax"], &construct, )?; - let text = text_operand(field(payload, "this", &construct)?, &construct, site)?; - let start = integer_operand(field(payload, "start", &construct)?, &construct, site)?; - let length = match payload.get("length").filter(|length| !length.is_null()) { + let text = convert_expression(field(payload, "this", &construct)?, site)?; + let start = field(payload, "start", &construct)?; + let length = payload.get("length").filter(|length| !length.is_null()); + if text.data_type == SnowflakeType::Binary { + return byte_substring((&text, start, length), &construct, site); + } + let _rule = site.context.log.rule(TEXT_POSITION); + let text = emitted_text_operand(text, &construct, site)?; + let start = integer_operand(start, &construct, site)?; + let length = match length { None => String::new(), Some(length) => { let emitted = convert_expression(length, site)?; @@ -147,10 +168,14 @@ pub(crate) fn substring(payload: &Value, site: Site<'_>) -> Result) -> Result { let construct = node_construct("length"); - let _rule = site.context.log.rule(TEXT_POSITION); let payload = payload_object(payload, &construct)?; require_understood(payload, &["this", "original_name"], &construct)?; - let text = text_operand(field(payload, "this", &construct)?, &construct, site)?; + let text = convert_expression(field(payload, "this", &construct)?, site)?; + if text.data_type == SnowflakeType::Binary { + return Ok(byte_length(&text, site)); + } + let _rule = site.context.log.rule(TEXT_POSITION); + let text = emitted_text_operand(text, &construct, site)?; Ok(Emitted::new( format!("CAST(length({text}) AS DECIMAL(18,0))"), LENGTH_TYPE, diff --git a/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_search.rs b/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_search.rs index 69d52a638..c900379a2 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_search.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/expressions/text_search.rs @@ -5,7 +5,9 @@ use sqlquack_core::classification::models::Construct; use sqlquack_core::refusals::main::outside_conditions::outside_conditions; use sqlquack_core::refusals::models::Refusal; -use crate::conversion::_helpers::expressions::strings::text_operand; +use crate::conversion::_helpers::expressions::bytes::byte_position; +use crate::conversion::_helpers::expressions::scalar::convert_expression; +use crate::conversion::_helpers::expressions::strings::emitted_text_operand; use crate::conversion::_helpers::support::scope::Emitted; use crate::conversion::_helpers::support::site::Site; use crate::conversion::_helpers::support::tree::{ @@ -14,6 +16,7 @@ use crate::conversion::_helpers::support::tree::{ use crate::conversion::constants::{ POSITION_PAIR, TEXT_SEARCH_POSITION, TEXT_SEARCH_POSITION_TYPE, }; +use sqlquack_snowflake::typing::models::SnowflakeType; /// The name POSITION is called by, which converts as its CHARINDEX spelling. const POSITION_NAME: &str = "POSITION"; @@ -65,9 +68,17 @@ fn position_of( construct: &Construct, site: Site<'_>, ) -> Result { + let needle = convert_expression(needle, site)?; + let haystack = convert_expression(haystack, site)?; + if [&needle, &haystack] + .iter() + .any(|operand| operand.data_type == SnowflakeType::Binary) + { + return byte_position((&needle, &haystack), construct, site); + } let _rule = site.context.log.rule(TEXT_SEARCH_POSITION); - let needle = text_operand(needle, construct, site)?; - let haystack = text_operand(haystack, construct, site)?; + let needle = emitted_text_operand(needle, construct, site)?; + let haystack = emitted_text_operand(haystack, construct, site)?; Ok(Emitted::new( format!("CAST(strpos({haystack}, {needle}) AS DECIMAL(9,0))"), TEXT_SEARCH_POSITION_TYPE, diff --git a/crates/sqlquack-rules/src/conversion/_helpers/support/shapes.rs b/crates/sqlquack-rules/src/conversion/_helpers/support/shapes.rs index 19a2c510a..7fbcef01a 100644 --- a/crates/sqlquack-rules/src/conversion/_helpers/support/shapes.rs +++ b/crates/sqlquack-rules/src/conversion/_helpers/support/shapes.rs @@ -41,6 +41,7 @@ pub(crate) fn type_family(data_type: SnowflakeType) -> &'static str { SnowflakeType::Number { .. } => "decimal", SnowflakeType::Float => "float", SnowflakeType::Varchar => "text", + SnowflakeType::Binary => "binary", SnowflakeType::Boolean => "boolean", SnowflakeType::Date => "date", SnowflakeType::Time { .. } => "time", diff --git a/crates/sqlquack-rules/src/conversion/constants.rs b/crates/sqlquack-rules/src/conversion/constants.rs index c0a81f28c..d062775e9 100644 --- a/crates/sqlquack-rules/src/conversion/constants.rs +++ b/crates/sqlquack-rules/src/conversion/constants.rs @@ -615,6 +615,22 @@ pub const CAST_TO_TEXT: &str = "rule.cast_to_text"; pub const CAST_BOOLEAN: &str = "rule.cast_boolean"; /// CAST and TRY_CAST of text to NUMBER, FLOAT or BOOLEAN. pub const CAST_PARSED_TEXT: &str = "rule.cast_parsed_text"; +/// LENGTH, LEN and OCTET_LENGTH of BINARY: its count of bytes. +pub const BYTE_LENGTH: &str = "rule.byte_length"; +/// LEFT, RIGHT, SUBSTR and SUBSTRING of BINARY, counted in bytes. +pub const BYTE_SLICE: &str = "rule.byte_slice"; +/// LPAD and RPAD of BINARY with BINARY padding. +pub const BYTE_PAD: &str = "rule.byte_pad"; +/// POSITION and CHARINDEX of BINARY in BINARY, at whole bytes. +pub const BYTE_SEARCH: &str = "rule.byte_search"; +/// CONCAT, `||` and CONCAT_WS of BINARY parts. +pub const BYTE_CONCAT: &str = "rule.byte_concat"; +/// OCTET_LENGTH of VARCHAR: its count of UTF-8 bytes. +pub const TEXT_OCTET_LENGTH: &str = "rule.text_octet_length"; +/// What a byte-wise rule raises for values no BINARY recording reaches. +pub const UNRECORDED_BYTES: &str = "BINARY values outside the recorded conditions"; +/// VARCHAR to BINARY under the pinned BINARY_INPUT_FORMAT HEX. +pub const CAST_HEX_BINARY: &str = "rule.cast_hex_binary"; /// Casts of string literals. pub const CAST_TEXT_LITERAL: &str = "rule.cast_text_literal"; /// Casts to the operand's own type, and of NULL. @@ -1369,607 +1385,6 @@ pub const FLATTEN_ORDERED_COLUMNS: &[&str] = &["PATH", "INDEX", "THIS"]; pub const USER_OVERRIDE_RULE: &str = "rule.user_override"; /// The prefix of the macro converted SQL calls a user override by. pub const OVERRIDE_MACRO_PREFIX: &str = "__sqlquack_override__"; -/// Time zone names recorded as accepted by Snowflake, which matches them exactly, case included. -pub(crate) const RECORDED_TIME_ZONES: &[&str] = &[ - "Africa/Abidjan", - "Africa/Accra", - "Africa/Addis_Ababa", - "Africa/Algiers", - "Africa/Asmara", - "Africa/Asmera", - "Africa/Bamako", - "Africa/Bangui", - "Africa/Banjul", - "Africa/Bissau", - "Africa/Blantyre", - "Africa/Brazzaville", - "Africa/Bujumbura", - "Africa/Cairo", - "Africa/Casablanca", - "Africa/Ceuta", - "Africa/Conakry", - "Africa/Dakar", - "Africa/Dar_es_Salaam", - "Africa/Djibouti", - "Africa/Douala", - "Africa/El_Aaiun", - "Africa/Freetown", - "Africa/Gaborone", - "Africa/Harare", - "Africa/Johannesburg", - "Africa/Juba", - "Africa/Kampala", - "Africa/Khartoum", - "Africa/Kigali", - "Africa/Kinshasa", - "Africa/Lagos", - "Africa/Libreville", - "Africa/Lome", - "Africa/Luanda", - "Africa/Lubumbashi", - "Africa/Lusaka", - "Africa/Malabo", - "Africa/Maputo", - "Africa/Maseru", - "Africa/Mbabane", - "Africa/Mogadishu", - "Africa/Monrovia", - "Africa/Nairobi", - "Africa/Ndjamena", - "Africa/Niamey", - "Africa/Nouakchott", - "Africa/Ouagadougou", - "Africa/Porto-Novo", - "Africa/Sao_Tome", - "Africa/Timbuktu", - "Africa/Tripoli", - "Africa/Tunis", - "Africa/Windhoek", - "America/Adak", - "America/Anchorage", - "America/Anguilla", - "America/Antigua", - "America/Araguaina", - "America/Argentina/Buenos_Aires", - "America/Argentina/Catamarca", - "America/Argentina/ComodRivadavia", - "America/Argentina/Cordoba", - "America/Argentina/Jujuy", - "America/Argentina/La_Rioja", - "America/Argentina/Mendoza", - "America/Argentina/Rio_Gallegos", - "America/Argentina/Salta", - "America/Argentina/San_Juan", - "America/Argentina/San_Luis", - "America/Argentina/Tucuman", - "America/Argentina/Ushuaia", - "America/Aruba", - "America/Asuncion", - "America/Atikokan", - "America/Atka", - "America/Bahia", - "America/Bahia_Banderas", - "America/Barbados", - "America/Belem", - "America/Belize", - "America/Blanc-Sablon", - "America/Boa_Vista", - "America/Bogota", - "America/Boise", - "America/Buenos_Aires", - "America/Cambridge_Bay", - "America/Campo_Grande", - "America/Cancun", - "America/Caracas", - "America/Catamarca", - "America/Cayenne", - "America/Cayman", - "America/Chicago", - "America/Chihuahua", - "America/Ciudad_Juarez", - "America/Coral_Harbour", - "America/Cordoba", - "America/Costa_Rica", - "America/Coyhaique", - "America/Creston", - "America/Cuiaba", - "America/Curacao", - "America/Danmarkshavn", - "America/Dawson", - "America/Dawson_Creek", - "America/Denver", - "America/Detroit", - "America/Dominica", - "America/Edmonton", - "America/Eirunepe", - "America/El_Salvador", - "America/Ensenada", - "America/Fort_Nelson", - "America/Fort_Wayne", - "America/Fortaleza", - "America/Glace_Bay", - "America/Godthab", - "America/Goose_Bay", - "America/Grand_Turk", - "America/Grenada", - "America/Guadeloupe", - "America/Guatemala", - "America/Guayaquil", - "America/Guyana", - "America/Halifax", - "America/Havana", - "America/Hermosillo", - "America/Indiana/Indianapolis", - "America/Indiana/Knox", - "America/Indiana/Marengo", - "America/Indiana/Petersburg", - "America/Indiana/Tell_City", - "America/Indiana/Vevay", - "America/Indiana/Vincennes", - "America/Indiana/Winamac", - "America/Indianapolis", - "America/Inuvik", - "America/Iqaluit", - "America/Jamaica", - "America/Jujuy", - "America/Juneau", - "America/Kentucky/Louisville", - "America/Kentucky/Monticello", - "America/Knox_IN", - "America/Kralendijk", - "America/La_Paz", - "America/Lima", - "America/Los_Angeles", - "America/Louisville", - "America/Lower_Princes", - "America/Maceio", - "America/Managua", - "America/Manaus", - "America/Marigot", - "America/Martinique", - "America/Matamoros", - "America/Mazatlan", - "America/Mendoza", - "America/Menominee", - "America/Merida", - "America/Metlakatla", - "America/Mexico_City", - "America/Miquelon", - "America/Moncton", - "America/Monterrey", - "America/Montevideo", - "America/Montreal", - "America/Montserrat", - "America/Nassau", - "America/New_York", - "America/Nipigon", - "America/Nome", - "America/Noronha", - "America/North_Dakota/Beulah", - "America/North_Dakota/Center", - "America/North_Dakota/New_Salem", - "America/Nuuk", - "America/Ojinaga", - "America/Panama", - "America/Pangnirtung", - "America/Paramaribo", - "America/Phoenix", - "America/Port-au-Prince", - "America/Port_of_Spain", - "America/Porto_Acre", - "America/Porto_Velho", - "America/Puerto_Rico", - "America/Punta_Arenas", - "America/Rainy_River", - "America/Rankin_Inlet", - "America/Recife", - "America/Regina", - "America/Resolute", - "America/Rio_Branco", - "America/Rosario", - "America/Santa_Isabel", - "America/Santarem", - "America/Santiago", - "America/Santo_Domingo", - "America/Sao_Paulo", - "America/Scoresbysund", - "America/Shiprock", - "America/Sitka", - "America/St_Barthelemy", - "America/St_Johns", - "America/St_Kitts", - "America/St_Lucia", - "America/St_Thomas", - "America/St_Vincent", - "America/Swift_Current", - "America/Tegucigalpa", - "America/Thule", - "America/Thunder_Bay", - "America/Tijuana", - "America/Toronto", - "America/Tortola", - "America/Vancouver", - "America/Virgin", - "America/Whitehorse", - "America/Winnipeg", - "America/Yakutat", - "America/Yellowknife", - "Antarctica/Casey", - "Antarctica/Davis", - "Antarctica/DumontDUrville", - "Antarctica/Macquarie", - "Antarctica/Mawson", - "Antarctica/McMurdo", - "Antarctica/Palmer", - "Antarctica/Rothera", - "Antarctica/South_Pole", - "Antarctica/Syowa", - "Antarctica/Troll", - "Antarctica/Vostok", - "Arctic/Longyearbyen", - "Asia/Aden", - "Asia/Almaty", - "Asia/Amman", - "Asia/Anadyr", - "Asia/Aqtau", - "Asia/Aqtobe", - "Asia/Ashgabat", - "Asia/Ashkhabad", - "Asia/Atyrau", - "Asia/Baghdad", - "Asia/Bahrain", - "Asia/Baku", - "Asia/Bangkok", - "Asia/Barnaul", - "Asia/Beirut", - "Asia/Bishkek", - "Asia/Brunei", - "Asia/Calcutta", - "Asia/Chita", - "Asia/Choibalsan", - "Asia/Chongqing", - "Asia/Chungking", - "Asia/Colombo", - "Asia/Dacca", - "Asia/Damascus", - "Asia/Dhaka", - "Asia/Dili", - "Asia/Dubai", - "Asia/Dushanbe", - "Asia/Famagusta", - "Asia/Gaza", - "Asia/Harbin", - "Asia/Hebron", - "Asia/Ho_Chi_Minh", - "Asia/Hong_Kong", - "Asia/Hovd", - "Asia/Irkutsk", - "Asia/Istanbul", - "Asia/Jakarta", - "Asia/Jayapura", - "Asia/Jerusalem", - "Asia/Kabul", - "Asia/Kamchatka", - "Asia/Karachi", - "Asia/Kashgar", - "Asia/Kathmandu", - "Asia/Katmandu", - "Asia/Khandyga", - "Asia/Kolkata", - "Asia/Krasnoyarsk", - "Asia/Kuala_Lumpur", - "Asia/Kuching", - "Asia/Kuwait", - "Asia/Macao", - "Asia/Macau", - "Asia/Magadan", - "Asia/Makassar", - "Asia/Manila", - "Asia/Muscat", - "Asia/Nicosia", - "Asia/Novokuznetsk", - "Asia/Novosibirsk", - "Asia/Omsk", - "Asia/Oral", - "Asia/Phnom_Penh", - "Asia/Pontianak", - "Asia/Pyongyang", - "Asia/Qatar", - "Asia/Qostanay", - "Asia/Qyzylorda", - "Asia/Rangoon", - "Asia/Riyadh", - "Asia/Saigon", - "Asia/Sakhalin", - "Asia/Samarkand", - "Asia/Seoul", - "Asia/Shanghai", - "Asia/Singapore", - "Asia/Srednekolymsk", - "Asia/Taipei", - "Asia/Tashkent", - "Asia/Tbilisi", - "Asia/Tehran", - "Asia/Tel_Aviv", - "Asia/Thimbu", - "Asia/Thimphu", - "Asia/Tokyo", - "Asia/Tomsk", - "Asia/Ujung_Pandang", - "Asia/Ulaanbaatar", - "Asia/Ulan_Bator", - "Asia/Urumqi", - "Asia/Ust-Nera", - "Asia/Vientiane", - "Asia/Vladivostok", - "Asia/Yakutsk", - "Asia/Yangon", - "Asia/Yekaterinburg", - "Asia/Yerevan", - "Atlantic/Azores", - "Atlantic/Bermuda", - "Atlantic/Canary", - "Atlantic/Cape_Verde", - "Atlantic/Faeroe", - "Atlantic/Faroe", - "Atlantic/Jan_Mayen", - "Atlantic/Madeira", - "Atlantic/Reykjavik", - "Atlantic/South_Georgia", - "Atlantic/St_Helena", - "Atlantic/Stanley", - "Australia/ACT", - "Australia/Adelaide", - "Australia/Brisbane", - "Australia/Broken_Hill", - "Australia/Canberra", - "Australia/Currie", - "Australia/Darwin", - "Australia/Eucla", - "Australia/Hobart", - "Australia/LHI", - "Australia/Lindeman", - "Australia/Lord_Howe", - "Australia/Melbourne", - "Australia/NSW", - "Australia/North", - "Australia/Perth", - "Australia/Queensland", - "Australia/South", - "Australia/Sydney", - "Australia/Tasmania", - "Australia/Victoria", - "Australia/West", - "Australia/Yancowinna", - "Brazil/Acre", - "Brazil/DeNoronha", - "Brazil/East", - "Brazil/West", - "CET", - "CST6CDT", - "Canada/Atlantic", - "Canada/Central", - "Canada/Eastern", - "Canada/Mountain", - "Canada/Newfoundland", - "Canada/Pacific", - "Canada/Saskatchewan", - "Canada/Yukon", - "Chile/Continental", - "Chile/EasterIsland", - "Cuba", - "EET", - "EST", - "EST5EDT", - "Egypt", - "Eire", - "Etc/GMT", - "Etc/GMT+0", - "Etc/GMT+1", - "Etc/GMT+10", - "Etc/GMT+11", - "Etc/GMT+12", - "Etc/GMT+2", - "Etc/GMT+3", - "Etc/GMT+4", - "Etc/GMT+5", - "Etc/GMT+6", - "Etc/GMT+7", - "Etc/GMT+8", - "Etc/GMT+9", - "Etc/GMT-0", - "Etc/GMT-1", - "Etc/GMT-10", - "Etc/GMT-11", - "Etc/GMT-12", - "Etc/GMT-13", - "Etc/GMT-14", - "Etc/GMT-2", - "Etc/GMT-3", - "Etc/GMT-4", - "Etc/GMT-5", - "Etc/GMT-6", - "Etc/GMT-7", - "Etc/GMT-8", - "Etc/GMT-9", - "Etc/GMT0", - "Etc/Greenwich", - "Etc/UCT", - "Etc/UTC", - "Etc/Universal", - "Etc/Zulu", - "Europe/Amsterdam", - "Europe/Andorra", - "Europe/Astrakhan", - "Europe/Athens", - "Europe/Belfast", - "Europe/Belgrade", - "Europe/Berlin", - "Europe/Bratislava", - "Europe/Brussels", - "Europe/Bucharest", - "Europe/Budapest", - "Europe/Busingen", - "Europe/Chisinau", - "Europe/Copenhagen", - "Europe/Dublin", - "Europe/Gibraltar", - "Europe/Guernsey", - "Europe/Helsinki", - "Europe/Isle_of_Man", - "Europe/Istanbul", - "Europe/Jersey", - "Europe/Kaliningrad", - "Europe/Kiev", - "Europe/Kirov", - "Europe/Kyiv", - "Europe/Lisbon", - "Europe/Ljubljana", - "Europe/London", - "Europe/Luxembourg", - "Europe/Madrid", - "Europe/Malta", - "Europe/Mariehamn", - "Europe/Minsk", - "Europe/Monaco", - "Europe/Moscow", - "Europe/Nicosia", - "Europe/Oslo", - "Europe/Paris", - "Europe/Podgorica", - "Europe/Prague", - "Europe/Riga", - "Europe/Rome", - "Europe/Samara", - "Europe/San_Marino", - "Europe/Sarajevo", - "Europe/Saratov", - "Europe/Simferopol", - "Europe/Skopje", - "Europe/Sofia", - "Europe/Stockholm", - "Europe/Tallinn", - "Europe/Tirane", - "Europe/Tiraspol", - "Europe/Ulyanovsk", - "Europe/Uzhgorod", - "Europe/Vaduz", - "Europe/Vatican", - "Europe/Vienna", - "Europe/Vilnius", - "Europe/Volgograd", - "Europe/Warsaw", - "Europe/Zagreb", - "Europe/Zaporozhye", - "Europe/Zurich", - "Factory", - "GB", - "GB-Eire", - "GMT", - "GMT+0", - "GMT-0", - "GMT0", - "Greenwich", - "HST", - "Hongkong", - "Iceland", - "Indian/Antananarivo", - "Indian/Chagos", - "Indian/Christmas", - "Indian/Cocos", - "Indian/Comoro", - "Indian/Kerguelen", - "Indian/Mahe", - "Indian/Maldives", - "Indian/Mauritius", - "Indian/Mayotte", - "Indian/Reunion", - "Iran", - "Israel", - "Jamaica", - "Japan", - "Kwajalein", - "Libya", - "MET", - "MST", - "MST7MDT", - "Mexico/BajaNorte", - "Mexico/BajaSur", - "Mexico/General", - "NZ", - "NZ-CHAT", - "Navajo", - "PRC", - "PST8PDT", - "Pacific/Apia", - "Pacific/Auckland", - "Pacific/Bougainville", - "Pacific/Chatham", - "Pacific/Chuuk", - "Pacific/Easter", - "Pacific/Efate", - "Pacific/Enderbury", - "Pacific/Fakaofo", - "Pacific/Fiji", - "Pacific/Funafuti", - "Pacific/Galapagos", - "Pacific/Gambier", - "Pacific/Guadalcanal", - "Pacific/Guam", - "Pacific/Honolulu", - "Pacific/Johnston", - "Pacific/Kanton", - "Pacific/Kiritimati", - "Pacific/Kosrae", - "Pacific/Kwajalein", - "Pacific/Majuro", - "Pacific/Marquesas", - "Pacific/Midway", - "Pacific/Nauru", - "Pacific/Niue", - "Pacific/Norfolk", - "Pacific/Noumea", - "Pacific/Pago_Pago", - "Pacific/Palau", - "Pacific/Pitcairn", - "Pacific/Pohnpei", - "Pacific/Ponape", - "Pacific/Port_Moresby", - "Pacific/Rarotonga", - "Pacific/Saipan", - "Pacific/Samoa", - "Pacific/Tahiti", - "Pacific/Tarawa", - "Pacific/Tongatapu", - "Pacific/Truk", - "Pacific/Wake", - "Pacific/Wallis", - "Pacific/Yap", - "Poland", - "Portugal", - "ROC", - "ROK", - "Singapore", - "Turkey", - "UCT", - "US/Alaska", - "US/Aleutian", - "US/Arizona", - "US/Central", - "US/East-Indiana", - "US/Eastern", - "US/Hawaii", - "US/Indiana-Starke", - "US/Michigan", - "US/Mountain", - "US/Pacific", - "US/Samoa", - "UTC", - "Universal", - "W-SU", - "WET", - "Zulu", -]; /// The keyword of an inner join, whose USING keys all take the earlier side's type, as recorded. pub(crate) const INNER_JOIN_KEYWORD: &str = "JOIN"; /// The sequence-number column of FLATTEN and SPLIT_TO_TABLE, which no rule converts. diff --git a/crates/sqlquack-rules/src/lib.rs b/crates/sqlquack-rules/src/lib.rs index 4c23bf847..d29c3e0e3 100644 --- a/crates/sqlquack-rules/src/lib.rs +++ b/crates/sqlquack-rules/src/lib.rs @@ -5,3 +5,4 @@ pub mod catalogue; pub mod conversion; pub mod session; +pub mod time_zones; diff --git a/crates/sqlquack-rules/src/time_zones/constants.rs b/crates/sqlquack-rules/src/time_zones/constants.rs new file mode 100644 index 000000000..4c7d503a2 --- /dev/null +++ b/crates/sqlquack-rules/src/time_zones/constants.rs @@ -0,0 +1,603 @@ +//! The time zone names Snowflake was recorded accepting. + +/// Time zone names recorded as accepted by Snowflake, which matches them exactly, case included. +pub const RECORDED_TIME_ZONES: &[&str] = &[ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Coyhaique", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu", +]; diff --git a/crates/sqlquack-rules/src/time_zones/main/mod.rs b/crates/sqlquack-rules/src/time_zones/main/mod.rs new file mode 100644 index 000000000..f1814c444 --- /dev/null +++ b/crates/sqlquack-rules/src/time_zones/main/mod.rs @@ -0,0 +1 @@ +pub mod recorded_zone; diff --git a/crates/sqlquack-rules/src/time_zones/main/recorded_zone.rs b/crates/sqlquack-rules/src/time_zones/main/recorded_zone.rs new file mode 100644 index 000000000..0bb1f3da6 --- /dev/null +++ b/crates/sqlquack-rules/src/time_zones/main/recorded_zone.rs @@ -0,0 +1,8 @@ +//! `recorded_zone`: whether Snowflake was recorded accepting a time zone name. + +use crate::time_zones::constants::RECORDED_TIME_ZONES; + +/// Whether Snowflake was recorded accepting `name`, which it matches exactly, case included. +pub fn recorded_zone(name: &str) -> bool { + RECORDED_TIME_ZONES.contains(&name) +} diff --git a/crates/sqlquack-rules/src/time_zones/mod.rs b/crates/sqlquack-rules/src/time_zones/mod.rs new file mode 100644 index 000000000..bf4bd7e3e --- /dev/null +++ b/crates/sqlquack-rules/src/time_zones/mod.rs @@ -0,0 +1,4 @@ +//! The time zone names Snowflake was recorded accepting. + +pub mod constants; +pub mod main; diff --git a/crates/sqlquack-semantics/src/lineage/_helpers/typing/snowflake_sets.rs b/crates/sqlquack-semantics/src/lineage/_helpers/typing/snowflake_sets.rs index bb2ad2590..c8296343d 100644 --- a/crates/sqlquack-semantics/src/lineage/_helpers/typing/snowflake_sets.rs +++ b/crates/sqlquack-semantics/src/lineage/_helpers/typing/snowflake_sets.rs @@ -97,7 +97,7 @@ fn set_type(spelling: &str) -> Option { (Kind::Variant, None, 0) } SnowflakeType::SemiStructured => return None, - SnowflakeType::Null => return None, + SnowflakeType::Binary | SnowflakeType::Null => return None, }; Some(SetType { kind, diff --git a/crates/sqlquack-snowflake/src/typing/_helpers/targets.rs b/crates/sqlquack-snowflake/src/typing/_helpers/targets.rs index 830c12a6a..0276c5909 100644 --- a/crates/sqlquack-snowflake/src/typing/_helpers/targets.rs +++ b/crates/sqlquack-snowflake/src/typing/_helpers/targets.rs @@ -1,7 +1,7 @@ //! Parsing the arguments of a Snowflake type name. use crate::typing::_helpers::numbers::number; -use crate::typing::constants::{DEFAULT_FRACTIONAL_PRECISION, MAX_PRECISION}; +use crate::typing::constants::{DEFAULT_FRACTIONAL_PRECISION, MAX_BINARY_LENGTH, MAX_PRECISION}; use crate::typing::models::{CastTarget, SnowflakeType}; pub(crate) fn parse_arguments(rest: &str, name: &str) -> Result, String> { @@ -50,6 +50,24 @@ pub(crate) fn text_target( }) } +/// BINARY or BINARY(n): bytes, at most `n` of them. +pub(crate) fn binary_target(arguments: &[u32], name: &str) -> Result { + match arguments { + [] => Ok(CastTarget { + data_type: SnowflakeType::Binary, + max_length: None, + }), + [length] if (1..=MAX_BINARY_LENGTH).contains(length) => Ok(CastTarget { + data_type: SnowflakeType::Binary, + max_length: Some(*length), + }), + [length] => Err(format!( + "`{name}` declares {length} bytes, outside 1 to {MAX_BINARY_LENGTH}" + )), + _ => Err(format!("too many arguments in `{name}`")), + } +} + pub(crate) fn fractional(arguments: &[u32], name: &str) -> Result { match arguments { [] => Ok(DEFAULT_FRACTIONAL_PRECISION), diff --git a/crates/sqlquack-snowflake/src/typing/constants.rs b/crates/sqlquack-snowflake/src/typing/constants.rs index 15edbcde8..6a3bcbcd3 100644 --- a/crates/sqlquack-snowflake/src/typing/constants.rs +++ b/crates/sqlquack-snowflake/src/typing/constants.rs @@ -18,6 +18,8 @@ pub const AVERAGE_EXTRA_DIGITS: u8 = 12; pub const COUNT_PRECISION: u8 = 18; /// Precision of timestamps and times without a declared precision. pub const DEFAULT_FRACTIONAL_PRECISION: u8 = 9; +/// The most bytes BINARY(n) declares, as recorded for BINARY results: BINARY(67108864). +pub const MAX_BINARY_LENGTH: u32 = 67_108_864; /// `NUMBER(38,0)`, which integer type names denote. pub const NUMBER_38_0: SnowflakeType = SnowflakeType::Number { diff --git a/crates/sqlquack-snowflake/src/typing/main/cast_target.rs b/crates/sqlquack-snowflake/src/typing/main/cast_target.rs index 7c747f8ca..4363351e4 100644 --- a/crates/sqlquack-snowflake/src/typing/main/cast_target.rs +++ b/crates/sqlquack-snowflake/src/typing/main/cast_target.rs @@ -2,7 +2,9 @@ use sqlquack_core::provenance::models::ProvenanceLog; -use crate::typing::_helpers::targets::{fractional, number_target, parse_arguments, text_target}; +use crate::typing::_helpers::targets::{ + binary_target, fractional, number_target, parse_arguments, text_target, +}; use crate::typing::constants::{ CAST_TARGET, NUMBER_38_0, STRUCTURED_TYPES, STRUCTURED_UNSUPPORTED, }; @@ -49,6 +51,7 @@ pub fn cast_target(name: &str, log: &ProvenanceLog) -> Result text_target(&arguments, Some(1), name), + "BINARY" | "VARBINARY" => binary_target(&arguments, name), "BOOLEAN" => plain(SnowflakeType::Boolean), "DATE" => plain(SnowflakeType::Date), "TIME" => fractional(&arguments, name).map(|precision| CastTarget { diff --git a/crates/sqlquack-snowflake/src/typing/models.rs b/crates/sqlquack-snowflake/src/typing/models.rs index f6b4ccfd8..9831903fc 100644 --- a/crates/sqlquack-snowflake/src/typing/models.rs +++ b/crates/sqlquack-snowflake/src/typing/models.rs @@ -42,6 +42,8 @@ pub enum SnowflakeType { /// Fractional-second digits. precision: u8, }, + /// `BINARY`; the declared length never changes a value it accepts. + Binary, /// `VARIANT`, `OBJECT` or `ARRAY`. SemiStructured, /// An untyped `NULL` literal, which takes the type of its context. @@ -70,6 +72,7 @@ impl Display for SnowflakeType { Self::TimestampNtz { precision } => write!(formatter, "TIMESTAMP_NTZ({precision})"), Self::TimestampTz { precision } => write!(formatter, "TIMESTAMP_TZ({precision})"), Self::TimestampLtz { precision } => write!(formatter, "TIMESTAMP_LTZ({precision})"), + Self::Binary => formatter.write_str("BINARY"), Self::SemiStructured => formatter.write_str("VARIANT"), Self::Null => formatter.write_str("NULL"), } @@ -81,6 +84,6 @@ impl Display for SnowflakeType { pub struct CastTarget { /// The target type. pub data_type: SnowflakeType, - /// For VARCHAR(n) and CHAR(n): `n`; longer values raise an error. + /// For VARCHAR(n) and CHAR(n) characters, for BINARY(n) bytes: `n`; longer values raise. pub max_length: Option, } diff --git a/crates/sqlquack/src/analysis/tests/test_analyze.rs b/crates/sqlquack/src/analysis/tests/test_analyze.rs index 84eccf154..7bd7fc2ac 100644 --- a/crates/sqlquack/src/analysis/tests/test_analyze.rs +++ b/crates/sqlquack/src/analysis/tests/test_analyze.rs @@ -36,6 +36,16 @@ fn given_sql_and_a_schema_when_analyzing_then_it_converts_or_refuses_with_a_cate sql: "SELECT l.amount FROM TABLE(large_orders(CAST(10 AS NUMBER(10,2)))) AS l", expected_summary: converted(), }, + AnalyzeTestCase { + description: "hex text cast to BINARY(n) converts, raising for longer bytes as recorded", + sql: "SELECT CAST('AABB' AS BINARY(2)) AS b", + expected_summary: converted(), + }, + AnalyzeTestCase { + description: "BINARY(0) is refused as a length Snowflake does not declare", + sql: "SELECT CAST('' AS BINARY(0)) AS b", + expected_summary: refused(RefusalCategory::TypeCondition), + }, AnalyzeTestCase { description: "an ORDER BY expression naming an alias that shadows a column uses the alias", sql: "SELECT amount * 2 AS amount FROM orders ORDER BY amount + 1", diff --git a/crates/sqlquack/src/analysis/tests/test_coverage_batch.rs b/crates/sqlquack/src/analysis/tests/test_coverage_batch.rs index 6e187592b..e4f47f3b3 100644 --- a/crates/sqlquack/src/analysis/tests/test_coverage_batch.rs +++ b/crates/sqlquack/src/analysis/tests/test_coverage_batch.rs @@ -77,6 +77,26 @@ fn given_computed_subscripts_when_converting_then_they_read_as_get() { sql: "SELECT PARSE_JSON('{\"1\":\"x\"}')[note]::VARCHAR AS v FROM customers", expected_outcome: Ok("x"), }, + FirstValueTestCase { + description: "BINARY cast to a BINARY(n) it fits keeps its bytes", + sql: "SELECT OCTET_LENGTH(CAST(CAST('AABB' AS BINARY) AS BINARY(2))) AS v", + expected_outcome: Ok("2"), + }, + FirstValueTestCase { + description: "BINARY cast to a shorter BINARY(n) raises 100145 rather than truncating", + sql: "SELECT OCTET_LENGTH(CAST(CAST('AABBCC' AS BINARY) AS BINARY(2))) AS v", + expected_outcome: Err("too long and would be truncated"), + }, + FirstValueTestCase { + description: "BINARY with :: to a shorter BINARY(n) raises too", + sql: "SELECT OCTET_LENGTH(CAST('AABBCC' AS BINARY)::BINARY(1)) AS v", + expected_outcome: Err("too long and would be truncated"), + }, + FirstValueTestCase { + description: "TRY_CAST of BINARY to BINARY(n) stays refused without a recording", + sql: "SELECT OCTET_LENGTH(TRY_CAST(CAST('AABBCC' AS BINARY) AS BINARY(2))) AS v", + expected_outcome: Err("has no proven conversion"), + }, FirstValueTestCase { description: "a negative constant index stays refused", sql: "SELECT PARSE_JSON('[1]')[-1] AS v", diff --git a/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.arrow b/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.arrow new file mode 100644 index 000000000..98db8daa4 Binary files /dev/null and b/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.arrow differ diff --git a/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.json b/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.json new file mode 100644 index 000000000..420ca82a0 --- /dev/null +++ b/evidence/results/097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "34c78da36d0738055bab36902880600800b6383f6a7734c5f72d33887271f5a9", + "case_hash": "097322dd0ccf7f6ec7763d314e173b4fcd7f67ce47905b68c3b204a697c7ee1d", + "columns": [ + { + "internal_size": 67108864, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "93cb3f6e3ac843b4b128f4c88564893a950569860963e6133718bb88162118f2", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 2, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/0c0d907c6f3f98326a3f1d4827f706ec1fdc1ed4adc3ad7e8540f1e5c1ae8349.json b/evidence/results/0c0d907c6f3f98326a3f1d4827f706ec1fdc1ed4adc3ad7e8540f1e5c1ae8349.json new file mode 100644 index 000000000..eec602559 --- /dev/null +++ b/evidence/results/0c0d907c6f3f98326a3f1d4827f706ec1fdc1ed4adc3ad7e8540f1e5c1ae8349.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "0c0d907c6f3f98326a3f1d4827f706ec1fdc1ed4adc3ad7e8540f1e5c1ae8349", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 100115, + "message": "The following string is not a legal hex-encoded value: '0G'", + "sqlstate": "22000" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "12ee83ad16ca9807ed2114d78c90a43d8e3e288d5839ae7b9303a1f268ca540b", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.arrow b/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.arrow new file mode 100644 index 000000000..15fcb998e Binary files /dev/null and b/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.arrow differ diff --git a/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.json b/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.json new file mode 100644 index 000000000..4c39e7454 --- /dev/null +++ b/evidence/results/0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7.json @@ -0,0 +1,70 @@ +{ + "arrow_sha256": "f0973b6991b5d2505425752e83b110fa3c535bb1172677afd1eb477cd52f000f", + "case_hash": "0cebc9733a2f1c105d336d26e8431e7437cb31f7fe5c26ac348b91292585b1d7", + "columns": [ + { + "internal_size": 2, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + }, + { + "internal_size": null, + "name": "L", + "nullable": true, + "precision": 18, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "770305dc8afe05385c83dc636dd2b43dfadb273d630c5ea0a23050f5349bcbc2", + "recorded_at": "2026-11-06T20:05:23.650305+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 2, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/14fad7b933237055472cc650853e09a251682ed52a4a0b8e4908f0968882437f.json b/evidence/results/14fad7b933237055472cc650853e09a251682ed52a4a0b8e4908f0968882437f.json new file mode 100644 index 000000000..c9d1437ea --- /dev/null +++ b/evidence/results/14fad7b933237055472cc650853e09a251682ed52a4a0b8e4908f0968882437f.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "14fad7b933237055472cc650853e09a251682ed52a4a0b8e4908f0968882437f", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 100145, + "message": "Binary value 'AABBCC' is too long and would be truncated", + "sqlstate": "22000" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "9fd392f850a14987982d6985734f262aea76525d2b6792fde553f96378b6f445", + "recorded_at": "2026-11-06T18:51:05.161808+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/1bc7db6a2375637aec95474a4683661ded5e33f4292d0ae9b8fdbb96639afb8c.json b/evidence/results/1bc7db6a2375637aec95474a4683661ded5e33f4292d0ae9b8fdbb96639afb8c.json new file mode 100644 index 000000000..3ff5c6c7f --- /dev/null +++ b/evidence/results/1bc7db6a2375637aec95474a4683661ded5e33f4292d0ae9b8fdbb96639afb8c.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "1bc7db6a2375637aec95474a4683661ded5e33f4292d0ae9b8fdbb96639afb8c", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 100145, + "message": "Binary value 'AABBCC' is too long and would be truncated", + "sqlstate": "22000" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "290fcb78207f7c40fe313b82e377dba1bad988bca7fe7bab7b6016425c199a3e", + "recorded_at": "2026-11-06T20:05:23.650305+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.arrow b/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.arrow new file mode 100644 index 000000000..15becf90a Binary files /dev/null and b/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.arrow differ diff --git a/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.json b/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.json new file mode 100644 index 000000000..720858856 --- /dev/null +++ b/evidence/results/43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095.json @@ -0,0 +1,70 @@ +{ + "arrow_sha256": "6453e533ef655c99b522b80f57d5b46ed0128c4763ed1dc1f03e30471e7741cd", + "case_hash": "43e2c93942ca2e0a63cc45bde8abafd44e021da137f59654133025490abd1095", + "columns": [ + { + "internal_size": 67108864, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + }, + { + "internal_size": null, + "name": "N", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 13, + "type_name": "BOOLEAN" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "2a1baaf206e36f5e48f0395b605c93355bf58b1494cc8ffc4c000dd52ab43274", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 2, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/6094d64591d9ae9e4dbaaa41d443fa572e7f0ff2e19669a610e9d3fde05ae600.json b/evidence/results/6094d64591d9ae9e4dbaaa41d443fa572e7f0ff2e19669a610e9d3fde05ae600.json new file mode 100644 index 000000000..c63b4bdfa --- /dev/null +++ b/evidence/results/6094d64591d9ae9e4dbaaa41d443fa572e7f0ff2e19669a610e9d3fde05ae600.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "6094d64591d9ae9e4dbaaa41d443fa572e7f0ff2e19669a610e9d3fde05ae600", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 100115, + "message": "The following string is not a legal hex-encoded value: 'ABC'", + "sqlstate": "22000" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "b791d27859ca9db7d5ea10df81f3c3f1a44f2e2e3dc7e1710fd3ef34ddfbd4d6", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.arrow b/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.arrow new file mode 100644 index 000000000..262f87ed3 Binary files /dev/null and b/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.arrow differ diff --git a/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.json b/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.json new file mode 100644 index 000000000..933265010 --- /dev/null +++ b/evidence/results/9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "1ab9c72a0dfd58e5ce2e09444abad1811e3e8a7d0f7721c74bc34b8ff8dc4308", + "case_hash": "9df2179d7dd3418acb40a44323fe1a12deb1982bafa76f75b3217a69a02fd21c", + "columns": [ + { + "internal_size": 67108864, + "name": "R", + "nullable": false, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "b947f4982e75723b7b0f4140666d51dbf461ec8c1b944d38ac2854f810366a6f", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 3, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.arrow b/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.arrow new file mode 100644 index 000000000..8117da539 Binary files /dev/null and b/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.arrow differ diff --git a/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.json b/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.json new file mode 100644 index 000000000..d3ed2c5f8 --- /dev/null +++ b/evidence/results/9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053.json @@ -0,0 +1,61 @@ +{ + "arrow_sha256": "494e9c6a6ba7d2bd86b06408a657f046c0c17bf35a1d1895c088b381fc2b1f86", + "case_hash": "9ed793b297cf289d5c2b5c800d78836e261af67d7603cbfb20990e514292f053", + "columns": [ + { + "internal_size": 67108864, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "9a97bfab54acab1044b5b4d32a3fee5c432bb308d63fab5d877a8ad24451144e", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 4, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.arrow b/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.arrow new file mode 100644 index 000000000..15fcb998e Binary files /dev/null and b/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.arrow differ diff --git a/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.json b/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.json new file mode 100644 index 000000000..c6e4f1cb8 --- /dev/null +++ b/evidence/results/f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132.json @@ -0,0 +1,70 @@ +{ + "arrow_sha256": "f0973b6991b5d2505425752e83b110fa3c535bb1172677afd1eb477cd52f000f", + "case_hash": "f235c5b2398f85bfff572de26c581d2eb7402b4645ddf70c7358b5f58446a132", + "columns": [ + { + "internal_size": 2, + "name": "R", + "nullable": true, + "precision": null, + "scale": null, + "type_code": 11, + "type_name": "BINARY" + }, + { + "internal_size": null, + "name": "L", + "nullable": true, + "precision": 18, + "scale": 0, + "type_code": 0, + "type_name": "FIXED" + } + ], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": null, + "format": 1, + "outcome": "rows", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "1bac77d37cebe6cc9dc8ef54277d906b1e0b215fce7ee305a2de2376ee53b45a", + "recorded_at": "2026-11-06T18:51:05.161808+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": 2, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +} diff --git a/evidence/results/fa9b733c08d8c9c1091747472a6882ef4e54cb9644673bb1d77abaac894a85da.json b/evidence/results/fa9b733c08d8c9c1091747472a6882ef4e54cb9644673bb1d77abaac894a85da.json new file mode 100644 index 000000000..1bd3fd444 --- /dev/null +++ b/evidence/results/fa9b733c08d8c9c1091747472a6882ef4e54cb9644673bb1d77abaac894a85da.json @@ -0,0 +1,55 @@ +{ + "arrow_sha256": null, + "case_hash": "fa9b733c08d8c9c1091747472a6882ef4e54cb9644673bb1d77abaac894a85da", + "columns": [], + "connector": "snowflake-connector-python 4.7.2", + "duckdb_version": "v2.0.0", + "effective_parameters": { + "BINARY_INPUT_FORMAT": "HEX", + "BINARY_OUTPUT_FORMAT": "HEX", + "DATE_INPUT_FORMAT": "AUTO", + "DATE_OUTPUT_FORMAT": "YYYY-MM-DD", + "DEFAULT_NULL_ORDERING": "LAST", + "GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON", + "GEOMETRY_OUTPUT_FORMAT": "GeoJSON", + "JSON_INDENT": "2", + "QUOTED_IDENTIFIERS_IGNORE_CASE": "false", + "STRICT_JSON_OUTPUT": "false", + "TIMESTAMP_DAY_IS_ALWAYS_24H": "false", + "TIMESTAMP_INPUT_FORMAT": "AUTO", + "TIMESTAMP_LTZ_OUTPUT_FORMAT": "", + "TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3", + "TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM", + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMESTAMP_TZ_OUTPUT_FORMAT": "", + "TIMEZONE": "UTC", + "TIME_INPUT_FORMAT": "AUTO", + "TIME_OUTPUT_FORMAT": "HH24:MI:SS", + "TWO_DIGIT_CENTURY_START": "1970", + "USE_CACHED_RESULT": "false", + "WEEK_OF_YEAR_POLICY": "0", + "WEEK_START": "0" + }, + "error": { + "errno": 100145, + "message": "Binary value 'ABCDEF' is too long and would be truncated", + "sqlstate": "22000" + }, + "format": 1, + "outcome": "error", + "parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61", + "query_id_sha256": "345bcf5c2d1bf1065e4b8a6be29c5a19298aa1dbcc0cfd422b6560291f463737", + "recorded_at": "2026-11-06T16:33:28.866701+00:00", + "recorder": "sqlquack-record 0.1.0", + "row_count": null, + "session_parameters": { + "DEFAULT_NULL_ORDERING": "LAST", + "QUOTED_IDENTIFIERS_IGNORE_CASE": false, + "TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ", + "TIMEZONE": "UTC", + "USE_CACHED_RESULT": false, + "WEEK_OF_YEAR_POLICY": 0, + "WEEK_START": 0 + }, + "warehouse_version": "10.40.102" +}