Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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
39 changes: 39 additions & 0 deletions cases/translation/functions/flatten.path.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
category = "translation"
construct = "sf.functions.flatten"
session_profile = "utc-v1"

[[case]]
id = "t001_object_under_path"
description = "PATH reaching an object: one row per key, with KEY, VALUE and a PATH prefixed with the given path, for a plain key and one with a space. Catches: the prefix joined without a dot, or a quoted key written as a plain one"
columns = [{ name = "a", type = "VARCHAR" }]
rows = [
["'{\"x\":{\"y\":{\"b\":1,\"a c\":2}}}'"],
]
query = "SELECT i.ordinal, f.key, f.index, f.value, f.path FROM input i, LATERAL FLATTEN(INPUT => PARSE_JSON(i.a), PATH => 'x.y') f ORDER BY i.ordinal, f.key"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t002_path_key_case"
description = "PATH keys match case-sensitively: 'Items' finds nothing where the key is items, and one-step 'items' reaches the array. Catches: keys matched ignoring case"
columns = [{ name = "a", type = "VARCHAR" }]
rows = [
["'{\"items\":[10,20]}'"],
]
query = "SELECT 'lower' AS k, f.index, f.value, f.path FROM input i, LATERAL FLATTEN(INPUT => PARSE_JSON(i.a), PATH => 'items') f UNION ALL SELECT 'upper', f.index, f.value, f.path FROM input i, LATERAL FLATTEN(INPUT => PARSE_JSON(i.a), PATH => 'Items') f ORDER BY 1, 2"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t003_scalar_and_null_at_path"
description = "PATH reaching a scalar, a JSON null and an empty array gives no rows. Catches: a scalar flattened into one row, or a JSON null giving a row"
columns = [{ name = "a", type = "VARCHAR" }]
rows = [
["'{\"x\":5}'"],
["'{\"x\":null}'"],
["'{\"x\":[]}'"],
["'{\"x\":[7]}'"],
]
query = "SELECT i.ordinal, f.index, f.value, f.path FROM input i, LATERAL FLATTEN(INPUT => PARSE_JSON(i.a), PATH => 'x') f ORDER BY i.ordinal, f.index"
order = "ordered"
tags = ["type-combination"]
57 changes: 57 additions & 0 deletions cases/translation/functions/pow.power-of-two.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
category = "translation"
construct = "sf.functions.pow.power"
session_profile = "utc-v1"

[[case]]
id = "t001_whole_exponents"
description = "POWER(2, n) for whole exponents from -52 to 64, as dbt_utils.date_spine writes it for each bit. Catches: an inexact power of two, an integer result, or a lost low bit past 2^53"
columns = [{ name = "n", type = "NUMBER(38,0)" }]
rows = [
["0"], ["1"], ["2"], ["3"], ["4"], ["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"],
["31"], ["32"], ["52"], ["53"], ["62"], ["63"], ["64"],
["-1"], ["-2"], ["-52"],
]
query = "SELECT POWER(2, n) AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t002_literal_exponents_in_a_sum"
description = "The dbt_utils.date_spine shape: a NUMBER times POWER(2, k) for literal k, summed, plus 1. Catches: the result typed NUMBER instead of FLOAT, or a power other than 2^k"
columns = [{ name = "a", type = "NUMBER(38,0)" }, { name = "b", type = "NUMBER(38,0)" }]
rows = [
["0", "0"],
["1", "0"],
["1", "1"],
["NULL", "1"],
]
query = "SELECT a * POWER(2, 0) + b * POWER(2, 1) + 1 AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t003_range_edges"
description = "The largest and smallest finite powers of two in a double, 2^1023 and 2^-1074, and 2^-1075 below the smallest. Catches: 2^1023 overflowing, or the subnormal 2^-1074 flushed to 0"
columns = [{ name = "n", type = "NUMBER(38,0)" }]
rows = [["1023"], ["-1074"], ["-1075"]]
query = "SELECT POWER(2, n) AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t004_overflow"
description = "2^1024, past the largest double. Catches: a finite value or an error where Snowflake gives its overflow result"
columns = [{ name = "n", type = "NUMBER(38,0)" }]
rows = [["1024"]]
query = "SELECT POWER(2, n) AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t005_null_exponent"
description = "POWER(2, NULL). Catches: 1 or 2 returned instead of NULL"
columns = [{ name = "n", type = "NUMBER(38,0)" }]
rows = [["NULL"], ["3"]]
query = "SELECT POWER(2, n) AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
category = "translation"
construct = "sf.functions.regexp-substr"
session_profile = "utc-v1"

