Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 8 additions & 8 deletions coverage/translation/results-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -9373,11 +9373,11 @@
"translation/functions/getdate.toml#t003_null_none": "refused",
"translation/functions/getvariable.toml#t001_unset": "refused",
"translation/functions/getvariable.toml#t002_null_name": "refused",
"translation/functions/greatest-ignore-nulls.toml#t001_number": "refused",
"translation/functions/greatest-ignore-nulls.toml#t001_number": "matching",
"translation/functions/greatest-ignore-nulls.toml#t002_first_type": "refused",
"translation/functions/greatest-ignore-nulls.toml#t003_varchar": "refused",
"translation/functions/greatest-ignore-nulls.toml#t004_date": "refused",
"translation/functions/greatest-ignore-nulls.toml#t005_null_expr": "refused",
"translation/functions/greatest-ignore-nulls.toml#t003_varchar": "matching",
"translation/functions/greatest-ignore-nulls.toml#t004_date": "matching",
"translation/functions/greatest-ignore-nulls.toml#t005_null_expr": "matching",
"translation/functions/greatest.toml#t001_number": "matching",
"translation/functions/greatest.toml#t002_first_type": "refused",
"translation/functions/greatest.toml#t003_varchar": "matching",
Expand Down Expand Up @@ -9750,11 +9750,11 @@
"translation/functions/lead.toml#t002_ignore_nulls": "matching",
"translation/functions/lead.toml#t003_negative_offset": "matching",
"translation/functions/lead.toml#t004_null_expr": "matching",
"translation/functions/least-ignore-nulls.toml#t001_number": "refused",
"translation/functions/least-ignore-nulls.toml#t001_number": "matching",
"translation/functions/least-ignore-nulls.toml#t002_first_type": "refused",
"translation/functions/least-ignore-nulls.toml#t003_varchar": "refused",
"translation/functions/least-ignore-nulls.toml#t004_date": "refused",
"translation/functions/least-ignore-nulls.toml#t005_null_expr": "refused",
"translation/functions/least-ignore-nulls.toml#t003_varchar": "matching",
"translation/functions/least-ignore-nulls.toml#t004_date": "matching",
"translation/functions/least-ignore-nulls.toml#t005_null_expr": "matching",
"translation/functions/least.toml#t001_number": "matching",
"translation/functions/least.toml#t002_first_type": "refused",
"translation/functions/least.toml#t003_varchar": "matching",
Expand Down
9 changes: 7 additions & 2 deletions crates/sqlquack-rules/src/catalogue/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1126,8 +1126,13 @@ pub const RULES: &[RuleSpec] = &[
},
RuleSpec {
id: LEAST_GREATEST,
constructs: &["fn:LEAST", "fn:GREATEST"],
conditions: "Arguments that compare directly with each other, NUMBER beside FLOAT included as recorded; the result has their unified type and is NULL when any argument is NULL (DuckDB's own functions skip NULLs).",
constructs: &[
"fn:LEAST",
"fn:GREATEST",
"fn:LEAST_IGNORE_NULLS",
"fn:GREATEST_IGNORE_NULLS",
],
conditions: "Arguments that compare directly with each other, NUMBER beside FLOAT included as recorded; the result has their unified type. LEAST and GREATEST are NULL when any argument is NULL; LEAST_IGNORE_NULLS and GREATEST_IGNORE_NULLS skip NULLs, as DuckDB's own functions do, and are NULL only when every argument is.",
failures: &[],
},
RuleSpec {
Expand Down
29 changes: 19 additions & 10 deletions crates/sqlquack-rules/src/conversion/_helpers/expressions/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ use crate::conversion::constants::{
};
use sqlquack_policy::policy::constants::NUMERIC_RANGE;

/// The suffix of LEAST and GREATEST forms that skip NULL arguments.
const IGNORE_NULLS_SUFFIX: &str = "_IGNORE_NULLS";

/// SEQ4() or SEQ8() in a query level reading only a generator: the row's number from 0.
fn generator_sequence(
name: &str,
Expand Down Expand Up @@ -327,14 +330,16 @@ pub(crate) fn convert_function(payload: &Value, site: Site<'_>) -> Result<Emitte
("TO_TIMESTAMP_TZ" | "TO_TIMESTAMP_LTZ" | "TRY_TO_TIMESTAMP_LTZ", [value]) => {
zoned_text_call(&name, value, &construct, site)
}
("LEAST" | "GREATEST", parts) if !parts.is_empty() => {
("LEAST" | "GREATEST" | "LEAST_IGNORE_NULLS" | "GREATEST_IGNORE_NULLS", parts)
if !parts.is_empty() =>
{
least_greatest(&name, parts, &construct, site)
}
_ => Err(missing(construct, format!("no rule converts fn:{name}"))),
}
}

/// LEAST or GREATEST, NULL when any argument is NULL, as in Snowflake.
/// LEAST or GREATEST, NULL when any argument is NULL; the _IGNORE_NULLS forms skip NULLs.
fn least_greatest(
name: &str,
arguments: &[Value],
Expand All @@ -350,19 +355,23 @@ fn least_greatest(
comparable(converted[0].clone(), other.clone(), construct, site.context)?;
}
let (branches, target) = unified(&converted, construct, site.context)?;
let (function, ignoring) = match name.strip_suffix(IGNORE_NULLS_SUFFIX) {
Some(function) => (function.to_ascii_lowercase(), true),
None => (name.to_ascii_lowercase(), false),
};
let call = format!("{function}({})", branches.join(", "));
let any_null: Vec<String> = branches
.iter()
.map(|branch| format!("{branch} IS NULL"))
.collect();
Ok(Emitted::new(
format!(
"(CASE WHEN {} THEN NULL ELSE {}({}) END)",
any_null.join(" OR "),
name.to_ascii_lowercase(),
branches.join(", ")
let sql = match ignoring {
true => call,
false => format!(
"(CASE WHEN {} THEN NULL ELSE {call} END)",
any_null.join(" OR ")
),
target,
))
};
Ok(Emitted::new(sql, target))
}

fn nullif(left: &Value, right: &Value, site: Site<'_>) -> Result<Emitted, Refusal> {
Expand Down
2 changes: 1 addition & 1 deletion sqlquack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ minimum_bytes = 4194304
# can return another value.

[divergences.numeric_range]
constructs = ["fn:SUM", "fn:AVG", "node:add", "node:sub", "node:mul", "node:div", "node:eq", "node:neq", "node:lt", "node:lte", "node:gt", "node:gte", "node:null_safe_eq", "node:null_safe_neq", "node:in", "fn:EQUAL_NULL", "fn:NULLIF", "node:union", "node:except", "node:intersect", "node:case", "fn:COALESCE", "node:coalesce", "fn:DECODE", "fn:GREATEST", "fn:LEAST", "node:nvl", "fn:NVL2", "fn:IFF", "fn:IFNULL", "fn:ZEROIFNULL", "fn:EXP"]
constructs = ["fn:SUM", "fn:AVG", "node:add", "node:sub", "node:mul", "node:div", "node:eq", "node:neq", "node:lt", "node:lte", "node:gt", "node:gte", "node:null_safe_eq", "node:null_safe_neq", "node:in", "fn:EQUAL_NULL", "fn:NULLIF", "node:union", "node:except", "node:intersect", "node:case", "fn:COALESCE", "node:coalesce", "fn:DECODE", "fn:GREATEST", "fn:LEAST", "fn:GREATEST_IGNORE_NULLS", "fn:LEAST_IGNORE_NULLS", "node:nvl", "fn:NVL2", "fn:IFF", "fn:IFNULL", "fn:ZEROIFNULL", "fn:EXP"]
default = "warn"
strict = "refuse"
failure = "loud"
Expand Down
Loading