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;
20const NO_FILTER: &str = "TRUE";
22
23impl<'a> Canonicalizer<'a> {
24 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 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
77fn 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#[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 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#[derive(Clone, Copy, Eq, PartialEq)]
465enum Folding {
466 LowerUnquoted,
468 UpperUnquoted,
470 CaseInsensitive,
472 Exact,
474}
475
476pub struct Canonicalizer<'a> {
481 dialect: &'a dyn Dialect,
482 folding: Folding,
483}
484
485impl<'a> Canonicalizer<'a> {
486 #[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
502fn identifier_text(
505 identifier: &Ident,
506 context: &Canonicalizer<'_>,
507) -> Result<String, CanonicalizeError> {
508 let quoted = identifier.quote_style.is_some();
509 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 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
541fn bare_spelling_is_faithful(name: &str, quoted: bool, folding: Folding) -> bool {
546 if !quoted {
547 return true;
550 }
551 if !is_plain_non_keyword(name) {
552 return false;
553 }
554 match folding {
555 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
564fn 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
585fn 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
603fn 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}