1use alloc::boxed::Box;
2use alloc::format;
3use alloc::string::{String, ToString};
4use alloc::vec;
5use alloc::vec::Vec;
6use core::hash::{Hash, Hasher};
7
8use seahash::SeaHasher;
9use sqlparser::ast::{
10 AccessExpr, BinaryOperator, CastKind, CeilFloorKind, DateTimeField, Distinct, Expr,
11 ExtractSyntax, Function, FunctionArg, FunctionArgExpr, FunctionArguments, GroupByExpr, Ident,
12 Interval, LimitClause, ObjectName, Query, Select, SelectItem, SelectModifiers, SetExpr,
13 Statement, Subscript, TableFactor, UnaryOperator, Value,
14};
15use sqlparser::dialect::{AnsiDialect, Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
16use sqlparser::keywords::ALL_KEYWORDS;
17use sqlparser::parser::Parser;
18use sqlparser::tokenizer::Token;
19
20use crate::CanonicalizeError;
21
22const MAX_EXPR_DEPTH: usize = 128;
23const MAX_SQL_LEN: usize = 8192;
24
25impl<'a> Canonicalizer<'a> {
26 pub fn normalize_sql(&self, sql: &str) -> Result<String, CanonicalizeError> {
31 let canonical = normalize_sql_inner(sql, self)?;
32 confirm_reads_back_as_itself(canonical, self)
33 }
34
35 pub fn normalize_where_clause(
41 &self,
42 where_expr: Option<&Expr>,
43 ) -> Result<String, CanonicalizeError> {
44 let canonical = normalize_where_clause_inner(where_expr, self)?;
45 confirm_reads_back_as_itself(canonical, self)
46 }
47}
48
49fn normalize_sql_inner(
50 sql: &str,
51 context: &Canonicalizer<'_>,
52) -> Result<String, CanonicalizeError> {
53 if sql.len() > MAX_SQL_LEN {
54 return Err(CanonicalizeError::InputTooLong { limit: MAX_SQL_LEN });
55 }
56 check_sql_sanity(sql)?;
57
58 let statements = Parser::parse_sql(context.dialect, sql)
59 .map_err(|error| CanonicalizeError::Parse(error.to_string()))?;
60 let [statement] = statements.as_slice() else {
61 return Err(CanonicalizeError::Unsupported(
62 "Expected exactly one SELECT statement".to_string(),
63 ));
64 };
65 let where_expr = extract_where(statement)?;
66 normalize_where_clause_inner(where_expr, context)
67}
68
69fn normalize_where_clause_inner(
70 where_expr: Option<&Expr>,
71 context: &Canonicalizer<'_>,
72) -> Result<String, CanonicalizeError> {
73 match where_expr {
74 Some(expr) => normalize_expr_inner(expr, 0, false, context),
75 None => {
79 let every_row = if context.true_is_reserved {
80 Expr::Value(Value::Boolean(true).with_empty_span())
81 } else {
82 let one = || {
83 Box::new(Expr::Value(
84 Value::Number("1".into(), false).with_empty_span(),
85 ))
86 };
87 Expr::BinaryOp {
88 left: one(),
89 op: BinaryOperator::Eq,
90 right: one(),
91 }
92 };
93 normalize_expr_inner(&every_row, 0, false, context)
94 }
95 }
96}
97
98fn confirm_reads_back_as_itself(
105 canonical: String,
106 context: &Canonicalizer<'_>,
107) -> Result<String, CanonicalizeError> {
108 if read_back(&canonical, context).is_some_and(|again| again == canonical) {
109 Ok(canonical)
110 } else {
111 Err(CanonicalizeError::NotRoundTrippable(canonical))
112 }
113}
114
115fn read_back(canonical: &str, context: &Canonicalizer<'_>) -> Option<String> {
117 let mut parser = Parser::new(context.dialect).try_with_sql(canonical).ok()?;
118 let expr = parser.parse_expr().ok()?;
119 if !matches!(parser.peek_token_ref().token, Token::EOF) {
121 return None;
122 }
123 normalize_expr_inner(&expr, 0, false, context).ok()
124}
125
126#[must_use]
128pub fn hash_canonical(normalized: &str) -> u128 {
129 let mut first = SeaHasher::new();
130 normalized.hash(&mut first);
131 let first = first.finish();
132
133 let mut second = SeaHasher::with_seeds(
134 first,
135 first.wrapping_add(1),
136 first.wrapping_add(2),
137 first.wrapping_add(3),
138 );
139 normalized.hash(&mut second);
140 (u128::from(first) << 64) | u128::from(second.finish())
141}
142
143fn check_sql_sanity(sql: &str) -> Result<(), CanonicalizeError> {
144 let mut paren_depth = 0usize;
145 let mut bracket_depth = 0usize;
146 let mut consecutive_ops = 0usize;
147
148 for byte in sql.bytes() {
149 match byte {
150 b'(' => {
151 paren_depth += 1;
152 consecutive_ops += 1;
153 }
154 b')' => {
155 paren_depth = paren_depth.saturating_sub(1);
156 consecutive_ops = 0;
157 }
158 b'[' => {
159 bracket_depth += 1;
160 consecutive_ops = 0;
161 }
162 b']' => {
163 bracket_depth = bracket_depth.saturating_sub(1);
164 consecutive_ops = 0;
165 }
166 b'+' | b'-' | b'*' | b'/' | b'=' | b'<' | b'>' | b'!' | b'~' => {
167 consecutive_ops += 1;
168 }
169 b' ' | b'\t' | b'\n' | b'\r' => {}
170 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F => {
171 return Err(CanonicalizeError::Unsupported(
172 "Control character in SQL".to_string(),
173 ));
174 }
175 _ => consecutive_ops = 0,
176 }
177
178 if paren_depth > MAX_EXPR_DEPTH
179 || bracket_depth > MAX_EXPR_DEPTH
180 || consecutive_ops > MAX_EXPR_DEPTH
181 {
182 return Err(CanonicalizeError::TooDeep {
183 limit: MAX_EXPR_DEPTH,
184 });
185 }
186 }
187
188 if paren_depth != 0 {
189 return Err(CanonicalizeError::Unsupported(
190 "Unbalanced parentheses".to_string(),
191 ));
192 }
193 if bracket_depth != 0 {
194 return Err(CanonicalizeError::Unsupported(
195 "Unbalanced square brackets".to_string(),
196 ));
197 }
198 Ok(())
199}
200
201fn extract_where(statement: &Statement) -> Result<Option<&Expr>, CanonicalizeError> {
202 let Statement::Query(query) = statement else {
203 return Err(CanonicalizeError::Unsupported(
204 "Only SELECT statements are supported".to_string(),
205 ));
206 };
207 Ok(single_table_select(query)?.selection.as_ref())
208}
209
210fn single_table_select(query: &Query) -> Result<&Select, CanonicalizeError> {
211 let SetExpr::Select(select) = query.body.as_ref() else {
212 return Err(CanonicalizeError::Unsupported(
213 "Set operations are not supported".to_string(),
214 ));
215 };
216 if select.from.len() != 1 {
217 return Err(CanonicalizeError::Unsupported(
218 "Exactly one table is required".to_string(),
219 ));
220 }
221 if !select.from[0].joins.is_empty() {
222 return Err(CanonicalizeError::Unsupported(
223 "JOINs not supported".to_string(),
224 ));
225 }
226 if !matches!(select.from[0].relation, TableFactor::Table { .. }) {
227 return Err(CanonicalizeError::Unsupported(
228 "Subqueries and derived tables not supported".to_string(),
229 ));
230 }
231 check_served_clauses(query, select)?;
232 Ok(select)
233}
234
235fn check_served_clauses(query: &Query, select: &Select) -> Result<(), CanonicalizeError> {
236 let Query {
237 with,
238 body: _,
239 order_by: _,
240 limit_clause,
241 fetch,
242 locks,
243 for_clause,
244 settings,
245 format_clause,
246 pipe_operators,
247 } = query;
248 let Select {
249 select_token: _,
250 optimizer_hints,
251 distinct,
252 select_modifiers,
253 top,
254 top_before_distinct: _,
255 projection: _,
256 exclude,
257 into,
258 from: _,
259 lateral_views,
260 prewhere,
261 selection: _,
262 connect_by,
263 group_by: _,
264 cluster_by,
265 distribute_by,
266 sort_by,
267 having: _,
268 named_window,
269 qualify,
270 window_before_qualify: _,
271 value_table_mode,
272 flavor: _,
273 } = select;
274
275 let deduplicating = !matches!(distinct, None | Some(Distinct::All));
276 for (present, clause) in [
277 (with.is_some(), "WITH"),
278 (deduplicating, "DISTINCT"),
279 (
280 limit_clause.is_some(),
281 limit_clause_name(limit_clause.as_ref()),
282 ),
283 (fetch.is_some(), "FETCH"),
284 (!locks.is_empty(), "FOR UPDATE or FOR SHARE"),
285 (for_clause.is_some(), "FOR XML or FOR JSON"),
286 (settings.is_some(), "SETTINGS"),
287 (format_clause.is_some(), "FORMAT"),
288 (!pipe_operators.is_empty(), "pipe operators"),
289 (!optimizer_hints.is_empty(), "optimizer hints"),
290 (
291 select_modifiers
292 .as_ref()
293 .is_some_and(SelectModifiers::is_any_set),
294 "SELECT modifiers",
295 ),
296 (top.is_some(), "TOP"),
297 (exclude.is_some(), "EXCLUDE"),
298 (into.is_some(), "INTO"),
299 (!lateral_views.is_empty(), "LATERAL VIEW"),
300 (prewhere.is_some(), "PREWHERE"),
301 (!connect_by.is_empty(), "CONNECT BY"),
302 (!cluster_by.is_empty(), "CLUSTER BY"),
303 (!distribute_by.is_empty(), "DISTRIBUTE BY"),
304 (!sort_by.is_empty(), "SORT BY"),
305 (!named_window.is_empty(), "WINDOW"),
306 (qualify.is_some(), "QUALIFY"),
307 (
308 value_table_mode.is_some(),
309 "SELECT AS VALUE or SELECT AS STRUCT",
310 ),
311 ] {
312 if present {
313 return Err(CanonicalizeError::Unsupported(format!(
314 "{clause} is not supported"
315 )));
316 }
317 }
318 Ok(())
319}
320
321const fn limit_clause_name(limit: Option<&LimitClause>) -> &'static str {
322 match limit {
323 Some(LimitClause::LimitOffset {
324 limit: None,
325 offset,
326 limit_by,
327 }) => {
328 if offset.is_some() {
329 "OFFSET"
330 } else if limit_by.is_empty() {
331 "LIMIT"
332 } else {
333 "LIMIT BY"
334 }
335 }
336 None | Some(LimitClause::LimitOffset { .. } | LimitClause::OffsetCommaLimit { .. }) => {
337 "LIMIT"
338 }
339 }
340}
341
342fn normalize_expr_inner(
343 expr: &Expr,
344 depth: usize,
345 tight_parent: bool,
346 context: &Canonicalizer<'_>,
347) -> Result<String, CanonicalizeError> {
348 if depth > MAX_EXPR_DEPTH {
349 return Err(CanonicalizeError::TooDeep {
350 limit: MAX_EXPR_DEPTH,
351 });
352 }
353
354 let text = match expr {
355 Expr::BinaryOp { left, op, right } => {
356 if matches!(op, BinaryOperator::And | BinaryOperator::Or) {
357 let mut children = collect_flat_children(left, op);
358 children.extend(collect_flat_children(right, op));
359 let mut child_text: Vec<String> = children
360 .iter()
361 .map(|child| normalize_expr_inner(child, depth + 1, false, context))
362 .collect::<Result<_, _>>()?;
363 child_text.sort();
364 let operator = operator_text(op)?;
365 child_text
366 .into_iter()
367 .reduce(|left, right| format!("({left} {operator} {right})"))
368 .unwrap_or_default()
369 } else {
370 let operator = operator_text(op)?;
371 let order = operand_order(op, context);
372 infix_text(left, operator, right, order, depth, context)?
373 }
374 }
375 Expr::IsDistinctFrom(left, right) => {
376 let order = OperandOrder::Compared;
377 infix_text(left, "IS DISTINCT FROM", right, order, depth, context)?
378 }
379 Expr::IsNotDistinctFrom(left, right) => {
380 let order = OperandOrder::Compared;
381 infix_text(left, "IS NOT DISTINCT FROM", right, order, depth, context)?
382 }
383 Expr::UnaryOp { op, expr } => format!(
384 "{} {}",
385 unary_operator_text(op)?,
386 normalize_expr_inner(expr, depth + 1, true, context)?
387 ),
388 Expr::IsNull(operand) => postfix_text(operand, "IS NULL", depth, context)?,
389 Expr::IsNotNull(operand) => postfix_text(operand, "IS NOT NULL", depth, context)?,
390 Expr::IsTrue(operand) => postfix_text(operand, "IS TRUE", depth, context)?,
391 Expr::IsNotTrue(operand) => postfix_text(operand, "IS NOT TRUE", depth, context)?,
392 Expr::IsFalse(operand) => postfix_text(operand, "IS FALSE", depth, context)?,
393 Expr::IsNotFalse(operand) => postfix_text(operand, "IS NOT FALSE", depth, context)?,
394 Expr::IsUnknown(operand) => postfix_text(operand, "IS UNKNOWN", depth, context)?,
395 Expr::IsNotUnknown(operand) => postfix_text(operand, "IS NOT UNKNOWN", depth, context)?,
396 Expr::IsJson {
397 expr,
398 kind,
399 unique_keys,
400 negated,
401 } => {
402 let not = if *negated { "NOT " } else { "" };
403 let mut suffix = format!("IS {not}JSON");
404 if let Some(kind) = kind {
405 suffix.push_str(&format!(" {kind}"));
406 }
407 if let Some(unique_keys) = unique_keys {
408 suffix.push_str(&format!(" {unique_keys}"));
409 }
410 postfix_text(expr, &suffix, depth, context)?
411 }
412 Expr::IsNormalized {
413 expr,
414 form,
415 negated,
416 } => {
417 let not = if *negated { "NOT " } else { "" };
418 let suffix = form.as_ref().map_or_else(
419 || format!("IS {not}NORMALIZED"),
420 |form| format!("IS {not}{form} NORMALIZED"),
421 );
422 postfix_text(expr, &suffix, depth, context)?
423 }
424 Expr::InList {
425 expr,
426 list,
427 negated,
428 } => {
429 let mut items: Vec<String> = list
430 .iter()
431 .map(|item| normalize_expr_inner(item, depth + 1, true, context))
432 .collect::<Result<_, _>>()?;
433 items.sort();
434 let not = if *negated { "NOT " } else { "" };
435 format!(
436 "{} {not}IN ({})",
437 normalize_expr_inner(expr, depth + 1, true, context)?,
438 items.join(", ")
439 )
440 }
441 Expr::InSubquery {
442 expr,
443 subquery,
444 negated,
445 } => {
446 let not = if *negated { "NOT " } else { "" };
447 format!(
448 "{} {not}IN ({})",
449 normalize_expr_inner(expr, depth + 1, true, context)?,
450 subquery_text(subquery, depth, context)?
451 )
452 }
453 Expr::Exists { subquery, negated } => {
454 let not = if *negated { "NOT " } else { "" };
455 format!("{not}EXISTS ({})", subquery_text(subquery, depth, context)?)
456 }
457 Expr::Subquery(query) => format!("({})", subquery_text(query, depth, context)?),
458 Expr::Between {
459 expr,
460 low,
461 high,
462 negated,
463 } => {
464 let not = if *negated { "NOT " } else { "" };
465 format!(
466 "{} {not}BETWEEN {} AND {}",
467 normalize_expr_inner(expr, depth + 1, true, context)?,
468 normalize_expr_inner(low, depth + 1, true, context)?,
469 normalize_expr_inner(high, depth + 1, true, context)?
470 )
471 }
472 Expr::Like {
473 negated,
474 any,
475 expr,
476 pattern,
477 escape_char,
478 } => {
479 let operator = match_operator(*negated, "LIKE");
480 let escape = escape_char.as_deref();
481 pattern_match_text(expr, &operator, *any, pattern, escape, depth, context)?
482 }
483 Expr::ILike {
484 negated,
485 any,
486 expr,
487 pattern,
488 escape_char,
489 } => {
490 let operator = match_operator(*negated, "ILIKE");
491 let escape = escape_char.as_deref();
492 pattern_match_text(expr, &operator, *any, pattern, escape, depth, context)?
493 }
494 Expr::SimilarTo {
495 negated,
496 expr,
497 pattern,
498 escape_char,
499 } => {
500 let operator = match_operator(*negated, "SIMILAR TO");
501 let escape = escape_char.as_deref();
502 pattern_match_text(expr, &operator, false, pattern, escape, depth, context)?
503 }
504 Expr::RLike {
507 negated,
508 expr,
509 pattern,
510 regexp: _,
511 } => {
512 let operator = match_operator(*negated, "RLIKE");
513 pattern_match_text(expr, &operator, false, pattern, None, depth, context)?
514 }
515 Expr::AnyOp {
517 left,
518 compare_op,
519 right,
520 is_some: _,
521 } => quantified_text(left, compare_op, "ANY", right, depth, context)?,
522 Expr::AllOp {
523 left,
524 compare_op,
525 right,
526 } => quantified_text(left, compare_op, "ALL", right, depth, context)?,
527 Expr::MemberOf(member) => format!(
528 "{} MEMBER OF({})",
529 normalize_expr_inner(&member.value, depth + 1, true, context)?,
530 normalize_expr_inner(&member.array, depth + 1, false, context)?
531 ),
532 Expr::AtTimeZone {
533 timestamp,
534 time_zone,
535 } => format!(
536 "{} AT TIME ZONE {}",
537 normalize_expr_inner(timestamp, depth + 1, true, context)?,
538 normalize_expr_inner(time_zone, depth + 1, true, context)?
539 ),
540 Expr::Collate { expr, collation } => format!(
543 "{} COLLATE {collation}",
544 normalize_expr_inner(expr, depth + 1, true, context)?
545 ),
546 Expr::MatchAgainst {
547 columns,
548 match_value,
549 opt_search_modifier,
550 } => {
551 let columns = columns
552 .iter()
553 .map(|column| object_name_text(column, NamePlace::Operand, context))
554 .collect::<Result<Vec<_>, _>>()?
555 .join(", ");
556 reject_lossy_quoting(&match_value.value)?;
557 let modifier = opt_search_modifier
558 .as_ref()
559 .map_or_else(String::new, |modifier| format!(" {modifier}"));
560 format!(
561 "MATCH ({columns}) AGAINST ({}{modifier})",
562 match_value.value
563 )
564 }
565 Expr::Nested(inner) => normalize_expr_inner(inner, depth + 1, tight_parent, context)?,
566 Expr::Identifier(identifier) => {
567 identifier_text(identifier, context.folding, NamePlace::Operand, context)?
568 }
569 Expr::CompoundIdentifier(parts) => {
570 qualified_name_text(parts.iter(), NamePlace::Operand, context)?
571 }
572 Expr::Value(value) => {
573 reject_lossy_quoting(&value.value)?;
574 format!("{}", value.value)
575 }
576 Expr::JsonAccess { .. } => {
579 return Err(CanonicalizeError::Unsupported(
580 "JSON path access is not supported".to_string(),
581 ));
582 }
583 Expr::Function(function) => function_text(function, depth, context)?,
584 Expr::Cast {
586 kind,
587 expr,
588 data_type,
589 format,
590 } => {
591 if format.is_some() {
592 return Err(CanonicalizeError::Unsupported(
593 "CAST with FORMAT is not supported".to_string(),
594 ));
595 }
596 let keyword = match kind {
597 CastKind::Cast | CastKind::DoubleColon => "CAST",
598 CastKind::TryCast => "TRY_CAST",
599 CastKind::SafeCast => "SAFE_CAST",
600 };
601 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
602 format!("{keyword}({expr} AS {data_type})")
603 }
604 Expr::Convert {
605 is_try,
606 expr,
607 data_type,
608 charset,
609 target_before_value,
610 styles,
611 } => {
612 if *target_before_value || !styles.is_empty() {
614 return Err(CanonicalizeError::Unsupported(
615 "CONVERT with the type first is not supported".to_string(),
616 ));
617 }
618 let target = match (data_type, charset) {
619 (Some(data_type), Some(charset)) => {
620 format!(", {data_type} CHARACTER SET {charset}")
621 }
622 (Some(data_type), None) => format!(", {data_type}"),
623 (None, Some(charset)) => format!(" USING {charset}"),
624 (None, None) => {
625 return Err(CanonicalizeError::Unsupported(
626 "CONVERT without a target is not supported".to_string(),
627 ));
628 }
629 };
630 let prefix = if *is_try { "TRY_" } else { "" };
631 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
632 format!("{prefix}CONVERT({expr}{target})")
633 }
634 Expr::Extract {
635 field,
636 syntax,
637 expr,
638 } => {
639 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
640 match syntax {
641 ExtractSyntax::From => format!("EXTRACT({field} FROM {expr})"),
642 ExtractSyntax::Comma => format!("EXTRACT({field}, {expr})"),
643 }
644 }
645 Expr::Ceil { expr, field } => rounding_text("CEIL", expr, field, depth, context)?,
646 Expr::Floor { expr, field } => rounding_text("FLOOR", expr, field, depth, context)?,
647 Expr::Position { expr, r#in } => format!(
649 "POSITION({} IN {})",
650 normalize_expr_inner(expr, depth + 1, true, context)?,
651 normalize_expr_inner(r#in, depth + 1, false, context)?
652 ),
653 Expr::Substring {
654 expr,
655 substring_from,
656 substring_for,
657 special,
658 shorthand,
659 } => {
660 let name = if *shorthand { "SUBSTR" } else { "SUBSTRING" };
661 let (from, length) = if *special {
662 (", ", ", ")
663 } else {
664 (" FROM ", " FOR ")
665 };
666 let mut text = format!(
667 "{name}({}",
668 normalize_expr_inner(expr, depth + 1, false, context)?
669 );
670 for (separator, part) in [(from, substring_from), (length, substring_for)] {
671 if let Some(part) = part {
672 text.push_str(separator);
673 text.push_str(&normalize_expr_inner(part, depth + 1, false, context)?);
674 }
675 }
676 text.push(')');
677 text
678 }
679 Expr::Trim {
680 expr,
681 trim_where,
682 trim_what,
683 trim_characters,
684 } => {
685 let mut text = String::from("TRIM(");
686 if let Some(trim_where) = trim_where {
687 text.push_str(&format!("{trim_where} "));
688 }
689 if let Some(trim_what) = trim_what {
690 text.push_str(&normalize_expr_inner(trim_what, depth + 1, false, context)?);
691 text.push_str(" FROM ");
692 }
693 text.push_str(&normalize_expr_inner(expr, depth + 1, false, context)?);
694 if let Some(characters) = trim_characters {
695 text.push_str(", ");
696 text.push_str(&list_text(characters, depth, context)?);
697 }
698 text.push(')');
699 text
700 }
701 Expr::Overlay {
702 expr,
703 overlay_what,
704 overlay_from,
705 overlay_for,
706 } => {
707 let mut text = format!(
708 "OVERLAY({} PLACING {} FROM {}",
709 normalize_expr_inner(expr, depth + 1, false, context)?,
710 normalize_expr_inner(overlay_what, depth + 1, false, context)?,
711 normalize_expr_inner(overlay_from, depth + 1, false, context)?
712 );
713 if let Some(overlay_for) = overlay_for {
714 text.push_str(" FOR ");
715 text.push_str(&normalize_expr_inner(
716 overlay_for,
717 depth + 1,
718 false,
719 context,
720 )?);
721 }
722 text.push(')');
723 text
724 }
725 Expr::Case {
726 case_token: _,
727 end_token: _,
728 operand,
729 conditions,
730 else_result,
731 } => {
732 let mut text = String::from("CASE");
733 if let Some(operand) = operand {
734 text.push(' ');
735 text.push_str(&normalize_expr_inner(operand, depth + 1, false, context)?);
736 }
737 for when in conditions {
738 text.push_str(" WHEN ");
739 text.push_str(&normalize_expr_inner(
740 &when.condition,
741 depth + 1,
742 false,
743 context,
744 )?);
745 text.push_str(" THEN ");
746 text.push_str(&normalize_expr_inner(
747 &when.result,
748 depth + 1,
749 false,
750 context,
751 )?);
752 }
753 if let Some(result) = else_result.as_deref().filter(|result| !is_null(result)) {
755 text.push_str(" ELSE ");
756 text.push_str(&normalize_expr_inner(result, depth + 1, false, context)?);
757 }
758 text.push_str(" END");
759 text
760 }
761 Expr::Tuple(items) => format!("({})", list_text(items, depth, context)?),
762 Expr::Array(array) => {
763 let keyword = if array.named { "ARRAY" } else { "" };
764 format!("{keyword}[{}]", list_text(&array.elem, depth, context)?)
765 }
766 Expr::Interval(interval) => interval_text(interval, depth, context)?,
767 Expr::TypedString(typed) => {
768 if typed.uses_odbc_syntax {
769 return Err(CanonicalizeError::Unsupported(
770 "ODBC escape literals are not supported".to_string(),
771 ));
772 }
773 reject_lossy_quoting(&typed.value.value)?;
774 format!("{} {}", typed.data_type, typed.value)
775 }
776 Expr::Prefixed { prefix, value } => format!(
777 "{prefix} {}",
778 normalize_expr_inner(value, depth + 1, true, context)?
779 ),
780 Expr::CompoundFieldAccess { root, access_chain } => {
781 field_access_text(root, access_chain, depth, context)?
782 }
783 Expr::InUnnest { .. } => return Err(refused("IN UNNEST")),
785 Expr::Struct { .. } => return Err(refused("STRUCT")),
786 Expr::Named { .. } => return Err(refused("a named expression")),
787 Expr::Dictionary(_) => return Err(refused("a dictionary literal")),
788 Expr::Map(_) => return Err(refused("a map literal")),
789 Expr::Lambda(_) => return Err(refused("a lambda")),
790 Expr::OuterJoin(_) => return Err(refused("the (+) outer join marker")),
791 Expr::Prior(_) => return Err(refused("PRIOR")),
792 Expr::GroupingSets(_) | Expr::Cube(_) | Expr::Rollup(_) => {
793 return Err(refused("a grouping set"));
794 }
795 Expr::Wildcard(_) | Expr::QualifiedWildcard(..) => return Err(refused("a wildcard")),
796 };
797 Ok(if tight_parent && !encloses_itself(expr) {
798 format!("({text})")
799 } else {
800 text
801 })
802}
803
804#[derive(Clone, Copy)]
806enum OperandOrder {
807 Written,
809 Sorted,
811 Compared,
814 Mirrored(&'static str),
816}
817
818fn infix_text(
821 left: &Expr,
822 operator: &'static str,
823 right: &Expr,
824 order: OperandOrder,
825 depth: usize,
826 context: &Canonicalizer<'_>,
827) -> Result<String, CanonicalizeError> {
828 let swappable =
831 context.collation_is_symmetric || is_literal(left, context) || is_literal(right, context);
832 let left = normalize_expr_inner(left, depth + 1, true, context)?;
833 let right = normalize_expr_inner(right, depth + 1, true, context)?;
834 let (left, operator, right) = match order {
835 OperandOrder::Sorted if left > right => (right, operator, left),
836 OperandOrder::Compared if swappable && left > right => (right, operator, left),
837 OperandOrder::Mirrored(mirrored)
840 if (swappable && left > right) || (left == right && mirrored < operator) =>
841 {
842 (right, mirrored, left)
843 }
844 _ => (left, operator, right),
845 };
846 Ok(format!("({left} {operator} {right})"))
847}
848
849fn postfix_text(
851 operand: &Expr,
852 suffix: &str,
853 depth: usize,
854 context: &Canonicalizer<'_>,
855) -> Result<String, CanonicalizeError> {
856 let operand = normalize_expr_inner(operand, depth + 1, true, context)?;
857 Ok(format!("{operand} {suffix}"))
858}
859
860fn match_operator(negated: bool, operator: &str) -> String {
862 if negated {
863 format!("NOT {operator}")
864 } else {
865 operator.to_string()
866 }
867}
868
869fn pattern_match_text(
872 subject: &Expr,
873 operator: &str,
874 any: bool,
875 pattern: &Expr,
876 escape: Option<&Expr>,
877 depth: usize,
878 context: &Canonicalizer<'_>,
879) -> Result<String, CanonicalizeError> {
880 let subject = normalize_expr_inner(subject, depth + 1, true, context)?;
881 let pattern = if any {
882 format!("ANY {}", argument_text(pattern, depth, context)?)
883 } else {
884 normalize_expr_inner(pattern, depth + 1, true, context)?
885 };
886 let escape = match escape {
887 Some(escape) => format!(
888 " ESCAPE {}",
889 normalize_expr_inner(escape, depth + 1, true, context)?
890 ),
891 None => String::new(),
892 };
893 Ok(format!("{subject} {operator} {pattern}{escape}"))
894}
895
896fn quantified_text(
898 left: &Expr,
899 operator: &BinaryOperator,
900 quantifier: &str,
901 right: &Expr,
902 depth: usize,
903 context: &Canonicalizer<'_>,
904) -> Result<String, CanonicalizeError> {
905 let left = normalize_expr_inner(left, depth + 1, true, context)?;
906 let operator = operator_text(operator)?;
907 let right = argument_text(right, depth, context)?;
908 Ok(format!("{left} {operator} {quantifier}{right}"))
909}
910
911fn argument_text(
914 argument: &Expr,
915 depth: usize,
916 context: &Canonicalizer<'_>,
917) -> Result<String, CanonicalizeError> {
918 let text = normalize_expr_inner(argument, depth + 1, false, context)?;
919 let mut inner = argument;
920 while let Expr::Nested(nested) = inner {
921 inner = nested;
922 }
923 Ok(if matches!(inner, Expr::Tuple(_) | Expr::Subquery(_)) {
924 text
925 } else {
926 format!("({text})")
927 })
928}
929
930fn reject_quantifier_name(name: &ObjectName) -> Result<(), CanonicalizeError> {
935 let quantifier = match name.0.as_slice() {
936 [part] => part.as_ident().is_some_and(|ident| {
937 ident.quote_style.is_none()
938 && ["ANY", "SOME", "ALL"]
939 .iter()
940 .any(|keyword| ident.value.eq_ignore_ascii_case(keyword))
941 }),
942 _ => false,
943 };
944 if quantifier {
945 Err(CanonicalizeError::Unsupported(format!(
946 "Function named {name} reads back as a quantifier"
947 )))
948 } else {
949 Ok(())
950 }
951}
952
953fn qualified_name_text<'i>(
955 parts: impl ExactSizeIterator<Item = &'i Ident>,
956 place: NamePlace,
957 context: &Canonicalizer<'_>,
958) -> Result<String, CanonicalizeError> {
959 let column = parts.len().saturating_sub(1);
960 Ok(parts
961 .enumerate()
962 .map(|(index, part)| {
963 let folding = if index == column {
964 context.folding
965 } else {
966 context.qualifier_folding
967 };
968 identifier_text(part, folding, place, context)
969 })
970 .collect::<Result<Vec<_>, _>>()?
971 .join("."))
972}
973
974fn refused(form: &str) -> CanonicalizeError {
976 CanonicalizeError::Unsupported(format!("{form} is not supported in a predicate"))
977}
978
979fn subquery_text(
984 query: &Query,
985 depth: usize,
986 context: &Canonicalizer<'_>,
987) -> Result<String, CanonicalizeError> {
988 let unsupported = |part: &str| {
989 CanonicalizeError::Unsupported(format!("{part} in a subquery is not supported"))
990 };
991 let select = single_table_select(query)?;
992 if query.order_by.is_some() {
993 return Err(unsupported("ORDER BY"));
994 }
995 if !matches!(&select.group_by, GroupByExpr::Expressions(exprs, modifiers) if exprs.is_empty() && modifiers.is_empty())
996 {
997 return Err(unsupported("GROUP BY"));
998 }
999 if select.having.is_some() {
1000 return Err(unsupported("HAVING"));
1001 }
1002 let TableFactor::Table {
1003 name,
1004 alias: None,
1005 args: None,
1006 with_hints,
1007 version: None,
1008 with_ordinality: false,
1009 partitions,
1010 json_path: None,
1011 sample: None,
1012 index_hints,
1013 } = &select.from[0].relation
1014 else {
1015 return Err(unsupported("a table alias or table option"));
1016 };
1017 if !with_hints.is_empty() || !partitions.is_empty() || !index_hints.is_empty() {
1018 return Err(unsupported("a table hint"));
1019 }
1020 let items = select
1021 .projection
1022 .iter()
1023 .map(|item| match item {
1024 SelectItem::UnnamedExpr(expr) => normalize_expr_inner(expr, depth + 1, false, context),
1025 SelectItem::Wildcard(options)
1026 if options.opt_ilike.is_none()
1027 && options.opt_exclude.is_none()
1028 && options.opt_except.is_none()
1029 && options.opt_replace.is_none()
1030 && options.opt_rename.is_none()
1031 && options.opt_alias.is_none() =>
1032 {
1033 Ok("*".to_string())
1034 }
1035 _ => Err(unsupported("an alias or qualified wildcard")),
1036 })
1037 .collect::<Result<Vec<_>, _>>()?
1038 .join(", ");
1039 let table = name
1040 .0
1041 .iter()
1042 .map(|part| match part.as_ident() {
1043 Some(part) => {
1044 identifier_text(part, context.qualifier_folding, NamePlace::Operand, context)
1045 }
1046 None => Err(unsupported("a computed table name")),
1047 })
1048 .collect::<Result<Vec<_>, _>>()?
1049 .join(".");
1050 let mut text = format!("SELECT {items} FROM {table}");
1051 if let Some(filter) = select
1053 .selection
1054 .as_ref()
1055 .filter(|filter| !(context.true_is_reserved && is_true(filter)))
1056 {
1057 text.push_str(" WHERE ");
1058 text.push_str(&normalize_expr_inner(filter, depth + 1, false, context)?);
1059 }
1060 Ok(text)
1061}
1062
1063fn function_text(
1065 function: &Function,
1066 depth: usize,
1067 context: &Canonicalizer<'_>,
1068) -> Result<String, CanonicalizeError> {
1069 let Function {
1070 name,
1071 uses_odbc_syntax,
1072 parameters,
1073 args,
1074 filter,
1075 null_treatment,
1076 over,
1077 within_group,
1078 } = function;
1079 reject_quantifier_name(name)?;
1080 let plain = !*uses_odbc_syntax
1081 && matches!(parameters, FunctionArguments::None)
1082 && filter.is_none()
1083 && null_treatment.is_none()
1084 && over.is_none()
1085 && within_group.is_empty();
1086 let unsupported =
1087 || CanonicalizeError::Unsupported(format!("Call to {name} is not a plain function call"));
1088 if !plain {
1089 return Err(unsupported());
1090 }
1091 let name = object_name_text(name, NamePlace::Function, context)?;
1092 match args {
1093 FunctionArguments::None => Ok(name),
1094 FunctionArguments::Subquery(_) => Err(unsupported()),
1095 FunctionArguments::List(list) => {
1096 if list.duplicate_treatment.is_some() || !list.clauses.is_empty() {
1097 return Err(unsupported());
1098 }
1099 let args = list
1100 .args
1101 .iter()
1102 .map(|arg| match arg {
1103 FunctionArg::Unnamed(FunctionArgExpr::Expr(arg)) => {
1106 normalize_expr_inner(arg, depth + 1, true, context)
1107 }
1108 _ => Err(unsupported()),
1109 })
1110 .collect::<Result<Vec<_>, _>>()?;
1111 Ok(format!("{name}({})", args.join(", ")))
1112 }
1113 }
1114}
1115
1116fn list_text(
1118 items: &[Expr],
1119 depth: usize,
1120 context: &Canonicalizer<'_>,
1121) -> Result<String, CanonicalizeError> {
1122 Ok(items
1123 .iter()
1124 .map(|item| normalize_expr_inner(item, depth + 1, false, context))
1125 .collect::<Result<Vec<_>, _>>()?
1126 .join(", "))
1127}
1128
1129fn rounding_text(
1131 name: &str,
1132 expr: &Expr,
1133 field: &CeilFloorKind,
1134 depth: usize,
1135 context: &Canonicalizer<'_>,
1136) -> Result<String, CanonicalizeError> {
1137 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
1138 Ok(match field {
1139 CeilFloorKind::DateTimeField(DateTimeField::NoDateTime) => format!("{name}({expr})"),
1140 CeilFloorKind::DateTimeField(field) => format!("{name}({expr} TO {field})"),
1141 CeilFloorKind::Scale(scale) => format!("{name}({expr}, {scale})"),
1142 })
1143}
1144
1145fn interval_text(
1147 interval: &Interval,
1148 depth: usize,
1149 context: &Canonicalizer<'_>,
1150) -> Result<String, CanonicalizeError> {
1151 let Interval {
1152 value,
1153 leading_field,
1154 leading_precision,
1155 last_field,
1156 fractional_seconds_precision,
1157 } = interval;
1158 let mut text = format!(
1159 "INTERVAL {}",
1160 normalize_expr_inner(value, depth + 1, true, context)?
1161 );
1162 if let (Some(DateTimeField::Second), Some(leading), Some(fractional)) = (
1163 leading_field,
1164 leading_precision,
1165 fractional_seconds_precision,
1166 ) {
1167 text.push_str(&format!(" SECOND ({leading}, {fractional})"));
1168 return Ok(text);
1169 }
1170 if let Some(leading_field) = leading_field {
1171 text.push_str(&format!(" {leading_field}"));
1172 }
1173 if let Some(leading_precision) = leading_precision {
1174 text.push_str(&format!(" ({leading_precision})"));
1175 }
1176 if let Some(last_field) = last_field {
1177 text.push_str(&format!(" TO {last_field}"));
1178 }
1179 if let Some(fractional) = fractional_seconds_precision {
1180 text.push_str(&format!(" ({fractional})"));
1181 }
1182 Ok(text)
1183}
1184
1185fn field_access_text(
1191 root: &Expr,
1192 access_chain: &[AccessExpr],
1193 depth: usize,
1194 context: &Canonicalizer<'_>,
1195) -> Result<String, CanonicalizeError> {
1196 let mut text = match root {
1197 Expr::Nested(inner) => format!(
1198 "({})",
1199 normalize_expr_inner(inner, depth + 1, false, context)?
1200 ),
1201 Expr::Identifier(name) => {
1202 identifier_text(name, context.qualifier_folding, NamePlace::Operand, context)?
1203 }
1204 Expr::Value(_) => return Err(literal_access()),
1205 root => normalize_expr_inner(root, depth + 1, true, context)?,
1206 };
1207 for access in access_chain {
1208 match access {
1209 AccessExpr::Dot(Expr::Identifier(name)) => {
1210 text.push('.');
1211 text.push_str(&identifier_text(
1212 name,
1213 context.qualifier_folding,
1214 NamePlace::Operand,
1215 context,
1216 )?);
1217 }
1218 AccessExpr::Dot(Expr::Value(_)) => return Err(literal_access()),
1219 AccessExpr::Dot(field) => {
1220 text.push('.');
1221 text.push_str(&normalize_expr_inner(field, depth + 1, true, context)?);
1222 }
1223 AccessExpr::Subscript(Subscript::Index { index }) => {
1224 text.push('[');
1225 text.push_str(&normalize_expr_inner(index, depth + 1, false, context)?);
1226 text.push(']');
1227 }
1228 AccessExpr::Subscript(Subscript::Slice {
1229 lower_bound,
1230 upper_bound,
1231 stride,
1232 }) => {
1233 text.push('[');
1234 if let Some(lower_bound) = lower_bound {
1235 text.push_str(&normalize_expr_inner(
1236 lower_bound,
1237 depth + 1,
1238 false,
1239 context,
1240 )?);
1241 }
1242 text.push(':');
1243 if let Some(upper_bound) = upper_bound {
1244 text.push_str(&normalize_expr_inner(
1245 upper_bound,
1246 depth + 1,
1247 false,
1248 context,
1249 )?);
1250 }
1251 if let Some(stride) = stride {
1252 text.push(':');
1253 text.push_str(&normalize_expr_inner(stride, depth + 1, false, context)?);
1254 }
1255 text.push(']');
1256 }
1257 }
1258 }
1259 Ok(text)
1260}
1261
1262fn literal_access() -> CanonicalizeError {
1265 CanonicalizeError::Unsupported("A field access on a literal is not supported".to_string())
1266}
1267
1268fn is_literal(expr: &Expr, context: &Canonicalizer<'_>) -> bool {
1273 match expr {
1274 Expr::Nested(inner) => is_literal(inner, context),
1275 Expr::UnaryOp {
1276 op: UnaryOperator::Minus | UnaryOperator::Plus,
1277 expr,
1278 } => is_literal(expr, context),
1279 Expr::Value(value) => context.true_is_reserved || !matches!(value.value, Value::Boolean(_)),
1280 _ => false,
1281 }
1282}
1283
1284fn is_true(expr: &Expr) -> bool {
1286 match expr {
1287 Expr::Nested(inner) => is_true(inner),
1288 Expr::Value(value) => matches!(value.value, Value::Boolean(true)),
1289 _ => false,
1290 }
1291}
1292
1293fn is_null(expr: &Expr) -> bool {
1295 match expr {
1296 Expr::Nested(inner) => is_null(inner),
1297 Expr::Value(value) => matches!(value.value, Value::Null),
1298 _ => false,
1299 }
1300}
1301
1302fn object_name_text(
1304 name: &ObjectName,
1305 place: NamePlace,
1306 context: &Canonicalizer<'_>,
1307) -> Result<String, CanonicalizeError> {
1308 let parts = name
1309 .0
1310 .iter()
1311 .map(|part| {
1312 part.as_ident().ok_or_else(|| {
1313 CanonicalizeError::Unsupported(format!("Computed name part: {part}"))
1314 })
1315 })
1316 .collect::<Result<Vec<_>, _>>()?;
1317 qualified_name_text(parts.into_iter(), place, context)
1318}
1319
1320const fn encloses_itself(expr: &Expr) -> bool {
1328 match expr {
1329 Expr::UnaryOp { op, .. } => !matches!(op, UnaryOperator::Not),
1331 Expr::Exists { negated, .. } => !*negated,
1332 Expr::Identifier(_)
1333 | Expr::CompoundIdentifier(_)
1334 | Expr::CompoundFieldAccess { .. }
1335 | Expr::Value(_)
1336 | Expr::TypedString(_)
1337 | Expr::Prefixed { .. }
1338 | Expr::Nested(_)
1339 | Expr::BinaryOp { .. }
1340 | Expr::IsDistinctFrom(..)
1341 | Expr::IsNotDistinctFrom(..)
1342 | Expr::Function(_)
1343 | Expr::Cast { .. }
1344 | Expr::Convert { .. }
1345 | Expr::Extract { .. }
1346 | Expr::Ceil { .. }
1347 | Expr::Floor { .. }
1348 | Expr::Position { .. }
1349 | Expr::Substring { .. }
1350 | Expr::Trim { .. }
1351 | Expr::Overlay { .. }
1352 | Expr::Case { .. }
1353 | Expr::Subquery(_)
1354 | Expr::Tuple(_)
1355 | Expr::Array(_)
1356 | Expr::MatchAgainst { .. }
1357 | Expr::Wildcard(_)
1358 | Expr::QualifiedWildcard(..) => true,
1359 Expr::IsNull(_)
1360 | Expr::IsNotNull(_)
1361 | Expr::IsTrue(_)
1362 | Expr::IsNotTrue(_)
1363 | Expr::IsFalse(_)
1364 | Expr::IsNotFalse(_)
1365 | Expr::IsUnknown(_)
1366 | Expr::IsNotUnknown(_)
1367 | Expr::IsJson { .. }
1368 | Expr::IsNormalized { .. }
1369 | Expr::InList { .. }
1370 | Expr::InSubquery { .. }
1371 | Expr::InUnnest { .. }
1372 | Expr::Between { .. }
1373 | Expr::Like { .. }
1374 | Expr::ILike { .. }
1375 | Expr::SimilarTo { .. }
1376 | Expr::RLike { .. }
1377 | Expr::AnyOp { .. }
1378 | Expr::AllOp { .. }
1379 | Expr::MemberOf(_)
1380 | Expr::AtTimeZone { .. }
1381 | Expr::Collate { .. }
1382 | Expr::Interval(_)
1383 | Expr::JsonAccess { .. }
1384 | Expr::GroupingSets(_)
1385 | Expr::Cube(_)
1386 | Expr::Rollup(_)
1387 | Expr::Struct { .. }
1388 | Expr::Named { .. }
1389 | Expr::Dictionary(_)
1390 | Expr::Map(_)
1391 | Expr::Lambda(_)
1392 | Expr::OuterJoin(_)
1393 | Expr::Prior(_) => false,
1394 }
1395}
1396
1397#[derive(Clone, Copy, Eq, PartialEq)]
1399enum Folding {
1400 LowerUnquoted,
1402 UpperUnquoted,
1404 CaseInsensitive,
1406 Exact,
1408}
1409
1410pub struct Canonicalizer<'a> {
1415 dialect: &'a dyn Dialect,
1416 folding: Folding,
1417 qualifier_folding: Folding,
1418 true_is_reserved: bool,
1420 plus_is_numeric: bool,
1422 collation_is_symmetric: bool,
1424}
1425
1426impl<'a> Canonicalizer<'a> {
1427 #[must_use]
1429 pub fn new(dialect: &'a dyn Dialect) -> Self {
1430 let folding = if dialect.is::<PostgreSqlDialect>() {
1431 Folding::LowerUnquoted
1432 } else if dialect.is::<AnsiDialect>() {
1433 Folding::UpperUnquoted
1434 } else if dialect.is::<MySqlDialect>() || dialect.is::<SQLiteDialect>() {
1435 Folding::CaseInsensitive
1436 } else {
1437 Folding::Exact
1438 };
1439 let qualifier_folding = if dialect.is::<MySqlDialect>() {
1442 Folding::Exact
1443 } else {
1444 folding
1445 };
1446 let true_is_reserved = dialect.is::<PostgreSqlDialect>()
1447 || dialect.is::<MySqlDialect>()
1448 || dialect.is::<AnsiDialect>();
1449 let plus_is_numeric = true_is_reserved || dialect.is::<SQLiteDialect>();
1450 let collation_is_symmetric = true_is_reserved;
1453 Self {
1454 dialect,
1455 folding,
1456 qualifier_folding,
1457 true_is_reserved,
1458 plus_is_numeric,
1459 collation_is_symmetric,
1460 }
1461 }
1462}
1463
1464#[derive(Clone, Copy, Eq, PartialEq)]
1466enum NamePlace {
1467 Operand,
1469 Function,
1472}
1473
1474fn identifier_text(
1477 identifier: &Ident,
1478 folding: Folding,
1479 place: NamePlace,
1480 context: &Canonicalizer<'_>,
1481) -> Result<String, CanonicalizeError> {
1482 let quoted = identifier.quote_style.is_some();
1483 let folding = if identifier.value.is_ascii() {
1486 folding
1487 } else {
1488 Folding::Exact
1489 };
1490 let resolved = match folding {
1491 Folding::LowerUnquoted if !quoted => identifier.value.to_ascii_lowercase(),
1492 Folding::UpperUnquoted if !quoted => identifier.value.to_ascii_uppercase(),
1493 Folding::CaseInsensitive => identifier.value.to_ascii_lowercase(),
1494 _ => identifier.value.clone(),
1495 };
1496
1497 if bare_spelling_is_faithful(&resolved, quoted, folding, place) {
1498 return Ok(resolved);
1499 }
1500 if !quoted && folding == Folding::Exact {
1503 return Err(CanonicalizeError::Unsupported(format!(
1504 "Name {resolved} reads as a keyword out of its place"
1505 )));
1506 }
1507 let quote = identifier
1510 .quote_style
1511 .or_else(|| context.dialect.identifier_quote_style(&resolved))
1512 .unwrap_or('"');
1513 let mut delimited = String::with_capacity(resolved.len() + 2);
1514 delimited.push(quote);
1515 for character in resolved.chars() {
1516 if character == quote {
1517 delimited.push(quote);
1518 }
1519 delimited.push(character);
1520 }
1521 delimited.push(quote);
1522 Ok(delimited)
1523}
1524
1525fn bare_spelling_is_faithful(name: &str, quoted: bool, folding: Folding, place: NamePlace) -> bool {
1530 if !quoted {
1531 return place == NamePlace::Function || !starts_expression_syntax(name);
1536 }
1537 if place == NamePlace::Function || !is_plain_non_keyword(name) {
1539 return false;
1540 }
1541 match folding {
1542 Folding::LowerUnquoted => !name.bytes().any(|byte| byte.is_ascii_uppercase()),
1545 Folding::UpperUnquoted => !name.bytes().any(|byte| byte.is_ascii_lowercase()),
1546 Folding::CaseInsensitive => true,
1547 Folding::Exact => false,
1548 }
1549}
1550
1551fn is_keyword(name: &str) -> bool {
1553 let upper = || name.bytes().map(|byte| byte.to_ascii_uppercase());
1554 ALL_KEYWORDS
1555 .binary_search_by(|keyword| keyword.bytes().cmp(upper()))
1556 .is_ok()
1557}
1558
1559const EXPRESSION_KEYWORDS: &[&str] = &[
1564 "ARRAY",
1565 "BOX",
1566 "CASE",
1567 "CAST",
1568 "CEIL",
1569 "CIRCLE",
1570 "CONVERT",
1571 "CURRENT_CATALOG",
1572 "CURRENT_DATE",
1573 "CURRENT_TIME",
1574 "CURRENT_TIMESTAMP",
1575 "CURRENT_USER",
1576 "EXISTS",
1577 "EXTRACT",
1578 "FALSE",
1579 "FLOOR",
1580 "INTERVAL",
1581 "LAMBDA",
1582 "LINE",
1583 "LOCALTIME",
1584 "LOCALTIMESTAMP",
1585 "LSEG",
1586 "MAP",
1587 "MATCH",
1588 "NOT",
1589 "NULL",
1590 "OVERLAY",
1591 "PATH",
1592 "POINT",
1593 "POLYGON",
1594 "POSITION",
1595 "PRIOR",
1596 "SAFE_CAST",
1597 "SELECT",
1598 "SESSION_USER",
1599 "STRUCT",
1600 "SUBSTR",
1601 "SUBSTRING",
1602 "TRIM",
1603 "TRUE",
1604 "TRY_CAST",
1605 "TRY_CONVERT",
1606 "USER",
1607 "WITH",
1608];
1609
1610fn starts_expression_syntax(name: &str) -> bool {
1612 EXPRESSION_KEYWORDS
1613 .iter()
1614 .any(|keyword| keyword.eq_ignore_ascii_case(name))
1615}
1616
1617fn is_plain_non_keyword(name: &str) -> bool {
1624 let mut characters = name.chars();
1625 let Some(first) = characters.next() else {
1626 return false;
1627 };
1628 if !(first.is_ascii_alphabetic() || first == '_')
1629 || !characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
1630 {
1631 return false;
1632 }
1633 !is_keyword(name)
1634}
1635
1636fn reject_lossy_quoting(value: &Value) -> Result<(), CanonicalizeError> {
1638 let intact = match value {
1639 Value::SingleQuotedString(text) | Value::NationalStringLiteral(text) => {
1640 quoting_survives_printing(text, '\'')
1641 }
1642 Value::DoubleQuotedString(text) => quoting_survives_printing(text, '"'),
1643 _ => true,
1644 };
1645 if intact {
1646 Ok(())
1647 } else {
1648 Err(CanonicalizeError::NotRoundTrippable(
1649 "string literal".to_string(),
1650 ))
1651 }
1652}
1653
1654fn quoting_survives_printing(text: &str, quote: char) -> bool {
1659 let mut characters = text.chars().peekable();
1660 let mut previous = char::default();
1661 while let Some(character) = characters.next() {
1662 if character == quote
1663 && (previous == '\\' || characters.peek().is_some_and(|next| *next == quote))
1664 {
1665 return false;
1666 }
1667 previous = character;
1668 }
1669 true
1670}
1671
1672fn collect_flat_children<'a>(expr: &'a Expr, operator: &BinaryOperator) -> Vec<&'a Expr> {
1673 match expr {
1674 Expr::Nested(inner) => collect_flat_children(inner, operator),
1675 Expr::BinaryOp { left, op, right } if op == operator => {
1676 let mut children = collect_flat_children(left, operator);
1677 children.extend(collect_flat_children(right, operator));
1678 children
1679 }
1680 _ => vec![expr],
1681 }
1682}
1683
1684const fn operand_order(operator: &BinaryOperator, context: &Canonicalizer<'_>) -> OperandOrder {
1688 match operator {
1689 BinaryOperator::Eq | BinaryOperator::NotEq | BinaryOperator::Spaceship => {
1690 OperandOrder::Compared
1691 }
1692 BinaryOperator::Plus | BinaryOperator::Multiply if context.plus_is_numeric => {
1694 OperandOrder::Sorted
1695 }
1696 BinaryOperator::Lt => OperandOrder::Mirrored(">"),
1697 BinaryOperator::Gt => OperandOrder::Mirrored("<"),
1698 BinaryOperator::LtEq => OperandOrder::Mirrored(">="),
1699 BinaryOperator::GtEq => OperandOrder::Mirrored("<="),
1700 _ => OperandOrder::Written,
1701 }
1702}
1703
1704fn operator_text(operator: &BinaryOperator) -> Result<&'static str, CanonicalizeError> {
1705 match operator {
1706 BinaryOperator::And => Ok("AND"),
1707 BinaryOperator::Or => Ok("OR"),
1708 BinaryOperator::Eq => Ok("="),
1709 BinaryOperator::Spaceship => Ok("<=>"),
1710 BinaryOperator::NotEq => Ok("!="),
1711 BinaryOperator::Lt => Ok("<"),
1712 BinaryOperator::LtEq => Ok("<="),
1713 BinaryOperator::Gt => Ok(">"),
1714 BinaryOperator::GtEq => Ok(">="),
1715 BinaryOperator::Plus => Ok("+"),
1716 BinaryOperator::Minus => Ok("-"),
1717 BinaryOperator::Multiply => Ok("*"),
1718 BinaryOperator::Divide => Ok("/"),
1719 BinaryOperator::Modulo => Ok("%"),
1720 other => Err(CanonicalizeError::Unsupported(format!(
1721 "Unsupported binary operator: {other}"
1722 ))),
1723 }
1724}
1725
1726fn unary_operator_text(operator: &UnaryOperator) -> Result<&'static str, CanonicalizeError> {
1727 match operator {
1728 UnaryOperator::Not => Ok("NOT"),
1729 UnaryOperator::Plus => Ok("+"),
1730 UnaryOperator::Minus => Ok("-"),
1731 other => Err(CanonicalizeError::Unsupported(format!(
1732 "Unsupported unary operator: {other}"
1733 ))),
1734 }
1735}