walker: flag LIKE on a numeric column (ADR-0027 Amendment 1)
LIKE is a text-pattern match; against a numeric column (int, real, decimal, serial) it runs but is almost never intended. predicate_warnings now emits a WARNING for it, spanned at the target column. New Type::is_numeric; catalog key diagnostic.like_numeric; ADR-0027 gains "Amendment 1" and the adr/README index line is updated per the index-upkeep rule. bool and the text-/blob-backed types are deliberately not flagged — see the amendment for the rationale. 3 walker tests (int, decimal NOT LIKE, text-column clean). 1108 passing, clippy clean.
This commit is contained in:
+83
-3
@@ -526,14 +526,54 @@ fn predicate_warnings(
|
||||
}
|
||||
}
|
||||
}
|
||||
// `LIKE` is inherently a text-pattern test; flagging a
|
||||
// non-text target is a future model extension.
|
||||
// `LIKE` is a text-pattern test; against a numeric
|
||||
// column it runs but is almost never intended
|
||||
// (ADR-0027, Amendment 1). The negation is irrelevant —
|
||||
// `NOT LIKE` on a numeric column is just as dubious.
|
||||
Predicate::Like { target, .. } => {
|
||||
if let Some((message, span)) =
|
||||
like_numeric_warning(target, columns)
|
||||
{
|
||||
out.push(warn(message, span));
|
||||
}
|
||||
}
|
||||
// `IS [NOT] NULL` is the *correct* null test — never
|
||||
// flagged.
|
||||
Predicate::Like { .. } | Predicate::IsNull { .. } => {}
|
||||
Predicate::IsNull { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `LIKE` whose target is a numeric column: `LIKE` matches
|
||||
/// text patterns, so a numeric target is almost certainly a
|
||||
/// mistake (ADR-0027, Amendment 1). The message is paired with
|
||||
/// the target column operand's span. `None` when the target is
|
||||
/// a literal, an unknown column, or a non-numeric column.
|
||||
fn like_numeric_warning(
|
||||
target: &Operand,
|
||||
columns: &[crate::completion::TableColumn],
|
||||
) -> Option<(String, (usize, usize))> {
|
||||
let Operand::Column { name, span } = target else {
|
||||
return None;
|
||||
};
|
||||
let ty = columns
|
||||
.iter()
|
||||
.find(|tc| tc.name.eq_ignore_ascii_case(name))?
|
||||
.user_type;
|
||||
if !ty.is_numeric() {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
crate::friendly::translate(
|
||||
"diagnostic.like_numeric",
|
||||
&[
|
||||
("column", name as &dyn std::fmt::Display),
|
||||
("type", &ty.keyword() as &dyn std::fmt::Display),
|
||||
],
|
||||
),
|
||||
*span,
|
||||
))
|
||||
}
|
||||
|
||||
const fn is_null_literal(operand: &Operand) -> bool {
|
||||
matches!(
|
||||
operand,
|
||||
@@ -1816,6 +1856,46 @@ mod tests {
|
||||
assert_eq!(&input[s..e], "NoSuchCol");
|
||||
}
|
||||
|
||||
// ---- LIKE on a numeric column (ADR-0027, Amendment 1) -----
|
||||
|
||||
#[test]
|
||||
fn like_on_a_numeric_column_is_a_warning() {
|
||||
// `LIKE` is a text-pattern match — against an int
|
||||
// column it runs but is almost never intended.
|
||||
let schema = schema_with("Customers", &[("Age", Type::Int)]);
|
||||
let input = "delete from Customers where Age like '1%'";
|
||||
let diags = diagnostics(input, &schema);
|
||||
assert_eq!(diags.len(), 1);
|
||||
assert_eq!(diags[0].severity, super::Severity::Warning);
|
||||
let (s, e) = diags[0].span;
|
||||
assert_eq!(&input[s..e], "Age", "the span is the numeric column");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_like_on_a_numeric_column_is_also_a_warning() {
|
||||
let schema = schema_with("Orders", &[("Total", Type::Decimal)]);
|
||||
assert_eq!(
|
||||
super::input_verdict(
|
||||
"delete from Orders where Total not like '9%'",
|
||||
Some(&schema),
|
||||
),
|
||||
Some(super::Severity::Warning),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn like_on_a_text_column_is_clean() {
|
||||
// `LIKE 'A%'` on a text column is its intended use.
|
||||
let schema = schema_with("Customers", &[("Name", Type::Text)]);
|
||||
assert_eq!(
|
||||
super::input_verdict(
|
||||
"delete from Customers where Name like 'A%'",
|
||||
Some(&schema),
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_insert_with_explicit_column_list() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user