Skip to main content

sqlparser_canonicalize/
canonicalize.rs

1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec;
4use alloc::vec::Vec;
5use core::hash::{Hash, Hasher};
6
7use seahash::SeaHasher;
8use sqlparser::ast::{
9    BinaryOperator, Distinct, Expr, Ident, LimitClause, Query, Select, SelectModifiers, SetExpr,
10    Statement, TableFactor, Value,
11};
12use sqlparser::dialect::{AnsiDialect, Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
13use sqlparser::keywords::ALL_KEYWORDS;
14use sqlparser::parser::Parser;
15
16use crate::CanonicalizeError;
17
18const MAX_EXPR_DEPTH: usize = 128;
19const MAX_SQL_LEN: usize = 8192;
20/// Canonical text for a `SELECT` with no `WHERE` clause.
21const NO_FILTER: &str = "TRUE";
22
23impl<'a> Canonicalizer<'a> {
24    /// Parses one `SELECT` and returns canonical text for its `WHERE` clause.
25    ///
26    /// The canonical text is verified to survive a parse of itself, so a predicate whose
27    /// canonical form would read back as something else is rejected instead of hashed.
28    pub fn normalize_sql(&self, sql: &str) -> Result<String, CanonicalizeError> {
29        let canonical = normalize_sql_inner(sql, self)?;
30        confirm_reads_back_as_itself(canonical, self)
31    }
32
33    /// Canonicalizes a parsed `WHERE` clause in O(n log n) time for boolean chains and O(n)
34    /// otherwise.
35    ///
36    /// `where_expr` MUST have been parsed with this canonicalizer's dialect, which decides
37    /// how names fold and which reads the canonical text back to check it.
38    pub fn normalize_where_clause(
39        &self,
40        where_expr: Option<&Expr>,
41    ) -> Result<String, CanonicalizeError> {
42        let canonical = normalize_where_clause_inner(where_expr, self)?;
43        confirm_reads_back_as_itself(canonical, self)
44    }
45}
46
47fn normalize_sql_inner(
48    sql: &str,
49    context: &Canonicalizer<'_>,
50) -> Result<String, CanonicalizeError> {
51    if sql.len() > MAX_SQL_LEN {
52        return Err(CanonicalizeError::InputTooLong { limit: MAX_SQL_LEN });
53    }
54    check_sql_sanity(sql)?;
55
56    let statements = Parser::parse_sql(context.dialect, sql)
57        .map_err(|error| CanonicalizeError::Parse(error.to_string()))?;
58    let [statement] = statements.as_slice() else {
59        return Err(CanonicalizeError::Unsupported(
60            "Expected exactly one SELECT statement".to_string(),
61        ));
62    };
63    let where_expr = extract_where(statement)?;
64    normalize_where_clause_inner(where_expr, context)
65}
66
67fn normalize_where_clause_inner(
68    where_expr: Option<&Expr>,
69    context: &Canonicalizer<'_>,
70) -> Result<String, CanonicalizeError> {
71    where_expr.map_or_else(
72        || Ok(NO_FILTER.to_string()),
73        |expr| normalize_expr_inner(expr, 0, false, context),
74    )
75}
76
77/// Returns the canonical text only if a second pass reproduces it byte for byte.
78///
79/// Canonical text is a hash key, so text that reads back as a different predicate would
80/// give two distinct predicates one hash.
81fn confirm_reads_back_as_itself(
82    canonical: String,
83    context: &Canonicalizer<'_>,
84) -> Result<String, CanonicalizeError> {
85    let replay = if canonical == NO_FILTER {
86        "SELECT * FROM t".to_string()
87    } else {
88        format!("SELECT * FROM t WHERE {canonical}")
89    };
90    match normalize_sql_inner(&replay, context) {
91        Ok(again) if again == canonical => Ok(canonical),
92        _ => Err(CanonicalizeError::NotRoundTrippable(canonical)),
93    }
94}
95
96/// Returns the stable 128-bit SeaHash value for canonical text.
97#[must_use]
98pub fn hash_canonical(normalized: &str) -> u128 {
99    let mut first = SeaHasher::new();
100    normalized.hash(&mut first);
101    let first = first.finish();
102
103    let mut second = SeaHasher::with_seeds(
104        first,
105        first.wrapping_add(1),
106        first.wrapping_add(2),
107        first.wrapping_add(3),
108    );
109    normalized.hash(&mut second);
110    (u128::from(first) << 64) | u128::from(second.finish())
111}
112
113fn check_sql_sanity(sql: &str) -> Result<(), CanonicalizeError> {
114    let mut paren_depth = 0usize;
115    let mut bracket_depth = 0usize;
116    let mut consecutive_ops = 0usize;
117
118    for byte in sql.bytes() {
119        match byte {
120            b'(' => {
121                paren_depth += 1;
122                consecutive_ops += 1;
123            }
124            b')' => {
125                paren_depth = paren_depth.saturating_sub(1);
126                consecutive_ops = 0;
127            }
128            b'[' => {
129                bracket_depth += 1;
130                consecutive_ops = 0;
131            }
132            b']' => {
133                bracket_depth = bracket_depth.saturating_sub(1);
134                consecutive_ops = 0;
135            }
136            b'+' | b'-' | b'*' | b'/' | b'=' | b'<' | b'>' | b'!' | b'~' => {
137                consecutive_ops += 1;
138            }
139            b' ' | b'\t' | b'\n' | b'\r' => {}
140            0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F => {
141                return Err(CanonicalizeError::Unsupported(
142                    "Control character in SQL".to_string(),
143                ));
144            }
145            _ => consecutive_ops = 0,
146        }
147
148        if paren_depth > MAX_EXPR_DEPTH
149            || bracket_depth > MAX_EXPR_DEPTH
150            || consecutive_ops > MAX_EXPR_DEPTH
151        {
152            return Err(CanonicalizeError::TooDeep {
153                limit: MAX_EXPR_DEPTH,
154            });
155        }
156    }
157
158    if paren_depth != 0 {
159        return Err(CanonicalizeError::Unsupported(
160            "Unbalanced parentheses".to_string(),
161        ));
162    }
163    if bracket_depth != 0 {
164        return Err(CanonicalizeError::Unsupported(
165            "Unbalanced square brackets".to_string(),
166        ));
167    }
168    Ok(())
169}
170
171fn extract_where(statement: &Statement) -> Result<Option<&Expr>, CanonicalizeError> {
172    let Statement::Query(query) = statement else {
173        return Err(CanonicalizeError::Unsupported(
174            "Only SELECT statements are supported".to_string(),
175        ));
176    };
177    Ok(single_table_select(query)?.selection.as_ref())
178}
179
180fn single_table_select(query: &Query) -> Result<&Select, CanonicalizeError> {
181    let SetExpr::Select(select) = query.body.as_ref() else {
182        return Err(CanonicalizeError::Unsupported(
183            "Set operations are not supported".to_string(),
184        ));
185    };
186    if select.from.len() != 1 {
187        return Err(CanonicalizeError::Unsupported(
188            "Exactly one table is required".to_string(),
189        ));
190    }
191    if !select.from[0].joins.is_empty() {
192        return Err(CanonicalizeError::Unsupported(
193            "JOINs not supported".to_string(),
194        ));
195    }
196    if !matches!(select.from[0].relation, TableFactor::Table { .. }) {
197        return Err(CanonicalizeError::Unsupported(
198            "Subqueries and derived tables not supported".to_string(),
199        ));
200    }
201    check_served_clauses(query, select)?;
202    Ok(select)
203}
204
205fn check_served_clauses(query: &Query, select: &Select) -> Result<(), CanonicalizeError> {
206    let Query {
207        with,
208        body: _,
209        order_by: _,
210        limit_clause,
211        fetch,
212        locks,
213        for_clause,
214        settings,
215        format_clause,
216        pipe_operators,
217    } = query;
218    let Select {
219        select_token: _,
220        optimizer_hints,
221        distinct,
222        select_modifiers,
223        top,
224        top_before_distinct: _,
225        projection: _,
226        exclude,
227        into,
228        from: _,
229        lateral_views,
230        prewhere,
231        selection: _,
232        connect_by,
233        group_by: _,
234        cluster_by,
235        distribute_by,
236        sort_by,
237        having: _,
238        named_window,
239        qualify,
240        window_before_qualify: _,
241        value_table_mode,
242        flavor: _,
243    } = select;
244
245    let deduplicating = !matches!(distinct, None | Some(Distinct::All));
246    for (present, clause) in [
247        (with.is_some(), "WITH"),
248        (deduplicating, "DISTINCT"),
249        (
250            limit_clause.is_some(),
251            limit_clause_name(limit_clause.as_ref()),
252        ),
253        (fetch.is_some(), "FETCH"),
254        (!locks.is_empty(), "FOR UPDATE or FOR SHARE"),
255        (for_clause.is_some(), "FOR XML or FOR JSON"),
256        (settings.is_some(), "SETTINGS"),
257        (format_clause.is_some(), "FORMAT"),
258        (!pipe_operators.is_empty(), "pipe operators"),
259        (!optimizer_hints.is_empty(), "optimizer hints"),
260        (
261            select_modifiers
262                .as_ref()
263                .is_some_and(SelectModifiers::is_any_set),
264            "SELECT modifiers",
265        ),
266        (top.is_some(), "TOP"),
267        (exclude.is_some(), "EXCLUDE"),
268        (into.is_some(), "INTO"),
269        (!lateral_views.is_empty(), "LATERAL VIEW"),
270        (prewhere.is_some(), "PREWHERE"),
271        (!connect_by.is_empty(), "CONNECT BY"),
272        (!cluster_by.is_empty(), "CLUSTER BY"),
273        (!distribute_by.is_empty(), "DISTRIBUTE BY"),
274        (!sort_by.is_empty(), "SORT BY"),
275        (!named_window.is_empty(), "WINDOW"),
276        (qualify.is_some(), "QUALIFY"),
277        (
278            value_table_mode.is_some(),
279            "SELECT AS VALUE or SELECT AS STRUCT",
280        ),
281    ] {
282        if present {
283            return Err(CanonicalizeError::Unsupported(format!(
284                "{clause} is not supported"
285            )));
286        }
287    }
288    Ok(())
289}
290
291const fn limit_clause_name(limit: Option<&LimitClause>) -> &'static str {
292    match limit {
293        Some(LimitClause::LimitOffset {
294            limit: None,
295            offset,
296            limit_by,
297        }) => {
298            if offset.is_some() {
299                "OFFSET"
300            } else if limit_by.is_empty() {
301                "LIMIT"
302            } else {
303                "LIMIT BY"
304            }
305        }
306        None | Some(LimitClause::LimitOffset { .. } | LimitClause::OffsetCommaLimit { .. }) => {
307            "LIMIT"
308        }
309    }
310}
311
312fn normalize_expr_inner(
313    expr: &Expr,
314    depth: usize,
315    tight_parent: bool,
316    context: &Canonicalizer<'_>,
317) -> Result<String, CanonicalizeError> {
318    if depth > MAX_EXPR_DEPTH {
319        return Err(CanonicalizeError::TooDeep {
320            limit: MAX_EXPR_DEPTH,
321        });
322    }
323
324    Ok(match expr {
325        Expr::BinaryOp { left, op, right } => {
326            if matches!(op, BinaryOperator::And | BinaryOperator::Or) {
327                let mut children = collect_flat_children(left, op);
328                children.extend(collect_flat_children(right, op));
329                let mut child_text: Vec<String> = children
330                    .iter()
331                    .map(|child| normalize_expr_inner(child, depth + 1, false, context))
332                    .collect::<Result<_, _>>()?;
333                child_text.sort();
334                let operator = operator_text(op)?;
335                child_text
336                    .into_iter()
337                    .reduce(|left, right| format!("({left} {operator} {right})"))
338                    .unwrap_or_default()
339            } else {
340                let left = normalize_expr_inner(left, depth + 1, true, context)?;
341                let right = normalize_expr_inner(right, depth + 1, true, context)?;
342                let (left, right) = if is_commutative(op) && left > right {
343                    (right, left)
344                } else {
345                    (left, right)
346                };
347                format!("({left} {} {right})", operator_text(op)?)
348            }
349        }
350        Expr::UnaryOp { op, expr } => {
351            let operator = unary_operator_text(op)?;
352            let operand = normalize_expr_inner(expr, depth + 1, true, context)?;
353            // NOT binds looser than any operator that can enclose it, so a nested NOT
354            // reparses against the wrong operand without parentheses.
355            if tight_parent && matches!(op, sqlparser::ast::UnaryOperator::Not) {
356                format!("({operator} {operand})")
357            } else {
358                format!("{operator} {operand}")
359            }
360        }
361        Expr::IsNull(expr) => format!(
362            "{} IS NULL",
363            normalize_expr_inner(expr, depth + 1, true, context)?
364        ),
365        Expr::IsNotNull(expr) => {
366            format!(
367                "{} IS NOT NULL",
368                normalize_expr_inner(expr, depth + 1, true, context)?
369            )
370        }
371        Expr::InList {
372            expr,
373            list,
374            negated,
375        } => {
376            let mut items: Vec<String> = list
377                .iter()
378                .map(|item| normalize_expr_inner(item, depth + 1, true, context))
379                .collect::<Result<_, _>>()?;
380            items.sort();
381            let not = if *negated { "NOT " } else { "" };
382            format!(
383                "{} {not}IN ({})",
384                normalize_expr_inner(expr, depth + 1, true, context)?,
385                items.join(", ")
386            )
387        }
388        Expr::InSubquery {
389            expr,
390            subquery,
391            negated,
392        } => {
393            let not = if *negated { "NOT " } else { "" };
394            format!(
395                "{} {not}IN ({subquery})",
396                normalize_expr_inner(expr, depth + 1, true, context)?
397            )
398        }
399        Expr::Between {
400            expr,
401            low,
402            high,
403            negated,
404        } => {
405            let not = if *negated { "NOT " } else { "" };
406            format!(
407                "{} {not}BETWEEN {} AND {}",
408                normalize_expr_inner(expr, depth + 1, true, context)?,
409                normalize_expr_inner(low, depth + 1, true, context)?,
410                normalize_expr_inner(high, depth + 1, true, context)?
411            )
412        }
413        Expr::Like {
414            expr,
415            pattern,
416            negated,
417            escape_char,
418            ..
419        } => {
420            let not = if *negated { "NOT " } else { "" };
421            let escape = escape_char
422                .as_ref()
423                .map_or_else(String::new, |value| format!(" ESCAPE {value}"));
424            format!(
425                "{} {not}LIKE {}{escape}",
426                normalize_expr_inner(expr, depth + 1, true, context)?,
427                normalize_expr_inner(pattern, depth + 1, true, context)?
428            )
429        }
430        Expr::ILike {
431            expr,
432            pattern,
433            negated,
434            escape_char,
435            ..
436        } => {
437            let not = if *negated { "NOT " } else { "" };
438            let escape = escape_char
439                .as_ref()
440                .map_or_else(String::new, |value| format!(" ESCAPE {value}"));
441            format!(
442                "{} {not}ILIKE {}{escape}",
443                normalize_expr_inner(expr, depth + 1, true, context)?,
444                normalize_expr_inner(pattern, depth + 1, true, context)?
445            )
446        }
447        Expr::Nested(inner) => normalize_expr_inner(inner, depth + 1, tight_parent, context)?,
448        Expr::Identifier(identifier) => identifier_text(identifier, context)?,
449        Expr::CompoundIdentifier(parts) => parts
450            .iter()
451            .map(|part| identifier_text(part, context))
452            .collect::<Result<Vec<_>, _>>()?
453            .join("."),
454        Expr::Value(value) => {
455            reject_lossy_quoting(&value.value)?;
456            format!("{}", value.value)
457        }
458        Expr::Function(function) => format!("{function}"),
459        _ => format!("{expr}"),
460    })
461}
462
463/// How a dialect decides whether two spellings of a name are the same column.
464#[derive(Clone, Copy, Eq, PartialEq)]
465enum Folding {
466    /// An unquoted name folds to lower case, a quoted one keeps its spelling.
467    LowerUnquoted,
468    /// An unquoted name folds to upper case, a quoted one keeps its spelling.
469    UpperUnquoted,
470    /// Every spelling of the same letters is one name.
471    CaseInsensitive,
472    /// Unknown rule, so keep the name exactly as written and never merge two spellings.
473    Exact,
474}
475
476/// Canonicalizes predicates under one SQL dialect.
477///
478/// The dialect decides how identifiers fold, so it decides which two spellings mean one
479/// column. Build this from the dialect that parsed whatever you hand it.
480pub struct Canonicalizer<'a> {
481    dialect: &'a dyn Dialect,
482    folding: Folding,
483}
484
485impl<'a> Canonicalizer<'a> {
486    /// Reads the folding rule for `dialect`.
487    #[must_use]
488    pub fn new(dialect: &'a dyn Dialect) -> Self {
489        let folding = if dialect.is::<PostgreSqlDialect>() {
490            Folding::LowerUnquoted
491        } else if dialect.is::<AnsiDialect>() {
492            Folding::UpperUnquoted
493        } else if dialect.is::<MySqlDialect>() || dialect.is::<SQLiteDialect>() {
494            Folding::CaseInsensitive
495        } else {
496            Folding::Exact
497        };
498        Self { dialect, folding }
499    }
500}
501
502/// Resolves an identifier to the name the database would see, then spells it the one way
503/// that reads back as that same name.
504fn identifier_text(
505    identifier: &Ident,
506    context: &Canonicalizer<'_>,
507) -> Result<String, CanonicalizeError> {
508    let quoted = identifier.quote_style.is_some();
509    // Case folding rules are stated for ASCII. Anything else keeps its exact spelling,
510    // because merging two names the database might separate is the unsafe direction.
511    let folding = if identifier.value.is_ascii() {
512        context.folding
513    } else {
514        Folding::Exact
515    };
516    let resolved = match folding {
517        Folding::LowerUnquoted if !quoted => identifier.value.to_ascii_lowercase(),
518        Folding::UpperUnquoted if !quoted => identifier.value.to_ascii_uppercase(),
519        Folding::CaseInsensitive => identifier.value.to_ascii_lowercase(),
520        _ => identifier.value.clone(),
521    };
522
523    if bare_spelling_is_faithful(&resolved, quoted, folding) {
524        return Ok(resolved);
525    }
526    // A delimited name escapes its own delimiter by doubling it, so `a"b` is written
527    // `"a""b"`. Emitting the delimiter raw produces a name that reads back as something else.
528    let quote = identifier.quote_style.unwrap_or('"');
529    let mut delimited = String::with_capacity(resolved.len() + 2);
530    delimited.push(quote);
531    for character in resolved.chars() {
532        if character == quote {
533            delimited.push(quote);
534        }
535        delimited.push(character);
536    }
537    delimited.push(quote);
538    Ok(delimited)
539}
540
541/// Reports whether writing `name` without quotes reads back as `name` itself.
542///
543/// The answer must depend only on the name, never on how canonicalization is going, or one
544/// predicate could end up with two spellings and so two keys.
545fn bare_spelling_is_faithful(name: &str, quoted: bool, folding: Folding) -> bool {
546    if !quoted {
547        // This dialect already accepted the name without quotes, and folding only applied the
548        // change the dialect makes itself, so writing it bare reads back as the same name.
549        return true;
550    }
551    if !is_plain_non_keyword(name) {
552        return false;
553    }
554    match folding {
555        // Dropping the quotes hands the name to the dialect's folding, so it may only go bare
556        // when it is already in the folded form.
557        Folding::LowerUnquoted => !name.bytes().any(|byte| byte.is_ascii_uppercase()),
558        Folding::UpperUnquoted => !name.bytes().any(|byte| byte.is_ascii_lowercase()),
559        Folding::CaseInsensitive => true,
560        Folding::Exact => false,
561    }
562}
563
564/// Reports whether `name` is a bare word no dialect reads as a keyword.
565///
566/// Whether a given keyword may still name a column is dialect and position specific, so this
567/// errs towards keeping the quotes. The cost is that a quoted keyword does not share a key
568/// with its unquoted spelling, which splits one column into two entries rather than merging
569/// two columns into one.
570fn is_plain_non_keyword(name: &str) -> bool {
571    let mut characters = name.chars();
572    let Some(first) = characters.next() else {
573        return false;
574    };
575    if !(first.is_ascii_alphabetic() || first == '_')
576        || !characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
577    {
578        return false;
579    }
580    ALL_KEYWORDS
581        .binary_search(&name.to_ascii_uppercase().as_str())
582        .is_err()
583}
584
585/// Rejects a literal the parser cannot print without changing its value.
586fn reject_lossy_quoting(value: &Value) -> Result<(), CanonicalizeError> {
587    let intact = match value {
588        Value::SingleQuotedString(text) | Value::NationalStringLiteral(text) => {
589            quoting_survives_printing(text, '\'')
590        }
591        Value::DoubleQuotedString(text) => quoting_survives_printing(text, '"'),
592        _ => true,
593    };
594    if intact {
595        Ok(())
596    } else {
597        Err(CanonicalizeError::NotRoundTrippable(
598            "string literal".to_string(),
599        ))
600    }
601}
602
603/// Reports whether printing `text` inside `quote` delimiters preserves it.
604///
605/// `sqlparser` leaves a quote alone when it already looks escaped, either doubled or preceded
606/// by a backslash, so such text reads back one escape level shorter than it went in.
607fn quoting_survives_printing(text: &str, quote: char) -> bool {
608    let mut characters = text.chars().peekable();
609    let mut previous = char::default();
610    while let Some(character) = characters.next() {
611        if character == quote
612            && (previous == '\\' || characters.peek().is_some_and(|next| *next == quote))
613        {
614            return false;
615        }
616        previous = character;
617    }
618    true
619}
620
621fn collect_flat_children<'a>(expr: &'a Expr, operator: &BinaryOperator) -> Vec<&'a Expr> {
622    match expr {
623        Expr::Nested(inner) => collect_flat_children(inner, operator),
624        Expr::BinaryOp { left, op, right } if op == operator => {
625            let mut children = collect_flat_children(left, operator);
626            children.extend(collect_flat_children(right, operator));
627            children
628        }
629        _ => vec![expr],
630    }
631}
632
633const fn is_commutative(operator: &BinaryOperator) -> bool {
634    matches!(
635        operator,
636        BinaryOperator::And | BinaryOperator::Or | BinaryOperator::Eq
637    )
638}
639
640fn operator_text(operator: &BinaryOperator) -> Result<&'static str, CanonicalizeError> {
641    match operator {
642        BinaryOperator::And => Ok("AND"),
643        BinaryOperator::Or => Ok("OR"),
644        BinaryOperator::Eq => Ok("="),
645        BinaryOperator::NotEq => Ok("!="),
646        BinaryOperator::Lt => Ok("<"),
647        BinaryOperator::LtEq => Ok("<="),
648        BinaryOperator::Gt => Ok(">"),
649        BinaryOperator::GtEq => Ok(">="),
650        BinaryOperator::Plus => Ok("+"),
651        BinaryOperator::Minus => Ok("-"),
652        BinaryOperator::Multiply => Ok("*"),
653        BinaryOperator::Divide => Ok("/"),
654        BinaryOperator::Modulo => Ok("%"),
655        other => Err(CanonicalizeError::Unsupported(format!(
656            "Unsupported binary operator: {other}"
657        ))),
658    }
659}
660
661fn unary_operator_text(
662    operator: &sqlparser::ast::UnaryOperator,
663) -> Result<&'static str, CanonicalizeError> {
664    match operator {
665        sqlparser::ast::UnaryOperator::Not => Ok("NOT"),
666        sqlparser::ast::UnaryOperator::Plus => Ok("+"),
667        sqlparser::ast::UnaryOperator::Minus => Ok("-"),
668        other => Err(CanonicalizeError::Unsupported(format!(
669            "Unsupported unary operator: {other}"
670        ))),
671    }
672}