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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 18 additions & 6 deletions src/canonicalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use sqlparser::ast::{
AccessExpr, BinaryOperator, CastKind, CeilFloorKind, DateTimeField, Distinct, Expr,
ExtractSyntax, Function, FunctionArg, FunctionArgExpr, FunctionArguments, GroupByExpr, Ident,
InTarget, Interval, LimitClause, ObjectName, Query, Select, SelectItem, SelectModifiers,
SetExpr, Statement, Subscript, TableAlias, TableFactor, UnaryOperator, Value,
SetExpr, Statement, Subscript, TableAlias, TableFactor, TableInheritance, UnaryOperator, Value,
};
use sqlparser::dialect::{AnsiDialect, Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
use sqlparser::keywords::ALL_KEYWORDS;
Expand Down Expand Up @@ -1054,7 +1054,9 @@ fn refused(form: &str) -> CanonicalizeError {
/// Inside a subquery, grouping, ordering and column aliases change which rows the outer
/// predicate sees, so they are refused along with every clause an outer query may not carry.
/// A table alias only names the table for the qualifiers that follow, so it is kept, folded as
/// a qualifier and written without `AS`, which Oracle refuses there.
/// a qualifier and written without `AS`, which Oracle refuses there. `ONLY` keeps descendant
/// tables out of the rows read, so it is kept, while `u *` reads what a bare `u` reads and is
/// written as `u`.
fn subquery_text(
query: &Query,
depth: usize,
Expand Down Expand Up @@ -1085,6 +1087,7 @@ fn subquery_text(
json_path: None,
sample: None,
index_directives,
inheritance,
} = &select.from[0].relation
else {
return Err(unsupported("a table option"));
Expand Down Expand Up @@ -1122,7 +1125,11 @@ fn subquery_text(
})
.collect::<Result<Vec<_>, _>>()?
.join(".");
let mut text = format!("SELECT {items} FROM {table}");
let only = match inheritance {
Some(TableInheritance::Only { parenthesized: _ }) => "ONLY ",
Some(TableInheritance::IncludeDescendants) | None => "",
};
let mut text = format!("SELECT {items} FROM {only}{table}");
if let Some(TableAlias {
explicit: _,
name,
Expand Down Expand Up @@ -1788,9 +1795,11 @@ fn collect_flat_children<'a>(expr: &'a Expr, operator: &BinaryOperator) -> Vec<&
/// `AND` and `OR` never reach here, because their chains are flattened and sorted whole.
const fn operand_order(operator: &BinaryOperator, context: &Canonicalizer<'_>) -> OperandOrder {
match operator {
BinaryOperator::Eq | BinaryOperator::NotEq | BinaryOperator::Spaceship => {
OperandOrder::Compared
}
BinaryOperator::Eq
| BinaryOperator::NotEq
| BinaryOperator::Spaceship
| BinaryOperator::Is
| BinaryOperator::IsNot => OperandOrder::Compared,
// SQL Server also concatenates strings with `+`, where the order matters.
BinaryOperator::Plus | BinaryOperator::Multiply if context.plus_is_numeric => {
OperandOrder::Sorted
Expand Down Expand Up @@ -1819,6 +1828,9 @@ fn operator_text(operator: &BinaryOperator) -> Result<&'static str, Canonicalize
BinaryOperator::Multiply => Ok("*"),
BinaryOperator::Divide => Ok("/"),
BinaryOperator::Modulo => Ok("%"),
// SQLite also spells `IS NOT DISTINCT FROM` as `IS`, and `IS DISTINCT FROM` as `IS NOT`.
BinaryOperator::Is => Ok("IS NOT DISTINCT FROM"),
BinaryOperator::IsNot => Ok("IS DISTINCT FROM"),
other => Err(CanonicalizeError::Unsupported(format!(
"Unsupported binary operator: {other}"
))),
Expand Down
2 changes: 1 addition & 1 deletion tests/allocator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ fn fixed_corpus_allocation_count_does_not_grow() {
let counts = std::array::from_fn::<_, 16, _>(|_| measure(sql));
// Two passes over the predicate, the original statement parse and the expression
// re-read that proves the canonical text reads as itself.
assert_eq!(counts, [400; 16]);
assert_eq!(counts, [386; 16]);
}
4 changes: 2 additions & 2 deletions tests/goldens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,8 +856,8 @@ const GOLDENS: &[Golden] = &[
Golden {
dialect: DialectKind::SQLite,
sql: "SELECT * FROM t WHERE Flag IS UNKNOWN",
normalized: "flag IS UNKNOWN",
hash: 268016679043104776737949376478094949884,
normalized: "(flag IS NOT DISTINCT FROM unknown)",
hash: 146294889612496539165456862934178885956,
},
Golden {
dialect: DialectKind::PostgreSql,
Expand Down
86 changes: 65 additions & 21 deletions tests/meaning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,29 @@ enum Tree {
Substring(Box<Tree>, Box<Tree>, Box<Tree>),
Position(Box<Tree>, Box<Tree>),
Tuple(Box<Tree>, Box<Tree>),
/// Subject, filter, negation and table alias of `x [NOT] IN (SELECT a FROM u [v] [WHERE
/// Subject, filter, negation and table of `x [NOT] IN (SELECT a FROM table [WHERE
/// filter])`.
InQuery(Box<Tree>, Option<Box<Tree>>, bool, bool),
/// Filter, negation and table alias of `[NOT] EXISTS (SELECT * FROM u [v] [WHERE filter])`.
Exists(Option<Box<Tree>>, bool, bool),
/// Filter and table alias of the scalar subquery `(SELECT b FROM u [v] [WHERE filter])`.
Scalar(Option<Box<Tree>>, bool),
InQuery(Box<Tree>, Option<Box<Tree>>, bool, Table),
/// Filter, negation and table of `[NOT] EXISTS (SELECT * FROM table [WHERE filter])`.
Exists(Option<Box<Tree>>, bool, Table),
/// Filter and table of the scalar subquery `(SELECT b FROM table [WHERE filter])`.
Scalar(Option<Box<Tree>>, Table),
}

/// The table a subquery reads, `[ONLY] u [v]`.
#[derive(Clone, Copy)]
struct Table {
only: bool,
aliased: bool,
}

impl Table {
fn random(rng: &mut Rng) -> Self {
Self {
only: rng.percent(20),
aliased: rng.percent(30),
}
}
}

const BINARY: &[&str] = &[
Expand All @@ -83,11 +99,20 @@ const BINARY: &[&str] = &[
"<=>",
"IS DISTINCT FROM",
"IS NOT DISTINCT FROM",
"IS",
"IS NOT",
];
/// Comparisons whose operands swap freely where the collation of two operands does not depend
/// on their order.
const SYMMETRIC_COMPARISON: &[&str] =
&["=", "!=", "<=>", "IS DISTINCT FROM", "IS NOT DISTINCT FROM"];
const SYMMETRIC_COMPARISON: &[&str] = &[
"=",
"!=",
"<=>",
"IS DISTINCT FROM",
"IS NOT DISTINCT FROM",
"IS",
"IS NOT",
];
const POSTFIX: &[&str] = &[
"IS NULL",
"IS NOT NULL",
Expand Down Expand Up @@ -157,15 +182,15 @@ fn tree(rng: &mut Rng, depth: u32, quoted: &'static str) -> Tree {
19 => Tree::Tuple(child(rng), child(rng)),
20 => {
let filter = rng.percent(70).then(|| child(rng));
Tree::InQuery(child(rng), filter, rng.percent(50), rng.percent(30))
Tree::InQuery(child(rng), filter, rng.percent(50), Table::random(rng))
}
21 => {
let filter = rng.percent(70).then(|| child(rng));
Tree::Exists(filter, rng.percent(50), rng.percent(30))
Tree::Exists(filter, rng.percent(50), Table::random(rng))
}
_ => {
let filter = rng.percent(70).then(|| child(rng));
Tree::Scalar(filter, rng.percent(30))
Tree::Scalar(filter, Table::random(rng))
}
}
}
Expand All @@ -189,6 +214,9 @@ struct Spelling {
/// Spells a missing subquery filter as `WHERE TRUE` at times, which is the same filter only
/// where `TRUE` is reserved.
true_filter_synonym: bool,
/// Writes `u *` for `u` and `ONLY (u)` for `ONLY u` at times, which only dialects that
/// read inheritance markers treat as the same table.
inheritance_synonyms: bool,
/// Writes `b > a` for `a < b` at times, which is one comparison where the collation of two
/// operands does not depend on their order.
mirror_comparisons: bool,
Expand Down Expand Up @@ -362,38 +390,53 @@ fn spell(tree: &Tree, spelling: Spelling, rng: &mut Rng) -> String {
Tree::Tuple(first, second) => {
format!("({}, {})", operand(first, rng), operand(second, rng))
}
Tree::InQuery(subject, filter, negated, aliased) => {
Tree::InQuery(subject, filter, negated, table) => {
let subject = operand(subject, rng);
let not = if *negated { "NOT " } else { "" };
let not = keyword(not, spelling, rng);
let query = subquery("a", filter.as_deref(), *aliased, spelling, rng);
let query = subquery("a", filter.as_deref(), *table, spelling, rng);
format!("{subject} {not}IN ({query})")
}
Tree::Exists(filter, negated, aliased) => {
Tree::Exists(filter, negated, table) => {
let not = if *negated { "NOT " } else { "" };
let exists = keyword(&format!("{not}EXISTS"), spelling, rng);
format!(
"{exists} ({})",
subquery("*", filter.as_deref(), *aliased, spelling, rng)
subquery("*", filter.as_deref(), *table, spelling, rng)
)
}
Tree::Scalar(filter, aliased) => format!(
Tree::Scalar(filter, table) => format!(
"({})",
subquery("b", filter.as_deref(), *aliased, spelling, rng)
subquery("b", filter.as_deref(), *table, spelling, rng)
),
}
}

fn subquery(
projection: &str,
filter: Option<&Tree>,
aliased: bool,
table: Table,
spelling: Spelling,
rng: &mut Rng,
) -> String {
let flip_table_case = |rng: &mut Rng| spelling.flip_table_case && rng.percent(50);
let table = if flip_table_case(rng) { "U" } else { "u" };
let alias = if aliased {
let name = if flip_table_case(rng) { "U" } else { "u" };
let synonym = |rng: &mut Rng| spelling.inheritance_synonyms && rng.percent(50);
// Where the dialect reads no inheritance markers, `ONLY` is a table name and keeps its case.
let only = |rng: &mut Rng| {
if spelling.inheritance_synonyms {
keyword("ONLY", spelling, rng)
} else {
"ONLY".to_string()
}
};
let name = match (table.only, synonym(rng)) {
(true, true) => format!("{} ({name})", only(rng)),
(true, false) => format!("{} {name}", only(rng)),
(false, true) => format!("{name} *"),
(false, false) => name.to_string(),
};
let alias = if table.aliased {
let name = if flip_table_case(rng) { "V" } else { "v" };
// `AS` before a table alias is optional, so writing it is a spelling.
if spelling.flip_keyword_case && rng.percent(50) {
Expand All @@ -417,7 +460,7 @@ fn subquery(
}
None => String::new(),
};
format!("{select} {projection} {from} {table}{alias}{filter}")
format!("{select} {projection} {from} {name}{alias}{filter}")
}

fn parse(dialect: &dyn Dialect, predicate: &str) -> Option<Expr> {
Expand Down Expand Up @@ -488,6 +531,7 @@ fn equivalent_spellings_agree() {
double_colon_cast: dialect.is::<PostgreSqlDialect>(),
flip_table_case: !dialect.is::<MySqlDialect>() && !dialect.is::<GenericDialect>(),
true_filter_synonym: Folding::of(dialect).true_is_reserved,
inheritance_synonyms: dialect.supports_table_inheritance_markers(),
mirror_comparisons: Folding::of(dialect).collation_is_symmetric,
swap_numeric_operands: Folding::of(dialect).plus_is_numeric,
};
Expand Down
35 changes: 27 additions & 8 deletions tests/oracle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
//! Two expressions with the same meaning text are the same predicate under every equivalence
//! the canonicalizer promises. Those are redundant parentheses, the dialect's name folding, the
//! order of operands and items it treats as unordered, `a > b` for `b < a`, and synonyms such
//! as `SOME` for `ANY`, `x::T` for `CAST(x AS T)`, a missing `ELSE` for `ELSE NULL`, a table
//! alias with or without `AS`, and a subquery without `WHERE` for one filtered by `TRUE` where
//! as `SOME` for `ANY`, `x::T` for `CAST(x AS T)`, SQLite's `a IS b` for `a IS NOT DISTINCT
//! FROM b`, a missing `ELSE` for `ELSE NULL`, a table alias with or without `AS`, a subquery
//! table with or without `*`, and a subquery without `WHERE` for one filtered by `TRUE` where
//! `TRUE` is reserved. Anything else keeps its structure.

use sqlparser::ast::{
AccessExpr, BinaryOperator, CastKind, CeilFloorKind, Expr, FunctionArg, FunctionArgExpr,
FunctionArguments, GroupByExpr, Ident, InTarget, ObjectName, Query, SelectItem, SetExpr,
Subscript, TableFactor, UnaryOperator, Value,
Subscript, TableFactor, TableInheritance, UnaryOperator, Value,
};
use sqlparser::dialect::{AnsiDialect, Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};

Expand Down Expand Up @@ -151,15 +152,25 @@ pub fn meaning(expr: &Expr, folding: Folding) -> String {
let (left, right) = unordered(left, right, folding);
format!("({op} {left} {right})")
}
Expr::BinaryOp { left, op, right } => format!("({op} {} {})", m(left), m(right)),
Expr::IsDistinctFrom(left, right) => {
Expr::IsDistinctFrom(left, right)
| Expr::BinaryOp {
left,
op: BinaryOperator::IsNot,
right,
} => {
let (left, right) = compared(left, right, folding);
format!("(distinct {left} {right})")
}
Expr::IsNotDistinctFrom(left, right) => {
Expr::IsNotDistinctFrom(left, right)
| Expr::BinaryOp {
left,
op: BinaryOperator::Is,
right,
} => {
let (left, right) = compared(left, right, folding);
format!("(not-distinct {left} {right})")
}
Expr::BinaryOp { left, op, right } => format!("({op} {} {})", m(left), m(right)),
Expr::UnaryOp { op, expr } => match (op, strip_nested(expr)) {
// `NOT (EXISTS q)` and `NOT EXISTS q` are one test.
(
Expand Down Expand Up @@ -554,9 +565,17 @@ fn query_meaning(query: &Query, folding: Folding) -> String {
if !simple {
return query.to_string();
}
let TableFactor::Table { name, alias, .. } = &select.from[0].relation else {
let TableFactor::Table {
name,
alias,
inheritance,
..
} = &select.from[0].relation
else {
return query.to_string();
};
// `ONLY` leaves out descendant tables, which a bare name and `name *` both read.
let only = matches!(inheritance, Some(TableInheritance::Only { .. }));
// A table alias only names the table, so its meaning is its folded name.
let alias = match alias {
None => None,
Expand Down Expand Up @@ -590,7 +609,7 @@ fn query_meaning(query: &Query, folding: Folding) -> String {
.filter(|filter| !(folding.true_is_reserved && is_true(filter)))
.map(|filter| meaning(filter, folding));
format!(
"(select [{}] {} {alias:?} {filter:?})",
"(select [{}] {only} {} {alias:?} {filter:?})",
items.join(" "),
table.join(".")
)
Expand Down
39 changes: 39 additions & 0 deletions tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,27 @@ fn a_subquery_table_alias_is_kept_folded_as_a_qualifier() {
);
}

#[test]
fn a_subquery_table_keeps_only_and_drops_the_descendants_star() {
for dialect in [&PostgreSqlDialect {} as &dyn Dialect, &GenericDialect {}] {
assert_canonical(
dialect,
&[
("x IN (SELECT a FROM ONLY u)", "x IN (SELECT a FROM ONLY u)"),
(
"x IN (SELECT a FROM ONLY (u) AS v WHERE v.a = 1)",
"x IN (SELECT a FROM ONLY u v WHERE (1 = v.a))",
),
("x IN (SELECT a FROM u *)", "x IN (SELECT a FROM u)"),
(
"EXISTS (SELECT 1 FROM s.u * v WHERE v.a = 1)",
"EXISTS (SELECT 1 FROM s.u v WHERE (1 = v.a))",
),
],
);
}
}

#[test]
fn a_list_never_opens_with_an_item_read_as_a_query() {
assert_canonical(
Expand Down Expand Up @@ -1754,6 +1775,24 @@ fn equality_between_two_columns_keeps_its_order_where_the_left_collation_wins()
assert_canonical(&PostgreSqlDialect {}, &[("b = a", "(a = b)")]);
}

#[test]
fn sqlite_is_and_is_not_share_the_distinct_from_spellings() {
assert_canonical(
&SQLiteDialect {},
&[
("b IS a", "(b IS NOT DISTINCT FROM a)"),
("b IS NOT DISTINCT FROM a", "(b IS NOT DISTINCT FROM a)"),
("b IS NOT a", "(b IS DISTINCT FROM a)"),
("a IS 1", "(1 IS NOT DISTINCT FROM a)"),
("1 IS NOT a", "(1 IS DISTINCT FROM a)"),
("a IS b + 1", "(a IS NOT DISTINCT FROM (1 + b))"),
("(a IS b) = c", "((a IS NOT DISTINCT FROM b) = c)"),
// `UNKNOWN` is no SQLite keyword, so this compares with a column.
("flag IS UNKNOWN", "(flag IS NOT DISTINCT FROM unknown)"),
],
);
}

#[test]
fn a_boolean_is_no_literal_where_true_may_name_a_column() {
// SQLite reads `TRUE` as a column named `true` when the table has one, and that column
Expand Down