Skip to content

Commit 357e6c2

Browse files
authored
Merge branch 'main' into test/duckshop-blocker-parity
2 parents b254221 + 330b1ea commit 357e6c2

16 files changed

Lines changed: 338 additions & 30 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.187.1"
2+
".": "0.187.3"
33
}

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [0.187.3](https://github.com/sqlstack-io/sqlbuild/compare/v0.187.2...v0.187.3) (2026-11-02)
4+
5+
6+
### Bug Fixes
7+
8+
* Snowflake time and timestamp precision no longer fails source contracts ([#1294](https://github.com/sqlstack-io/sqlbuild/issues/1294)) ([9c41a61](https://github.com/sqlstack-io/sqlbuild/commit/9c41a61825c3dd5db34f91447143821684bc718c))
9+
10+
## [0.187.2](https://github.com/sqlstack-io/sqlbuild/compare/v0.187.1...v0.187.2) (2026-11-02)
11+
12+
13+
### Performance Improvements
14+
15+
* **rules:** pin SQLClean 0.4.3 for cheaper unused-CTE facts ([#1296](https://github.com/sqlstack-io/sqlbuild/issues/1296)) ([1ba0a3d](https://github.com/sqlstack-io/sqlbuild/commit/1ba0a3df837cdf34c4ef81f9a163127fe62841a9))
16+
317
## [0.187.1](https://github.com/sqlstack-io/sqlbuild/compare/v0.187.0...v0.187.1) (2026-11-02)
418

519

‎Cargo.lock‎

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ python-boundary = "sqlbuild-python"
5555
polyglot-floor = "sqlbuild-analysis"
5656

5757
[workspace.package]
58-
version = "0.187.1"
58+
version = "0.187.3"
5959
edition = "2024"
6060
license = "Apache-2.0"
6161
publish = false
@@ -140,7 +140,7 @@ sqlbuild-scopes = { path = "crates/sqlbuild-scopes" }
140140
sqlbuild-sqltext = { path = "crates/sqlbuild-sqltext" }
141141
sqlbuild-state = { path = "crates/sqlbuild-state" }
142142
# SQL lint Rules, quality Rules, the formatter and the canonical token stream.
143-
sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "571e40a746b3d8d20d6118620f05428eca45243c" }
143+
sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" }
144144
stacker = "0.1"
145145
syn = { version = "2", features = ["full", "visit", "extra-traits"] }
146146
tempfile = "3"

‎crates/sqlbuild-analysis/src/type_system/constants.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,12 @@ pub(crate) const POLYGLOT_TYPE_NAME_ALIASES: &[(&str, &str)] = &[
4343
pub(crate) const MAX_FUNCTION_CALL_DEPTH: usize = 512;
4444
/// Python's `str(0)`, the only integer text Python treats as false.
4545
pub(crate) const ZERO_TEXT: &str = "0";
46+
/// Snowflake time and timestamp types, word separators removed, whose precision metadata omits.
47+
pub(crate) const SNOWFLAKE_PRECISION_TEMPORAL_TYPES: &[&str] =
48+
&["TIME", "TIMESTAMPLTZ", "TIMESTAMPNTZ", "TIMESTAMPTZ"];
49+
/// The character that opens a type's parameter list.
50+
pub(crate) const TYPE_PARAMETERS_OPEN: char = '(';
51+
/// The word separator in Snowflake type names such as `TIMESTAMP_TZ`.
52+
pub(crate) const TYPE_WORD_SEPARATOR: char = '_';
53+
/// The character that closes a type's parameter list.
54+
pub(crate) const TYPE_PARAMETERS_CLOSE: char = ')';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod normalize_type;
22
pub mod polyglot_type_args;
3+
pub mod same_type;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//! `same_type`: equal normalized types, a Snowflake temporal precision left unstated matching any.
2+
3+
use crate::type_system::_helpers::dialects::coerce_type_dialect;
4+
use crate::type_system::constants::{
5+
SNOWFLAKE_PRECISION_TEMPORAL_TYPES, TYPE_PARAMETERS_CLOSE, TYPE_PARAMETERS_OPEN,
6+
TYPE_WORD_SEPARATOR,
7+
};
8+
use crate::type_system::models::{NormalizedType, TypeDialect};
9+
10+
/// Whether `left` and `right` name the same type under `dialect`, up to an unstated precision.
11+
#[must_use]
12+
pub fn same_type(left: &NormalizedType, right: &NormalizedType, dialect: &str) -> bool {
13+
if left == right {
14+
return true;
15+
}
16+
if coerce_type_dialect(dialect) != Some(TypeDialect::Snowflake) {
17+
return false;
18+
}
19+
let (Some((left_base, left_precision)), Some((right_base, right_precision))) = (
20+
temporal_parts(&left.normalized_name),
21+
temporal_parts(&right.normalized_name),
22+
) else {
23+
return false;
24+
};
25+
left_base == right_base
26+
&& (left_precision.is_none()
27+
|| right_precision.is_none()
28+
|| left_precision == right_precision)
29+
}
30+
31+
/// A Snowflake time or timestamp type's compact base name and its stated precision text.
32+
fn temporal_parts(name: &str) -> Option<(String, Option<&str>)> {
33+
let (base, parameters): (&str, Option<&str>) = match name.split_once(TYPE_PARAMETERS_OPEN) {
34+
Some((base, rest)) => (base, rest.strip_suffix(TYPE_PARAMETERS_CLOSE)),
35+
None => (name, None),
36+
};
37+
let compact: String = base.trim().replace(TYPE_WORD_SEPARATOR, "");
38+
SNOWFLAKE_PRECISION_TEMPORAL_TYPES
39+
.contains(&compact.as_str())
40+
.then(|| (compact, parameters.map(str::trim)))
41+
}

‎crates/sqlbuild-analysis/src/type_system/tests.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ mod helpers;
44
mod normalize_type;
55
#[path = "tests/test_python_text.rs"]
66
mod python_text;
7+
#[path = "tests/test_same_type.rs"]
8+
mod same_type;
79
#[path = "tests/test_types.rs"]
810
mod test_types;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use crate::type_system::main::normalize_type::normalize_type;
2+
use crate::type_system::main::same_type::same_type;
3+
use crate::type_system::tests::test_types::SameTypeTestCase;
4+
5+
#[test]
6+
fn given_two_types_when_comparing_then_an_unstated_snowflake_temporal_precision_matches() {
7+
let test_cases = [
8+
SameTypeTestCase {
9+
description: "a declared millisecond time matches metadata's precision-less time",
10+
dialect: "snowflake",
11+
types: ("TIME(3)", "TIME"),
12+
expected_same: true,
13+
},
14+
SameTypeTestCase {
15+
description: "a declared nanosecond time matches metadata's precision-less time",
16+
dialect: "snowflake",
17+
types: ("TIME", "TIME(9)"),
18+
expected_same: true,
19+
},
20+
SameTypeTestCase {
21+
description: "two stated time precisions must agree",
22+
dialect: "snowflake",
23+
types: ("TIME(3)", "TIME(9)"),
24+
expected_same: false,
25+
},
26+
SameTypeTestCase {
27+
description: "a declared offset timestamp precision matches metadata's spelling",
28+
dialect: "snowflake",
29+
types: ("TIMESTAMP_TZ(9)", "TIMESTAMP_TZ"),
30+
expected_same: true,
31+
},
32+
SameTypeTestCase {
33+
description: "two stated offset timestamp precisions must agree",
34+
dialect: "snowflake",
35+
types: ("TIMESTAMP_TZ(3)", "TIMESTAMP_TZ(9)"),
36+
expected_same: false,
37+
},
38+
SameTypeTestCase {
39+
description: "local time zone timestamps with and without precision",
40+
dialect: "snowflake",
41+
types: ("TIMESTAMP_LTZ(6)", "TIMESTAMP_LTZ"),
42+
expected_same: true,
43+
},
44+
SameTypeTestCase {
45+
description: "an offset timestamp is not a local time zone timestamp",
46+
dialect: "snowflake",
47+
types: ("TIMESTAMP_TZ", "TIMESTAMP_LTZ(9)"),
48+
expected_same: false,
49+
},
50+
SameTypeTestCase {
51+
description: "time is not a timestamp",
52+
dialect: "snowflake",
53+
types: ("TIME(9)", "TIMESTAMP_NTZ"),
54+
expected_same: false,
55+
},
56+
SameTypeTestCase {
57+
description: "other dialects keep exact comparison",
58+
dialect: "duckdb",
59+
types: ("TIME(3)", "TIME"),
60+
expected_same: false,
61+
},
62+
SameTypeTestCase {
63+
description: "an unstated text length is still a different type",
64+
dialect: "snowflake",
65+
types: ("VARCHAR(10)", "VARCHAR(20)"),
66+
expected_same: false,
67+
},
68+
];
69+
for test_case in test_cases {
70+
let (left, right) = test_case.types;
71+
let left = normalize_type(left, test_case.dialect)
72+
.expect("the type normalizes")
73+
.normalized;
74+
let right = normalize_type(right, test_case.dialect)
75+
.expect("the type normalizes")
76+
.normalized;
77+
assert_eq!(
78+
same_type(&left, &right, test_case.dialect),
79+
test_case.expected_same,
80+
"{}",
81+
test_case.description
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)