[[case]]
id = "t001_empty_match_at_and_past_the_end"
description = "REGEXP_SUBSTR(a, 'x*', n) on 'abc' with n at the last character, one past it and two past it, where 'x*' matches the empty string. Catches: '' where Snowflake gives NULL, or an error past the end where Snowflake answers"
columns = [{ name = "a", type = "VARCHAR" }, { name = "n", type = "NUMBER(38,0)" }]
rows = [
["'abc'", "3"],
["'abc'", "4"],
["'abc'", "5"],
]
query = "SELECT n, REGEXP_SUBSTR(a, 'x*', n) AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]
56 changes: 56 additions & 0 deletions cases/translation/functions/to-char.month-names.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
category = "translation"
construct = "sf.functions.to-char.to-char"
session_profile = "utc-v1"

[[case]]
id = "t001_mon_every_month"
description = "TO_CHAR(date, 'MON') for a date in every month, as dbt_date.get_date_dimension writes month_name_short. Catches: the month number written, the wrong letter case, or a locale's abbreviation"
columns = [{ name = "d", type = "DATE" }]
rows = [
["'2024-01-15'"], ["'2024-02-29'"], ["'2024-03-15'"], ["'2024-04-15'"], ["'2024-05-15'"], ["'2024-06-15'"],
["'2024-07-15'"], ["'2024-08-15'"], ["'2024-09-15'"], ["'2024-10-15'"], ["'2024-11-15'"], ["'2024-12-31'"],
]
query = "SELECT TO_CHAR(d, 'MON') AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t002_mmmm_every_month"
description = "TO_CHAR(date, 'MMMM') for a date in every month, as dbt_date.get_date_dimension writes month_name. Catches: MMMM read as MM twice ('0101'), the wrong letter case, or an abbreviation"
columns = [{ name = "d", type = "DATE" }]
rows = [
["'2024-01-15'"], ["'2024-02-29'"], ["'2024-03-15'"], ["'2024-04-15'"], ["'2024-05-15'"], ["'2024-06-15'"],
["'2024-07-15'"], ["'2024-08-15'"], ["'2024-09-15'"], ["'2024-10-15'"], ["'2024-11-15'"], ["'2024-12-31'"],
]
query = "SELECT TO_CHAR(d, 'MMMM') AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t003_month_names_in_a_format"
description = "MON and MMMM beside other fields and on a timestamp. Catches: a field next to the name misread, or the timestamp's time leaking into the name"
columns = [{ name = "d", type = "DATE" }, { name = "t", type = "TIMESTAMP_NTZ(9)" }]
rows = [
["'2024-09-05'", "'2024-05-01 23:59:59'"],
]
query = "SELECT TO_CHAR(d, 'DD MON YYYY') AS a, TO_CHAR(d, 'MMMM DD, YYYY') AS b, TO_CHAR(t, 'MON') AS c, TO_CHAR(t, 'MMMM') AS e FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t004_null"
description = "A NULL date. Catches: a month name for NULL"
columns = [{ name = "d", type = "DATE" }]
rows = [["NULL"], ["'2024-03-01'"]]
query = "SELECT TO_CHAR(d, 'MON') AS a, TO_CHAR(d, 'MMMM') AS b FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null"]

