Skip to content

Commit cf30fe2

Browse files
committed
Merge wip/to-timestamp-ntz-utc-2: convert TO_TIMESTAMP_NTZ of a TIMESTAMP_TZ known to hold the UTC offset
2 parents 54913be + 041825b commit cf30fe2

12 files changed

Lines changed: 250 additions & 7 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
category = "temporal"
2+
construct = "fn:TO_TIMESTAMP_NTZ"
3+
session_profile = "utc-v1"
4+
5+
[[case]]
6+
id = "to_timestamp_ntz_of_utc_tz"
7+
description = "TO_TIMESTAMP_NTZ of CONVERT_TIMEZONE('UTC', tz) over TIMESTAMP_TZ values at several offsets: the UTC wall-clock time, typed as Snowflake records it."
8+
columns = [{ name = "tz", type = "TIMESTAMP_TZ(9)" }]
9+
rows = [
10+
["'2024-02-29 13:45:01.123456789 +02:00'"],
11+
["'2024-02-29 23:30:00 -05:00'"],
12+
["'2024-03-31 00:30:00 +05:30'"],
13+
["'1969-12-31 23:59:59.999999 +00:00'"],
14+
["NULL"],
15+
]
16+
expression = "TO_TIMESTAMP_NTZ(CONVERT_TIMEZONE('UTC', tz))"
17+
tags = ["null", "boundary"]
18+
expected_divergence = "timestamp_tz_submicro"
19+
20+
[[case]]
21+
id = "to_timestamp_ntz_of_utc_ltz"
22+
description = "TO_TIMESTAMP_NTZ of CONVERT_TIMEZONE('UTC', ltz) over TIMESTAMP_LTZ values under a UTC session."
23+
columns = [{ name = "ltz", type = "TIMESTAMP_LTZ(9)" }]
24+
rows = [
25+
["'2024-02-29 13:45:01.123456789'"],
26+
["'1969-12-31 23:59:59'"],
27+
["NULL"],
28+
]
29+
expression = "TO_TIMESTAMP_NTZ(CONVERT_TIMEZONE('UTC', ltz))"
30+
tags = ["null", "boundary"]
31+
expected_divergence = "timestamp_tz_submicro"
32+
33+
[[case]]
34+
id = "to_timestamp_ntz_of_utc_current_timestamp"
35+
description = "The snapshot time expression TO_TIMESTAMP_NTZ(CONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP())): only the column name and type are the receipt; the value reads the clock, so it is typing evidence."
36+
evidence = "typing"
37+
columns = [{ name = "n", type = "NUMBER(10,2)" }]
38+
rows = [
39+
["1.00"],
40+
]
41+
query = "SELECT TO_TIMESTAMP_NTZ(CONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP())) AS t FROM input"
42+
order = "ordered"
43+
tags = ["boundary"]

‎corpora/everyday-models/queries.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6233,7 +6233,7 @@ sql = '''with snapshot_query as (
62336233
union all
62346234
select * from updates'''
62356235
expect = "refused SQ0008 fn:CONVERT_TIMEZONE at 445..489"
6236-
expect_repository = "refused SQ0003 fn:TO_TIMESTAMP_NTZ at 428..490"
6236+
expect_repository = "translated"
62376237

62386238
[[query]]
62396239
category = "dbt_snapshot"
@@ -6351,7 +6351,7 @@ sql = '''with snapshot_query as (
63516351
union all
63526352
select * from deletes'''
63536353
expect = "refused SQ0008 fn:CONVERT_TIMEZONE at 445..489"
6354-
expect_repository = "refused SQ0003 fn:TO_TIMESTAMP_NTZ at 428..490"
6354+
expect_repository = "translated"
63556355

63566356
[[query]]
63576357
category = "dbt_snapshot"
@@ -6445,7 +6445,7 @@ sql = '''with snapshot_query as (
64456445
union all
64466446
select * from deletes'''
64476447
expect = "refused SQ0008 node:cast at 654..682"
6448-
expect_repository = "refused SQ0003 fn:TO_TIMESTAMP_NTZ at 2154..2216"
6448+
expect_repository = "translated"
64496449

