Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cases/conditional/text_literal_timestamps.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# A string literal beside a TIMESTAMP_NTZ(9) branch, as an incremental model's high-water mark
# writes it: the literal is read as the timestamp it spells.
category = "conditional"
construct = "fn:COALESCE"
session_profile = "utc-v1"

[[case]]
id = "high_water_mark_defaults"
description = "COALESCE, IFNULL and NVL of MAX of a TIMESTAMP_NTZ(9) and a date or timestamp literal, over rows and over none."
columns = [{ name = "g", type = "NUMBER(38,0)" }, { name = "t", type = "TIMESTAMP_NTZ(9)" }]
rows = [
["1", "'2026-01-10 08:30:00.123456789'"],
["1", "'2026-01-02 00:00:00'"],
["2", "NULL"],
["3", "NULL"],
["NULL", "NULL"],
]
query = "SELECT g, COALESCE(MAX(t), '1900-01-01') AS c, IFNULL(MAX(t), '1900-01-01 00:00:00') AS i, NVL(MAX(t), '1970-01-01T12:00:00.5') AS n FROM input GROUP BY g ORDER BY g"
order = "ordered"
tags = ["null", "all-null", "mixed-types"]

[[case]]
id = "literal_branches_beside_timestamps"
description = "A timestamp literal first, beside a column, and as an IFF or CASE branch beside a TIMESTAMP_NTZ(9)."
columns = [{ name = "t", type = "TIMESTAMP_NTZ(9)" }]
rows = [
["'2026-01-10 08:30:00.123456789'"],
["NULL"],
]
query = "SELECT COALESCE('2000-02-29 23:59:59', t) AS first, COALESCE(t, '2262-04-10') AS last_day, IFF(t IS NULL, '1999-12-31', t) AS i, CASE WHEN t IS NULL THEN t ELSE '2001-01-01 01:01:01.000000001' END AS c FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null", "mixed-types", "boundary"]
1 change: 0 additions & 1 deletion cases/conversion/cast_batch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,3 @@ rows = [
]
expression = "n::VARCHAR"
tags = ["null", "boundary", "invalid"]
expected_divergence = "timestamp_text_rounding"
2 changes: 0 additions & 2 deletions cases/generated/hazards/cast_to_text__conversion.toml
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ rows = [
expression = "CAST(x0 AS VARCHAR)"
tags = ["null", "invalid", "boundary"]
origin = "generated: hazards@1"
expected_divergence = "timestamp_text_rounding"

[[case]]
id = "node_cast_5_0_timestamp_ntz_extreme_years"
Expand Down Expand Up @@ -676,7 +675,6 @@ rows = [
expression = "TO_VARCHAR(x0)"
tags = ["null", "invalid", "boundary"]
origin = "generated: hazards@1"
expected_divergence = "timestamp_text_rounding"

[[case]]
id = "fn_to_varchar_6_0_timestamp_ntz_extreme_years"
Expand Down
1 change: 1 addition & 0 deletions cases/generated/zoned_texts/column_comparisons.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ query = "SELECT z = '2024-01-01 00:00:00'::TIMESTAMP_NTZ AS r FROM input ORDER B
order = "ordered"
tags = ["null", "boundary"]
origin = "generated: zoned_texts@1"
expected_divergence = "timestamp_tz_submicro"

[[case]]
id = "timestamp_ltz_column_gte_date"
Expand Down
46 changes: 46 additions & 0 deletions cases/temporal/naive_beside_local.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Staging-model lookbacks: DATEADD and DATEDIFF of a TIMESTAMP_LTZ, and a TIMESTAMP_NTZ compared with a
# DATE or a TIMESTAMP_LTZ, in the UTC session.
category = "temporal"
construct = "fn:DATEADD"
session_profile = "utc-v1"

[[case]]
id = "dateadd_of_local"
description = "DATEADD of a TIMESTAMP_LTZ by hours, days and months, from values written at other offsets, across a day and a month end."
columns = [{ name = "s", type = "TIMESTAMP_LTZ(9)" }]
rows = [
["'2026-01-31 02:00:00 +05:00'"],
["'2024-02-29 23:30:00.123456 -05:00'"],
["'1970-01-01 00:00:00 +00:00'"],
["NULL"],
]
query = "SELECT DATEADD(HOUR, 5, s) AS h, DATEADD(DAY, -3, s) AS d, DATEADD(MONTH, 1, s) AS m FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null", "boundary", "timezone"]

[[case]]
id = "datediff_with_local"
description = "DATEDIFF between a TIMESTAMP_NTZ or a DATE and a TIMESTAMP_LTZ, in days and hours, both ways round."
columns = [{ name = "t", type = "TIMESTAMP_NTZ(9)" }, { name = "d", type = "DATE" }, { name = "s", type = "TIMESTAMP_LTZ(9)" }]
rows = [
["'2026-01-30 23:59:59.999999999'", "'2026-01-30'", "'2026-01-31 02:00:00 +05:00'"],
["'2026-01-01 00:00:00'", "'2025-12-31'", "'2026-01-01 00:30:00 +01:00'"],
["NULL", "'2026-01-01'", "'2026-01-01 00:00:00 +00:00'"],
]
query = "SELECT DATEDIFF(DAY, t, s) AS td, DATEDIFF(HOUR, s, t) AS st, DATEDIFF(DAY, d, s) AS dd, DATEDIFF('day', s, d) AS sd FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null", "boundary", "timezone"]