[[case]]
id = "t007_names_beside_month_and_day"
description = "MONMM and MMMMDD, a name directly before another field. Catches: the name's letters read as part of the next field"
columns = [{ name = "d", type = "DATE" }]
rows = [["'2024-09-05'"]]
query = "SELECT TO_CHAR(d, 'MONMM') AS a, TO_CHAR(d, 'MMMMDD') AS b FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]
22 changes: 18 additions & 4 deletions coverage/translation/results-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6919,7 +6919,7 @@
"semi-structured/flatten_outer.toml#flatten_top_level_outer_plain": "matching",
"semi-structured/flatten_outer.toml#flatten_top_level_outer_recursive": "refused",
"semi-structured/flatten_outer.toml#lateral_subquery_without_rows": "matching",
"semi-structured/flatten_recursive.toml#flatten_path_option": "refused",
"semi-structured/flatten_recursive.toml#flatten_path_option": "matching",
"semi-structured/flatten_recursive.toml#flatten_recursive": "matching",
"semi-structured/flatten_recursive.toml#flatten_recursive_deep": "matching",
"semi-structured/flatten_recursive.toml#flatten_recursive_filtered": "matching",
Expand Down Expand Up @@ -9388,8 +9388,11 @@
"translation/functions/first-value.toml#t002_ignore_nulls": "matching",
"translation/functions/first-value.toml#t003_frame": "matching",
"translation/functions/first-value.toml#t004_null_expr": "matching",
"translation/functions/flatten.path.toml#t001_object_under_path": "matching",
"translation/functions/flatten.path.toml#t002_path_key_case": "matching",
"translation/functions/flatten.path.toml#t003_scalar_and_null_at_path": "matching",
"translation/functions/flatten.toml#t001_default": "matching",
"translation/functions/flatten.toml#t002_path": "refused",
"translation/functions/flatten.toml#t002_path": "matching",
"translation/functions/flatten.toml#t003_outer": "matching",
"translation/functions/flatten.toml#t004_recursive": "matching",
"translation/functions/flatten.toml#t005_mode": "matching",
Expand Down Expand Up @@ -10228,6 +10231,11 @@
"translation/functions/pow.pow.toml#t007_overflow_inf": "refused",
"translation/functions/pow.pow.toml#t008_neg_base": "refused",
"translation/functions/pow.pow.toml#t009_neg_frac": "refused",
"translation/functions/pow.power-of-two.toml#t001_whole_exponents": "matching",
"translation/functions/pow.power-of-two.toml#t002_literal_exponents_in_a_sum": "matching",
"translation/functions/pow.power-of-two.toml#t003_range_edges": "matching",
"translation/functions/pow.power-of-two.toml#t004_overflow": "matching",
"translation/functions/pow.power-of-two.toml#t005_null_exponent": "matching",
"translation/functions/pow.power.toml#t001_int": "refused",
"translation/functions/pow.power.toml#t002_dec": "refused",
"translation/functions/pow.power.toml#t003_float": "refused",
Expand Down Expand Up @@ -10320,6 +10328,7 @@
"translation/functions/regexp-substr-all.toml#t009_null_regex_parameters": "refused",
"translation/functions/regexp-substr-all.toml#t010_null_group_num": "refused",
"translation/functions/regexp-substr-all.toml#t011_empty": "matching",
"translation/functions/regexp-substr.empty-match-position.toml#t001_empty_match_at_and_past_the_end": "refused",
"translation/functions/regexp-substr.toml#t001_varchar": "matching",
"translation/functions/regexp-substr.toml#t002_position_occurrence": "refused",
"translation/functions/regexp-substr.toml#t003_group": "refused",
Expand Down Expand Up @@ -11155,13 +11164,18 @@
"translation/functions/to-boolean.toml#t003_invalid_string": "matching_error",
"translation/functions/to-boolean.toml#t004_nan": "refused",
"translation/functions/to-boolean.toml#t005_null_string_or_numeric_expr": "matching",
"translation/functions/to-char.month-names.toml#t001_mon_every_month": "matching",
"translation/functions/to-char.month-names.toml#t002_mmmm_every_month": "matching",
"translation/functions/to-char.month-names.toml#t003_month_names_in_a_format": "refused",
"translation/functions/to-char.month-names.toml#t004_null": "matching",
"translation/functions/to-char.month-names.toml#t007_names_beside_month_and_day": "refused",
"translation/functions/to-char.to-char.toml#t001_number_format": "refused",
"translation/functions/to-char.to-char.toml#t002_date_format": "refused",
"translation/functions/to-char.to-char.toml#t002_date_format": "matching",
"translation/functions/to-char.to-char.toml#t003_binary_format": "refused",
"translation/functions/to-char.to-char.toml#t004_variant": "refused",
"translation/functions/to-char.to-char.toml#t005_null_expr": "matching",
"translation/functions/to-char.to-varchar.toml#t001_number_format": "refused",
"translation/functions/to-char.to-varchar.toml#t002_date_format": "refused",
"translation/functions/to-char.to-varchar.toml#t002_date_format": "matching",
"translation/functions/to-char.to-varchar.toml#t003_binary_format": "refused",
"translation/functions/to-char.to-varchar.toml#t004_variant": "matching",
"translation/functions/to-char.to-varchar.toml#t005_null_expr": "matching",
Expand Down
6 changes: 3 additions & 3 deletions crates/sqlquack-rules/src/catalogue/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ pub const RULES: &[RuleSpec] = &[
RuleSpec {
id: POWER_RULE,
constructs: &["fn:POWER"],
conditions: "POWER of the constant 10 to a whole NUMBER exponent, as the FLOAT nearest the power, ties to even, as recorded from 10^-3 to 10^38 (10^23 is the tie DuckDB's pow rounds the other way); far out of range inf or 0. NULL is NULL. Other bases refuse: DuckDB's pow is not correctly rounded, and no recording shows Snowflake's rounding.",
conditions: "POWER of the constant 10 to a whole NUMBER exponent, as the FLOAT nearest the power, ties to even, as recorded from 10^-3 to 10^38 (10^23 is the tie DuckDB's pow rounds the other way); far out of range inf or 0. NULL is NULL. POWER of the constant 2 to a whole NUMBER exponent is the exact power of two a double holds, from 2^-1074 to 2^1023, as recorded; NULL is NULL. Other bases refuse: DuckDB's pow is not correctly rounded, and no recording shows Snowflake's rounding.",
failures: &[],
},
RuleSpec {
Expand Down Expand Up @@ -993,7 +993,7 @@ pub const RULES: &[RuleSpec] = &[
RuleSpec {
id: FLATTEN_RULE,
constructs: &["node:lateral", "fn:FLATTEN"],
conditions: "LATERAL FLATTEN of a VARIANT input after another FROM source or after JOIN ... ON clauses, as a comma or CROSS JOIN source, with OUTER => a boolean literal at most: INDEX and PATH [i] for array elements, KEY and PATH for object keys (raising for keys that are not plain names), VALUE and THIS; scalars, JSON null and NULL give no rows. Under OUTER => TRUE an input with no rows gives one row: PATH '' and THIS the input for an empty container MODE takes, every column NULL otherwise (JSON null, NULL, a missing path, a scalar, a kind MODE skips), as recorded. MODE => 'ARRAY' or 'OBJECT' without RECURSIVE takes only that kind. RECURSIVE => TRUE walks every depth with keys in byte order, writing a key of letters, digits, `_`, spaces and dots that is not a plain name as ['key'] and raising for any other, MODE => 'BOTH', 'OBJECT' or 'ARRAY' choosing what it descends into, a scalar input giving one row of itself under BOTH and raising under OBJECT or ARRAY; RECURSIVE with OUTER, SEQ and PATH => are not converted.",
conditions: "LATERAL FLATTEN of a VARIANT input after another FROM source or after JOIN ... ON clauses, as a comma or CROSS JOIN source, with OUTER => a boolean literal at most: INDEX and PATH [i] for array elements, KEY and PATH for object keys (raising for keys that are not plain names), VALUE and THIS; scalars, JSON null and NULL give no rows. Under OUTER => TRUE an input with no rows gives one row: PATH '' and THIS the input for an empty container MODE takes, every column NULL otherwise (JSON null, NULL, a missing path, a scalar, a kind MODE skips), as recorded. MODE => 'ARRAY' or 'OBJECT' without RECURSIVE takes only that kind. RECURSIVE => TRUE walks every depth with keys in byte order, writing a key of letters, digits, `_`, spaces and dots that is not a plain name as ['key'] and raising for any other, MODE => 'BOTH', 'OBJECT' or 'ARRAY' choosing what it descends into, a scalar input giving one row of itself under BOTH and raising under OBJECT or ARRAY; PATH => a literal of dot-separated plain keys flattens the value at that path, PATH prefixed with it ('x.y[0]'), a missing path giving no rows, as recorded; other PATHs, PATH with RECURSIVE or OUTER, RECURSIVE with OUTER, and SEQ are not converted.",
failures: &[],
},
RuleSpec {
Expand Down Expand Up @@ -1169,7 +1169,7 @@ pub const RULES: &[RuleSpec] = &[
RuleSpec {
id: TEMPORAL_TEXT_FORMAT,
constructs: &["fn:TO_VARCHAR", "fn:TO_CHAR"],
conditions: "A TIME and a format of only HH24, MI, SS and FF1 to FF6 fields, truncating, as recorded; a DATE or TIMESTAMP_NTZ and a literal format of YYYY, MM and DD and, for timestamps, HH24, MI, SS and FF, FF3, FF6 and FF9 fields (each truncating, as recorded; FF writes as many digits as the TIMESTAMP_NTZ precision, recorded for 0, 3, 6 and 9), in any case, between the separators - _ / : . and space: each field zero-padded, as strftime writes it.",
conditions: "A TIME and a format of only HH24, MI, SS and FF1 to FF6 fields, truncating, as recorded; a DATE or TIMESTAMP_NTZ and a literal format of YYYY, MM and DD and, for timestamps, HH24, MI, SS and FF, FF3, FF6 and FF9 fields (each truncating, as recorded; FF writes as many digits as the TIMESTAMP_NTZ precision, recorded for 0, 3, 6 and 9), in any case, between the separators - _ / : . and space: each field zero-padded, as strftime writes it. MON and MMMM, written in upper case, write the English month's abbreviation (Jan) and full name (January) in title case, as recorded for every month. MMMM directly before DD writes the name then the day (MMMMDD is September05), as recorded. A format where two adjacent fields would spell a longer run of one letter (MMMMMM, DDDD, SSSS), where a field follows MON directly (MONMM writes the full name, as recorded), where a field other than DD follows MMMM directly, or where a month name directly follows another field, is not converted, nor any element other than these.",
failures: &[],
},
RuleSpec {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use crate::conversion::_helpers::support::site::Site;

use crate::conversion::constants::{
BITWISE, HALF_AWAY_FROM_ZERO, HALF_TO_EVEN, LN_NAME, LOGARITHM, NUMERIC_FUNCTION, POWER_BASE,
POWER_RULE, ROUND_FUNCTION, ROUND_WITH_MODE_ARGUMENTS, ROUNDING, SIGN_RULE, SQRT_NAME,
POWER_BASE_TWO, POWER_RULE, ROUND_FUNCTION, ROUND_WITH_MODE_ARGUMENTS, ROUNDING, SIGN_RULE,
SQRT_NAME,
};
use sqlquack_policy::policy::constants::NUMERIC_RANGE;

Expand Down Expand Up @@ -86,7 +87,7 @@ pub(crate) fn logarithm(
Ok(Emitted::new(sql, SnowflakeType::Float))
}

/// POWER(10, n) of a whole NUMBER exponent, as the FLOAT nearest the power; other bases refuse.
/// POWER(10, n) or POWER(2, n) of a whole NUMBER exponent, as a FLOAT; other bases refuse.
pub(crate) fn power(
arguments: &[Value],
construct: &Construct,
Expand All @@ -98,19 +99,27 @@ pub(crate) fn power(
return Err(refuse("POWER with these arguments".to_owned()));
};
let base = convert_expression(base, site)?;
if base.literal != Some(Literal::Number(POWER_BASE.to_owned())) {
let two = base.literal == Some(Literal::Number(POWER_BASE_TWO.to_owned()));
if base.literal != Some(Literal::Number(POWER_BASE.to_owned())) && !two {
return Err(refuse(
"POWER of a base other than the constant 10, whose rounding no recording shows"
"POWER of a base other than the constant 10 or 2, whose rounding no recording shows"
.to_owned(),
));
}
let exponent = convert_expression(exponent, site)?;
match exponent.data_type {
SnowflakeType::Number { scale: 0, .. } | SnowflakeType::Null => Ok(Emitted::new(
match (exponent.data_type, two) {
(SnowflakeType::Number { scale: 0, .. } | SnowflakeType::Null, true) => Ok(Emitted::new(
format!("pow(CAST(2 AS DOUBLE), CAST({} AS DOUBLE))", exponent.sql),
SnowflakeType::Float,
)),
(SnowflakeType::Number { scale: 0, .. } | SnowflakeType::Null, false) => Ok(Emitted::new(
power_of_ten(&exponent.sql, site.context.log),
SnowflakeType::Float,
)),
other => Err(refuse(format!("POWER(10, n) of a {other} exponent"))),
(other, _) => Err(refuse(format!(
"POWER({}, n) of a {other} exponent",
if two { POWER_BASE_TWO } else { POWER_BASE }
))),
}
}

Expand Down
Loading
Loading