64506450
[[query]]
64516451
category = "ddl_dml"

‎coverage/sql-size/baseline.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3716,6 +3716,8 @@
37163716
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_combination_0_timestamp_ntz": 322,
37173717
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_combination_1_date": 310,
37183718
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_combination_1_timestamp_ntz": 322,
3719+
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_tz_sub_microsecond": 353,
3720+
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_combination_0_timestamp_tz": 371,
37193721
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_boolean_hazards": 327,
37203722
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_date_extreme_years": 712,
37213723
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_date_hazards": 892,
@@ -7254,6 +7256,9 @@
72547256
"temporal/to_timestamp_format.toml#try_cast_text_timestamp": 16486,
72557257
"temporal/to_timestamp_format.toml#try_cast_text_timestamp_epoch": 5531,
72567258
"temporal/to_timestamp_format.toml#try_cast_text_timestamp_other_spelling": 5298,
7259+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_current_timestamp": 321,
7260+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_ltz": 683,
7261+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_tz": 924,
72577262
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_date": 1771,
72587263
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_date_typed": 299,
72597264
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_lower_precisions": 2335,

‎coverage/translation/results-baseline.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3802,9 +3802,9 @@
38023802
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_ltz_sub_microsecond": "refused",
38033803
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_tz_extreme_years": "refused",
38043804
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_tz_hazards": "refused",
3805-
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_tz_sub_microsecond": "refused",
3805+
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_0_timestamp_tz_sub_microsecond": "declared_divergence",
38063806
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_combination_0_timestamp_ltz": "refused",
3807-
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_combination_0_timestamp_tz": "refused",
3807+
"generated/hazards/to_timestamp_temporal__temporal.toml#fn_to_timestamp_ntz_probe_0_combination_0_timestamp_tz": "matching",
38083808
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_boolean_hazards": "matching",
38093809
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_date_extreme_years": "matching",
38103810
"generated/hazards/to_variant__semi-structured-typed.toml#fn_to_variant_0_date_hazards": "matching",
@@ -7774,6 +7774,9 @@
77747774
"temporal/to_timestamp_format.toml#try_cast_text_timestamp": "matching",
77757775
"temporal/to_timestamp_format.toml#try_cast_text_timestamp_epoch": "matching",
77767776
"temporal/to_timestamp_format.toml#try_cast_text_timestamp_other_spelling": "declared_divergence",
7777+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_current_timestamp": "shape_only",
7778+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_ltz": "declared_divergence",
7779+
"temporal/to_timestamp_ntz_of_utc.toml#to_timestamp_ntz_of_utc_tz": "declared_divergence",
77777780
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_date": "declared_divergence",
77787781
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_date_typed": "matching",
77797782
"temporal/to_timestamp_of_temporals.toml#to_timestamp_ntz_of_lower_precisions": "declared_divergence",