[[case]]
id = "naive_compared_with_date_and_local"
description = "A TIMESTAMP_NTZ compared with a DATE as its midnight, and with a TIMESTAMP_LTZ by its UTC wall time, down to a nanosecond."
columns = [{ name = "t", type = "TIMESTAMP_NTZ(9)" }, { name = "d", type = "DATE" }, { name = "s", type = "TIMESTAMP_LTZ(9)" }]
rows = [
["'2026-01-30 00:00:00'", "'2026-01-30'", "'2026-01-30 05:00:00 +05:00'"],
["'2026-01-30 00:00:00.000000001'", "'2026-01-30'", "'2026-01-30 00:00:00 +00:00'"],
["'2026-01-29 23:59:59.999999999'", "'2026-01-30'", "'2026-01-29 19:00:00 -05:00'"],
["NULL", "'2026-01-30'", "NULL"],
]
query = "SELECT t >= d AS on_or_after, t < d AS before_, t = s AS same, t > s AS after_, s <= t AS local_first, t BETWEEN d AND DATEADD(DAY, 1, d) AS in_day FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null", "boundary", "timezone"]
34 changes: 34 additions & 0 deletions cases/temporal/timestamp_text_digits.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
category = "temporal"
construct = "node:cast"
session_profile = "utc-v1"

[[case]]
id = "ntz9_to_varchar_dropped_digits"
description = "TO_VARCHAR of a nanosecond naive timestamp keeps three fractional digits. Catches: rounding where Snowflake truncates or the reverse, at .1235 against .1234999, and a carry into the next second, day and year."
columns = [{ name = "t", type = "TIMESTAMP_NTZ(9)" }]
rows = [
["'2024-02-29 13:45:01.123499999'"],
["'2024-02-29 13:45:01.123500000'"],
["'2024-02-29 13:45:01.123999999'"],
["'2024-02-29 13:45:01.000500000'"],
["'2024-02-29 13:45:01.999500000'"],
["'2024-12-31 23:59:59.999999999'"],
["'1969-12-31 23:59:59.999500000'"],
["NULL"],
]
expression = "TO_VARCHAR(t)"
tags = ["null", "boundary"]

