Skip to content
Merged
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
2 changes: 1 addition & 1 deletion crates/sqlquack-rules/src/catalogue/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,7 @@ pub const RULES: &[RuleSpec] = &[
RuleSpec {
id: INTERVAL_ARITHMETIC,
constructs: &["node:add", "node:sub", "node:interval"],
conditions: "A DATE or TIMESTAMP_NTZ plus or minus an INTERVAL literal of one whole number of days, hours, minutes or seconds, written '<n> <unit>' with the unit singular or plural in any case: DATEADD of n or -n of that unit. A DATE takes only days and stays a DATE; a TIMESTAMP_NTZ of any precision becomes a TIMESTAMP_NTZ(9), relying on timestamp_range. Other intervals and operands stay refused.",
conditions: "A DATE or TIMESTAMP_NTZ plus or minus an INTERVAL literal of one whole number of days, hours, minutes or seconds, written '<n> <unit>' with the unit singular or plural in any case: DATEADD of n or -n of that unit. A DATE takes only days: with the unit inside the quotes ('7 days') it stays a DATE, and with the unit after them (INTERVAL '3' DAY) it becomes a TIMESTAMP_NTZ(9) at midnight, as recorded, relying on timestamp_range; a TIMESTAMP_NTZ of any precision becomes a TIMESTAMP_NTZ(9), relying on timestamp_range. Other intervals and operands stay refused.",
failures: &[],
},
RuleSpec {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(crate) fn adds_interval(payload: &Value) -> bool {

/// `value + INTERVAL 'n unit'` or `value - INTERVAL 'n unit'`, as DATEADD of n or -n units.
pub(crate) fn interval_arithmetic(
kind: &str,
(kind, node): (&str, &Value),
payload: &Value,
site: Site<'_>,
) -> Result<Emitted, Refusal> {
Expand All @@ -41,6 +41,23 @@ pub(crate) fn interval_arithmetic(
let payload = payload_object(payload, &construct)?;
let value = convert_expression(field(payload, "left", &construct)?, site)?;
let interval = field(payload, "right", &construct)?;
let quoted_value = site
.context
.locator
.source_text(node)
.is_some_and(|written| {
let lower = written.to_ascii_lowercase();
lower.rfind(INTERVAL_KIND).is_some_and(|start| {
lower[start + INTERVAL_KIND.len()..]
.trim_start()
.starts_with('\'')
})
});
if !quoted_value {
return Err(refuse(
"an INTERVAL whose value is not a quoted string, which no recording shows".to_owned(),
));
}
let (amount, unit) =
interval_literal(interval, site)?.ok_or_else(|| refuse(INTERVAL_REFUSAL.to_owned()))?;
let accepted = match value.data_type {
Expand All @@ -54,8 +71,12 @@ pub(crate) fn interval_arithmetic(
value.data_type
)));
}
let unit_outside_quotes = node_kind(interval)
.and_then(|(_, payload)| payload.get("unit"))
.is_some_and(|unit| !is_inert(unit));
let result_type = match value.data_type {
SnowflakeType::TimestampNtz { .. } => {
SnowflakeType::Date if !unit_outside_quotes => SnowflakeType::Date,
SnowflakeType::Date | SnowflakeType::TimestampNtz { .. } => {
diverge(site.context, TIMESTAMP_RANGE, &construct)?;
SnowflakeType::TimestampNtz {
precision: INTERVAL_TIMESTAMP_PRECISION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ fn convert_kind(node: &Value, site: Site<'_>) -> Result<Emitted, Refusal> {
"annotated" => commented(payload, site),
"neg" => negate(payload, site),
"subquery" => scalar_subquery(payload, site),
"add" | "sub" if adds_interval(payload) => interval_arithmetic(kind, payload, site),
"add" | "sub" if adds_interval(payload) => interval_arithmetic((kind, node), payload, site),
"add" | "sub" | "mul" | "div" => arithmetic(kind, payload, site),
"mod" => modulo(payload, site),
"eq" | "neq" | "lt" | "lte" | "gt" | "gte" => comparison_predicate(kind, payload, site),
Expand Down
15 changes: 15 additions & 0 deletions crates/sqlquack/src/analysis/tests/test_coverage_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,26 @@ fn given_interval_literals_when_converting_then_dates_and_timestamps_move_by_the
sql: "SELECT ('2024-03-05'::DATE - INTERVAL '7 days')::VARCHAR AS v",
expected_outcome: Ok("2024-02-27"),
},
FirstValueTestCase {
description: "a DATE plus days written after the quotes is a TIMESTAMP_NTZ at midnight",
sql: "SELECT ('2026-04-01'::DATE + INTERVAL '3' DAYS)::VARCHAR AS v",
expected_outcome: Ok("2026-04-04 00:00:00.000"),
},
FirstValueTestCase {
description: "a DATE minus one day written after the quotes is a TIMESTAMP_NTZ too",
sql: "SELECT ('2024-03-01'::DATE - INTERVAL '1' DAY)::VARCHAR AS v",
expected_outcome: Ok("2024-02-29 00:00:00.000"),
},
FirstValueTestCase {
description: "a timestamp plus hours crosses midnight",
sql: "SELECT ('2024-02-28 23:30:00'::TIMESTAMP_NTZ + INTERVAL '2 hours')::VARCHAR AS v",
expected_outcome: Ok("2024-02-29 01:30:00.000"),
},
FirstValueTestCase {
description: "an interval of an unquoted number is refused",
sql: "SELECT '2024-03-05'::DATE + INTERVAL 3 DAY AS v",
expected_outcome: Err("not a quoted string"),
},
FirstValueTestCase {
description: "a DATE plus hours stays refused",
sql: "SELECT '2024-03-05'::DATE + INTERVAL '2 hours' AS v",
Expand Down
Loading