‎crates/sqlquack-rules/src/catalogue/constants.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,7 @@ pub const RULES: &[RuleSpec] = &[
16811681
RuleSpec {
16821682
id: TO_TIMESTAMP_TEMPORAL,
16831683
constructs: &["fn:TO_TIMESTAMP", "fn:TO_TIMESTAMP_NTZ"],
1684-
conditions: "A DATE or TIMESTAMP_NTZ(p) operand without a scale, cast to TIMESTAMP_NTZ(9) as CAST does, as recorded: a DATE is its midnight, a timestamp keeps its value, and the result is TIMESTAMP_NTZ(9) whatever p is; values outside DuckDB's nanosecond range rely on timestamp_range. TRY_ forms of these operands are not converted, as Snowflake rejects them (error 1065).",
1684+
conditions: "A DATE or TIMESTAMP_NTZ(p) operand without a scale, or for TO_TIMESTAMP_NTZ a TIMESTAMP_TZ known to hold the UTC offset, cast to TIMESTAMP_NTZ(9) as CAST does, as recorded: a DATE is its midnight, a timestamp keeps its value, a TIMESTAMP_TZ keeps its wall-clock time, and the result is TIMESTAMP_NTZ(9) whatever p is; values outside DuckDB's nanosecond range rely on timestamp_range. TRY_ forms of these operands are not converted, as Snowflake rejects them (error 1065).",
16851685
failures: &[],
16861686
},
16871687
RuleSpec {

‎crates/sqlquack-rules/src/conversion/_helpers/expressions/temporal_functions.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,16 @@ pub(crate) fn to_timestamp(
887887
let _temporal = log.rule(TO_TIMESTAMP_TEMPORAL);
888888
Ok(operand)
889889
}
890-
(SnowflakeType::Date | SnowflakeType::TimestampNtz { .. }, None, false) if !trying => {
890+
(
891+
SnowflakeType::Date
892+
| SnowflakeType::TimestampNtz { .. }
893+
| SnowflakeType::TimestampTz { .. },
894+
None,
895+
false,
896+
) if !trying
897+
&& (!matches!(operand.data_type, SnowflakeType::TimestampTz { .. })
898+
|| (operand.utc_offset && name.ends_with("_NTZ"))) =>
899+
{
891900
let _temporal = log.rule(TO_TIMESTAMP_TEMPORAL);
892901
let sql = cast_to(
893902
&operand,
Binary file not shown.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"arrow_sha256": "724632ed4b4a32182637d0fd1aebbf32354434e09f0486f8d3b4919e92b05754",
3+
"case_hash": "5915699155d6bb75a2d60a3157b9ff9cd765a18baaa105374116ec166043816e",
4+
"columns": [
5+
{
6+
"internal_size": null,
7+
"name": "VALUE",
8+
"nullable": true,
9+
"precision": 0,
10+
"scale": 9,
11+
"type_code": 8,
12+
"type_name": "TIMESTAMP_NTZ"
13+
}
14+
],
15+
"connector": "snowflake-connector-python 4.7.2",
16+
"duckdb_version": "v2.0.0",
17+
"effective_parameters": {
18+
"BINARY_INPUT_FORMAT": "HEX",
19+
"BINARY_OUTPUT_FORMAT": "HEX",
20+
"DATE_INPUT_FORMAT": "AUTO",
21+
"DATE_OUTPUT_FORMAT": "YYYY-MM-DD",
22+
"DEFAULT_NULL_ORDERING": "LAST",
23+
"GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON",
24+
"GEOMETRY_OUTPUT_FORMAT": "GeoJSON",
25+
"JSON_INDENT": "2",
26+
"QUOTED_IDENTIFIERS_IGNORE_CASE": "false",
27+
"STRICT_JSON_OUTPUT": "false",
28+
"TIMESTAMP_DAY_IS_ALWAYS_24H": "false",
29+
"TIMESTAMP_INPUT_FORMAT": "AUTO",
30+
"TIMESTAMP_LTZ_OUTPUT_FORMAT": "",
31+
"TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3",
32+
"TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM",
33+
"TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ",
34+
"TIMESTAMP_TZ_OUTPUT_FORMAT": "",
35+
"TIMEZONE": "UTC",
36+
"TIME_INPUT_FORMAT": "AUTO",
37+
"TIME_OUTPUT_FORMAT": "HH24:MI:SS",
38+
"TWO_DIGIT_CENTURY_START": "1970",
39+
"USE_CACHED_RESULT": "false",
40+
"WEEK_OF_YEAR_POLICY": "0",
41+
"WEEK_START": "0"
42+
},
43+
"error": null,
44+
"format": 1,
45+
"outcome": "rows",
46+
"parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61",
47+
"query_id_sha256": "a1deeed79a116ca90a3b64470e1c2afefebcf77f9da529fb583a636cc7881cc0",
48+
"recorded_at": "2026-11-06T15:49:01.096686+00:00",
49+
"recorder": "sqlquack-record 0.1.0",
50+
"row_count": 5,
51+
"session_parameters": {
52+
"DEFAULT_NULL_ORDERING": "LAST",
53+
"QUOTED_IDENTIFIERS_IGNORE_CASE": false,
54+
"TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ",
55+
"TIMEZONE": "UTC",
56+
"USE_CACHED_RESULT": false,
57+
"WEEK_OF_YEAR_POLICY": 0,
58+
"WEEK_START": 0
59+
},
60+
"warehouse_version": "10.40.102"
61+
}
Binary file not shown.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"arrow_sha256": "27ed0f3547f0c85b442fbabdaa7190792750d95e16e21b61ac248d2170fe274e",
3+
"case_hash": "718f96b27d46626afc8964a5ab93c45b1d01513be668beb3b32fc911ade960ad",
4+
"columns": [
5+
{
6+
"internal_size": null,
7+
"name": "VALUE",
8+
"nullable": true,
9+
"precision": 0,
10+
"scale": 9,
11+
"type_code": 8,
12+
"type_name": "TIMESTAMP_NTZ"
13+
}
14+
],
15+
"connector": "snowflake-connector-python 4.7.2",
16+
"duckdb_version": "v2.0.0",
17+
"effective_parameters": {
18+
"BINARY_INPUT_FORMAT": "HEX",
19+
"BINARY_OUTPUT_FORMAT": "HEX",
20+
"DATE_INPUT_FORMAT": "AUTO",
21+
"DATE_OUTPUT_FORMAT": "YYYY-MM-DD",
22+
"DEFAULT_NULL_ORDERING": "LAST",
23+
"GEOGRAPHY_OUTPUT_FORMAT": "GeoJSON",
24+
"GEOMETRY_OUTPUT_FORMAT": "GeoJSON",
25+
"JSON_INDENT": "2",
26+
"QUOTED_IDENTIFIERS_IGNORE_CASE": "false",
27+
"STRICT_JSON_OUTPUT": "false",
28+
"TIMESTAMP_DAY_IS_ALWAYS_24H": "false",
29+
"TIMESTAMP_INPUT_FORMAT": "AUTO",
30+
"TIMESTAMP_LTZ_OUTPUT_FORMAT": "",
31+
"TIMESTAMP_NTZ_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3",
32+
"TIMESTAMP_OUTPUT_FORMAT": "YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM",
33+
"TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ",
34+
"TIMESTAMP_TZ_OUTPUT_FORMAT": "",
35+
"TIMEZONE": "UTC",
36+
"TIME_INPUT_FORMAT": "AUTO",
37+
"TIME_OUTPUT_FORMAT": "HH24:MI:SS",
38+
"TWO_DIGIT_CENTURY_START": "1970",
39+
"USE_CACHED_RESULT": "false",
40+
"WEEK_OF_YEAR_POLICY": "0",
41+
"WEEK_START": "0"
42+
},
43+
"error": null,
44+
"format": 1,
45+
"outcome": "rows",
46+
"parameters_sha256": "d094d15417903748055eca8fde8cf2884751253939f05ad5ad8d9bf14299ac61",
47+
"query_id_sha256": "87ee703fd297c479bdfeec2357c12c9473d649ef94388b70a2d3159ed0c36b8b",
48+
"recorded_at": "2026-11-06T15:49:01.096686+00:00",
49+
"recorder": "sqlquack-record 0.1.0",
50+
"row_count": 3,
51+
"session_parameters": {
52+
"DEFAULT_NULL_ORDERING": "LAST",
53+
"QUOTED_IDENTIFIERS_IGNORE_CASE": false,
54+
"TIMESTAMP_TYPE_MAPPING": "TIMESTAMP_NTZ",
55+
"TIMEZONE": "UTC",
56+
"USE_CACHED_RESULT": false,
57+
"WEEK_OF_YEAR_POLICY": 0,
58+
"WEEK_START": 0
59+
},
60+
"warehouse_version": "10.40.102"
61+
}

0 commit comments

Comments
 (0)