[[case]]
id = "ntz9_cast_to_varchar_dropped_digits"
description = "A nanosecond naive timestamp cast to VARCHAR, the same digits as TO_VARCHAR. Catches: the cast and TO_VARCHAR treating the dropped digits differently."
columns = [{ name = "t", type = "TIMESTAMP_NTZ(9)" }]
rows = [
["'2024-02-29 13:45:01.123499999'"],
["'2024-02-29 13:45:01.123500000'"],
["'2024-02-29 13:45:01.999500000'"],
["'2024-12-31 23:59:59.999999999'"],
["NULL"],
]
expression = "t::VARCHAR"
tags = ["null", "boundary"]
6 changes: 3 additions & 3 deletions coverage/domains/casts.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ arguments = [
facets = { target = ["text"] }
template = "CAST({0} AS VARCHAR)"
category = "conversion"
divergences = { "timestamp_ntz.half_millisecond" = "timestamp_text_rounding" }
# Half-millisecond fractions keep their own case, recorded while the dropped digits were unknown.
alone = ["0.timestamp_ntz.half_millisecond"]

[[rule.form]]
construct = "fn:TO_VARCHAR"
Expand All @@ -138,9 +139,8 @@ arguments = [
template = "TO_VARCHAR({0})"
category = "conversion"
variants = ["TO_CHAR({0})"]
divergences = { "timestamp_ntz.half_millisecond" = "timestamp_text_rounding" }
# Subnormal FLOATs are written with ten significant digits, and negative zero as -0, as recorded.
alone = ["0.float.subnormal", "0.float.negative_zero"]
alone = ["0.float.subnormal", "0.float.negative_zero", "0.timestamp_ntz.half_millisecond"]

[[rule]]
id = "rule.cast_boolean"
Expand Down
19 changes: 13 additions & 6 deletions coverage/translation/results-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@
"conditional/nvl.toml#ifnull_text": "matching",
"conditional/nvl.toml#nvl_mixed_scales": "matching",
"conditional/nvl.toml#nvl_text_and_number": "matching_error",
"conditional/text_literal_timestamps.toml#high_water_mark_defaults": "matching",
"conditional/text_literal_timestamps.toml#literal_branches_beside_timestamps": "matching",
"conditional/text_unification.toml#case_without_else_readable_text": "matching",
"conditional/text_unification.toml#case_without_else_unreadable_text": "matching",
"conditional/text_unification.toml#coalesce_text_with_float_and_timestamp": "refused",
Expand Down Expand Up @@ -238,7 +240,7 @@
"conversion/cast_batch.toml#float_varchar_special": "matching",
"conversion/cast_batch.toml#ltz_to_ntz": "matching",
"conversion/cast_batch.toml#ntz_to_varchar": "declared_divergence",
"conversion/cast_batch.toml#ntz_to_varchar_rounding": "declared_divergence",
"conversion/cast_batch.toml#ntz_to_varchar_rounding": "matching",
"conversion/cast_batch.toml#tz_coalesce_ntz": "refused",
"conversion/cast_batch.toml#tz_to_date_direct": "refused",
"conversion/cast_batch.toml#tz_to_ntz_cast_syntax": "refused",
Expand Down Expand Up @@ -1180,7 +1182,7 @@
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_integer_digits_38": "matching",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_integer_hazards": "matching",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_timestamp_ntz_extreme_years": "declared_divergence",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_timestamp_ntz_half_millisecond": "declared_divergence",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_timestamp_ntz_half_millisecond": "matching",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_0_timestamp_ntz_hazards": "matching",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_combination_0_boolean": "matching",
"generated/hazards/cast_to_text__conversion.toml#fn_to_varchar_6_combination_0_date": "matching",
Expand Down Expand Up @@ -1213,7 +1215,7 @@
"generated/hazards/cast_to_text__conversion.toml#node_cast_4_combination_0_float": "matching",
"generated/hazards/cast_to_text__conversion.toml#node_cast_4_extra_rows": "matching",
"generated/hazards/cast_to_text__conversion.toml#node_cast_5_0_timestamp_ntz_extreme_years": "declared_divergence",
"generated/hazards/cast_to_text__conversion.toml#node_cast_5_0_timestamp_ntz_half_millisecond": "declared_divergence",
"generated/hazards/cast_to_text__conversion.toml#node_cast_5_0_timestamp_ntz_half_millisecond": "matching",
"generated/hazards/cast_to_text__conversion.toml#node_cast_5_0_timestamp_ntz_hazards": "matching",
"generated/hazards/cast_to_text__conversion.toml#node_cast_5_combination_0_timestamp_ntz": "matching",
"generated/hazards/cast_to_text__conversion.toml#node_cast_combination_0_integer": "matching",
Expand Down Expand Up @@ -6277,7 +6279,7 @@
"generated/zoned_texts/cast_tz.toml#cast_tz_wall": "declared_divergence",
"generated/zoned_texts/cast_tz.toml#cast_tz_year": "declared_divergence",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_between_texts": "declared_divergence",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_eq_ntz": "refused",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_eq_ntz": "declared_divergence",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_eq_offset_text": "declared_divergence",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_eq_t_text": "refused",
"generated/zoned_texts/column_comparisons.toml#timestamp_ltz_column_gte_date": "refused",
Expand Down Expand Up @@ -6582,7 +6584,7 @@
"relational/alias_resolution.toml#order_by_function_alias_shadows_column": "matching",
"relational/alias_resolution.toml#qualify_alias_shadows_column": "matching",
"relational/alias_resolution.toml#qualify_filter_alias_shadows_column": "matching",
"relational/asof_dates.toml#asof_join_date_against_timestamp": "refused",
"relational/asof_dates.toml#asof_join_date_against_timestamp": "matching",
"relational/asof_dates.toml#asof_join_dates_duplicate_left_rows": "matching",
"relational/asof_dates.toml#asof_join_dates_empty_right": "matching",
"relational/asof_dates.toml#asof_join_dates_greater_equal": "matching",
Expand Down Expand Up @@ -7410,7 +7412,7 @@
"temporal/current_time_batch2.toml#current_8": "matching",
"temporal/current_time_batch2.toml#current_9": "matching",
"temporal/current_time_batch2.toml#current_date_shapes": "matching",
"temporal/current_time_batch2.toml#current_timestamp_shape": "refused",
"temporal/current_time_batch2.toml#current_timestamp_shape": "matching",
"temporal/date_part.toml#date_part_day": "matching",
"temporal/date_part.toml#date_part_dayofweek": "matching",
"temporal/date_part.toml#date_part_dayofweekiso": "matching",
Expand Down Expand Up @@ -7472,6 +7474,9 @@
"temporal/last_day.toml#last_day_month_unit": "refused",
"temporal/last_day.toml#last_day_of_dates": "matching",
"temporal/last_day.toml#last_day_of_timestamps": "matching",
"temporal/naive_beside_local.toml#dateadd_of_local": "matching",
"temporal/naive_beside_local.toml#datediff_with_local": "matching",
"temporal/naive_beside_local.toml#naive_compared_with_date_and_local": "matching",
"temporal/parse_followups.toml#dateadd_amount_above_int32": "matching",
"temporal/parse_followups.toml#dateadd_amount_below_int32": "matching",
"temporal/parse_followups.toml#dateadd_amount_int32_bounds": "declared_divergence",
Expand Down Expand Up @@ -7571,6 +7576,8 @@
"temporal/timestamp_precision.toml#ntz_to_precision_6": "matching",
"temporal/timestamp_precision.toml#tz9_min_max": "declared_divergence",
"temporal/timestamp_precision.toml#tz9_values": "declared_divergence",
"temporal/timestamp_text_digits.toml#ntz9_cast_to_varchar_dropped_digits": "matching",
"temporal/timestamp_text_digits.toml#ntz9_to_varchar_dropped_digits": "matching",
"temporal/to_timestamp.toml#cast_text_beyond_nanosecond_range": "declared_divergence",
"temporal/to_timestamp.toml#to_date_text": "matching",
"temporal/to_timestamp.toml#to_timestamp_format": "matching",
Expand Down
2 changes: 0 additions & 2 deletions crates/sqlquack-policy/src/policy/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ pub const PERCENTILE_INTERPOLATION: &str = "percentile_interpolation";
pub const DATE_TEXT_SPELLING: &str = "date_text_spelling";
/// The divergence of the current date and time, which differ between engines and runs.
pub const CURRENT_TIME: &str = "current_time";
/// The divergence of a timestamp's text where truncating and rounding to milliseconds differ.
pub const TIMESTAMP_TEXT_ROUNDING: &str = "timestamp_text_rounding";
/// The divergence of ANY_VALUE, whose choice among a group's values differs between engines.
pub const ANY_VALUE_CHOICE: &str = "any_value_choice";
/// The divergence of MODE, whose choice among values tied for most frequent differs between engines.
Expand Down
8 changes: 4 additions & 4 deletions crates/sqlquack-rules/src/catalogue/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub const RULES: &[RuleSpec] = &[
constructs: &[
"node:eq", "node:neq", "node:lt", "node:lte", "node:gt", "node:gte",
],
conditions: "Both NUMBER or FLOAT (compared as doubles when one is FLOAT), both VARCHAR (bytewise), both BOOLEAN, both DATE, both TIMESTAMP_NTZ (any precisions) or both zoned (TIMESTAMP_TZ or TIMESTAMP_LTZ, compared as instants). NUMBERs whose common type needs more than 38 digits rely on numeric_range; Snowflake also raises 100046 for values beyond it, as recorded.",
conditions: "Both NUMBER or FLOAT (compared as doubles when one is FLOAT), both VARCHAR (bytewise), both BOOLEAN, both DATE, both TIMESTAMP_NTZ (any precisions) or both zoned (TIMESTAMP_TZ or TIMESTAMP_LTZ, compared as instants). A TIMESTAMP_NTZ beside a DATE reads the DATE as its midnight, and beside a TIMESTAMP_LTZ reads the LTZ as its UTC wall time, relying on session_timezone_utc; both rely on timestamp_range at nanosecond precision. NUMBERs whose common type needs more than 38 digits rely on numeric_range; Snowflake also raises 100046 for values beyond it, as recorded.",
failures: &[FailureMode {
kind: ErrorKind::NumericOverflow,
snowflake: &[NUMBER_OUT_OF_RANGE],
Expand Down Expand Up @@ -367,7 +367,7 @@ pub const RULES: &[RuleSpec] = &[
"node:intersect",
"fn:DECODE",
],
conditions: "Non-literal text branches beside NUMBER branches are read as NUMBER(18,5), beside a FLOAT as FLOAT and beside a TIMESTAMP_NTZ(9) as one, then widened to the unified type, as recorded; a unified NUMBER of more than 5 fraction digits refuses. COALESCE and NVL read every text branch for every row, whichever branch is chosen, so unreadable text raises as Snowflake raises (error 100038). IFF, CASE and DECODE read only the branch chosen, as recorded: an unreadable text branch not chosen raises nothing. A set operation reads a text column beside a NUMBER or FLOAT column the same way, for every row of that branch. NULLIF, LEAST and GREATEST, which no recording shows reading text, refuse text beside other types.",
conditions: "Non-literal text branches beside NUMBER branches are read as NUMBER(18,5), beside a FLOAT as FLOAT and beside a TIMESTAMP_NTZ(9) as one, then widened to the unified type, as recorded; a unified NUMBER of more than 5 fraction digits refuses. A string literal beside a TIMESTAMP_NTZ(9) branch is read as the timestamp it spells, YYYY-MM-DD[ HH:MI:SS[.f]] with a space or T, and a literal outside DuckDB's nanosecond range refuses. COALESCE and NVL read every text branch for every row, whichever branch is chosen, so unreadable text raises as Snowflake raises (error 100038). IFF, CASE and DECODE read only the branch chosen, as recorded: an unreadable text branch not chosen raises nothing. A set operation reads a text column beside a NUMBER or FLOAT column the same way, for every row of that branch. NULLIF, LEAST and GREATEST, which no recording shows reading text, refuse text beside other types.",
failures: &[],
},
RuleSpec {
Expand Down Expand Up @@ -417,7 +417,7 @@ pub const RULES: &[RuleSpec] = &[
RuleSpec {
id: CAST_TO_TEXT,
constructs: &["node:cast"],
conditions: "From NUMBER, BOOLEAN, DATE or VARCHAR; from FLOAT as recorded: max(10, min(15, whole digits + 9)) significant digits as %.{n}g writes them, so ten below 10, one more per whole digit, fifteen from 1e5, plain from 1e-4 to below 1e15 and an exponent outside, NaN, inf, -inf, 0 and -0; from TIMESTAMP_NTZ(9) in the output format YYYY-MM-DD HH24:MI:SS.FF3 where the dropped digits are below half a millisecond, so truncating and rounding agree, raising timestamp_text_rounding otherwise; a length is enforced by raising an error; not TRY_CAST unless from VARCHAR.",
conditions: "From NUMBER, BOOLEAN, DATE or VARCHAR; from FLOAT as recorded: max(10, min(15, whole digits + 9)) significant digits as %.{n}g writes them, so ten below 10, one more per whole digit, fifteen from 1e5, plain from 1e-4 to below 1e15 and an exponent outside, NaN, inf, -inf, 0 and -0; from TIMESTAMP_NTZ(9) in the output format YYYY-MM-DD HH24:MI:SS.FF3, the digits past the millisecond truncated, never rounded or carried, as recorded; a length is enforced by raising an error; not TRY_CAST unless from VARCHAR.",
failures: &[FailureMode {
kind: ErrorKind::StringTooLong,
snowflake: &[STRING_TOO_LONG],
Expand Down Expand Up @@ -1564,7 +1564,7 @@ pub const RULES: &[RuleSpec] = &[
"fn:TIMEDIFF",
"fn:TIMESTAMPDIFF",
],
conditions: "DATE_TRUNC and DATEADD of a TIMESTAMP_TZ known to hold the UTC offset, +00:00 (from CONVERT_TIMEZONE('UTC', x), a cast of a DATE or naive timestamp in the UTC session, or values chosen from such), computed on its UTC wall time and read back at +00:00, keeping its type; DATEDIFF of two such values from their UTC wall times. A UTC-offset value against a naive one, and zoned values of unknown offset, refuse.",
conditions: "DATEADD of a TIMESTAMP_LTZ, and DATEDIFF between one and a DATE, naive or LTZ value, computed on its UTC wall time and relying on session_timezone_utc, DATEADD read back as TIMESTAMP_LTZ. DATE_TRUNC and DATEADD of a TIMESTAMP_TZ known to hold the UTC offset, +00:00 (from CONVERT_TIMEZONE('UTC', x), a cast of a DATE or naive timestamp in the UTC session, or values chosen from such), computed on its UTC wall time and read back at +00:00, keeping its type; DATEDIFF of two such values from their UTC wall times. A UTC-offset value against a naive one, and zoned values of unknown offset, refuse.",
failures: &[],
},
RuleSpec {
Expand Down
Loading
Loading