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, TableAlias, 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 open_list(&mut items, context);
435 let not = if *negated { "NOT " } else { "" };
436 let mut text = format!(
437 "{} {not}IN (",
438 normalize_expr_inner(expr, depth + 1, true, context)?
439 );
440 for (index, item) in items.iter().enumerate() {
441 if index > 0 {
442 text.push_str(", ");
443 }
444 text.push_str(item);
445 }
446 text.push(')');
447 text
448 }
449 Expr::InSubquery {
450 expr,
451 subquery,
452 negated,
453 } => {
454 let not = if *negated { "NOT " } else { "" };
455 format!(
456 "{} {not}IN ({})",
457 normalize_expr_inner(expr, depth + 1, true, context)?,
458 subquery_text(subquery, depth, context)?
459 )
460 }
461 Expr::Exists { subquery, negated } => {
462 let not = if *negated { "NOT " } else { "" };
463 format!("{not}EXISTS ({})", subquery_text(subquery, depth, context)?)
464 }
465 Expr::Subquery(query) => format!("({})", subquery_text(query, depth, context)?),
466 Expr::Between {
467 expr,
468 low,
469 high,
470 negated,
471 } => {
472 let not = if *negated { "NOT " } else { "" };
473 format!(
474 "{} {not}BETWEEN {} AND {}",
475 normalize_expr_inner(expr, depth + 1, true, context)?,
476 normalize_expr_inner(low, depth + 1, true, context)?,
477 normalize_expr_inner(high, depth + 1, true, context)?
478 )
479 }
480 Expr::Like {
481 negated,
482 any,
483 expr,
484 pattern,
485 escape_char,
486 } => {
487 let operator = match_operator(*negated, "LIKE");
488 let escape = escape_char.as_deref();
489 pattern_match_text(expr, &operator, *any, pattern, escape, depth, context)?
490 }
491 Expr::ILike {
492 negated,
493 any,
494 expr,
495 pattern,
496 escape_char,
497 } => {
498 let operator = match_operator(*negated, "ILIKE");
499 let escape = escape_char.as_deref();
500 pattern_match_text(expr, &operator, *any, pattern, escape, depth, context)?
501 }
502 Expr::SimilarTo {
503 negated,
504 expr,
505 pattern,
506 escape_char,
507 } => {
508 let operator = match_operator(*negated, "SIMILAR TO");
509 let escape = escape_char.as_deref();
510 pattern_match_text(expr, &operator, false, pattern, escape, depth, context)?
511 }
512 Expr::RLike {
515 negated,
516 expr,
517 pattern,
518 regexp: _,
519 } => {
520 let operator = match_operator(*negated, "RLIKE");
521 pattern_match_text(expr, &operator, false, pattern, None, depth, context)?
522 }
523 Expr::AnyOp {
525 left,
526 compare_op,
527 right,
528 is_some: _,
529 } => quantified_text(left, compare_op, "ANY", right, depth, context)?,
530 Expr::AllOp {
531 left,
532 compare_op,
533 right,
534 } => quantified_text(left, compare_op, "ALL", right, depth, context)?,
535 Expr::MemberOf(member) => format!(
536 "{} MEMBER OF({})",
537 normalize_expr_inner(&member.value, depth + 1, true, context)?,
538 normalize_expr_inner(&member.array, depth + 1, false, context)?
539 ),
540 Expr::AtTimeZone {
541 timestamp,
542 time_zone,
543 } => format!(
544 "{} AT TIME ZONE {}",
545 normalize_expr_inner(timestamp, depth + 1, true, context)?,
546 normalize_expr_inner(time_zone, depth + 1, true, context)?
547 ),
548 Expr::Collate { expr, collation } => format!(
551 "{} COLLATE {collation}",
552 normalize_expr_inner(expr, depth + 1, true, context)?
553 ),
554 Expr::MatchAgainst {
555 columns,
556 match_value,
557 opt_search_modifier,
558 } => {
559 let columns = columns
560 .iter()
561 .map(|column| object_name_text(column, NamePlace::Operand, context))
562 .collect::<Result<Vec<_>, _>>()?
563 .join(", ");
564 reject_lossy_quoting(&match_value.value)?;
565 let modifier = opt_search_modifier
566 .as_ref()
567 .map_or_else(String::new, |modifier| format!(" {modifier}"));
568 format!(
569 "MATCH ({columns}) AGAINST ({}{modifier})",
570 match_value.value
571 )
572 }
573 Expr::Nested(inner) => normalize_expr_inner(inner, depth + 1, tight_parent, context)?,
574 Expr::Identifier(identifier) => {
575 identifier_text(identifier, context.folding, NamePlace::Operand, context)?
576 }
577 Expr::CompoundIdentifier(parts) => {
578 qualified_name_text(parts.iter(), NamePlace::Operand, context)?
579 }
580 Expr::Value(value) => {
581 reject_lossy_quoting(&value.value)?;
582 format!("{}", value.value)
583 }
584 Expr::JsonAccess { .. } => {
587 return Err(CanonicalizeError::Unsupported(
588 "JSON path access is not supported".to_string(),
589 ));
590 }
591 Expr::Function(function) => function_text(function, depth, context)?,
592 Expr::Cast {
594 kind,
595 expr,
596 data_type,
597 format,
598 } => {
599 if format.is_some() {
600 return Err(CanonicalizeError::Unsupported(
601 "CAST with FORMAT is not supported".to_string(),
602 ));
603 }
604 let keyword = match kind {
605 CastKind::Cast | CastKind::DoubleColon => "CAST",
606 CastKind::TryCast => "TRY_CAST",
607 CastKind::SafeCast => "SAFE_CAST",
608 };
609 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
610 format!("{keyword}({expr} AS {data_type})")
611 }
612 Expr::Convert {
613 is_try,
614 expr,
615 data_type,
616 charset,
617 target_before_value,
618 styles,
619 } => {
620 if *target_before_value || !styles.is_empty() {
622 return Err(CanonicalizeError::Unsupported(
623 "CONVERT with the type first is not supported".to_string(),
624 ));
625 }
626 let target = match (data_type, charset) {
627 (Some(data_type), Some(charset)) => {
628 format!(", {data_type} CHARACTER SET {charset}")
629 }
630 (Some(data_type), None) => format!(", {data_type}"),
631 (None, Some(charset)) => format!(" USING {charset}"),
632 (None, None) => {
633 return Err(CanonicalizeError::Unsupported(
634 "CONVERT without a target is not supported".to_string(),
635 ));
636 }
637 };
638 let prefix = if *is_try { "TRY_" } else { "" };
639 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
640 format!("{prefix}CONVERT({expr}{target})")
641 }
642 Expr::Extract {
643 field,
644 syntax,
645 expr,
646 } => {
647 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
648 match syntax {
649 ExtractSyntax::From => format!("EXTRACT({field} FROM {expr})"),
650 ExtractSyntax::Comma => format!("EXTRACT({field}, {expr})"),
651 }
652 }
653 Expr::Ceil { expr, field } => rounding_text("CEIL", expr, field, depth, context)?,
654 Expr::Floor { expr, field } => rounding_text("FLOOR", expr, field, depth, context)?,
655 Expr::Position { expr, r#in } => format!(
657 "POSITION({} IN {})",
658 normalize_expr_inner(expr, depth + 1, true, context)?,
659 normalize_expr_inner(r#in, depth + 1, false, context)?
660 ),
661 Expr::Substring {
662 expr,
663 substring_from,
664 substring_for,
665 special,
666 shorthand,
667 } => {
668 let name = if *shorthand { "SUBSTR" } else { "SUBSTRING" };
669 let (from, length) = if *special {
670 (", ", ", ")
671 } else {
672 (" FROM ", " FOR ")
673 };
674 let mut text = format!(
675 "{name}({}",
676 normalize_expr_inner(expr, depth + 1, false, context)?
677 );
678 for (separator, part) in [(from, substring_from), (length, substring_for)] {
679 if let Some(part) = part {
680 text.push_str(separator);
681 text.push_str(&normalize_expr_inner(part, depth + 1, false, context)?);
682 }
683 }
684 text.push(')');
685 text
686 }
687 Expr::Trim {
688 expr,
689 trim_where,
690 trim_what,
691 trim_characters,
692 } => {
693 let mut text = String::from("TRIM(");
694 if let Some(trim_where) = trim_where {
695 text.push_str(&format!("{trim_where} "));
696 }
697 if let Some(trim_what) = trim_what {
698 text.push_str(&normalize_expr_inner(trim_what, depth + 1, false, context)?);
699 text.push_str(" FROM ");
700 }
701 text.push_str(&normalize_expr_inner(expr, depth + 1, false, context)?);
702 if let Some(characters) = trim_characters {
703 text.push_str(", ");
704 text.push_str(&list_text(characters, depth, context)?);
705 }
706 text.push(')');
707 text
708 }
709 Expr::Overlay {
710 expr,
711 overlay_what,
712 overlay_from,
713 overlay_for,
714 } => {
715 let mut text = format!(
716 "OVERLAY({} PLACING {} FROM {}",
717 normalize_expr_inner(expr, depth + 1, false, context)?,
718 normalize_expr_inner(overlay_what, depth + 1, false, context)?,
719 normalize_expr_inner(overlay_from, depth + 1, false, context)?
720 );
721 if let Some(overlay_for) = overlay_for {
722 text.push_str(" FOR ");
723 text.push_str(&normalize_expr_inner(
724 overlay_for,
725 depth + 1,
726 false,
727 context,
728 )?);
729 }
730 text.push(')');
731 text
732 }
733 Expr::Case {
734 case_token: _,
735 end_token: _,
736 operand,
737 conditions,
738 else_result,
739 } => {
740 let mut text = String::from("CASE");
741 if let Some(operand) = operand {
742 text.push(' ');
743 text.push_str(&normalize_expr_inner(operand, depth + 1, false, context)?);
744 }
745 for when in conditions {
746 text.push_str(" WHEN ");
747 text.push_str(&normalize_expr_inner(
748 &when.condition,
749 depth + 1,
750 false,
751 context,
752 )?);
753 text.push_str(" THEN ");
754 text.push_str(&normalize_expr_inner(
755 &when.result,
756 depth + 1,
757 false,
758 context,
759 )?);
760 }
761 if let Some(result) = else_result.as_deref().filter(|result| !is_null(result)) {
763 text.push_str(" ELSE ");
764 text.push_str(&normalize_expr_inner(result, depth + 1, false, context)?);
765 }
766 text.push_str(" END");
767 text
768 }
769 Expr::Tuple(items) => format!("({})", list_text(items, depth, context)?),
770 Expr::Array(array) => {
771 let keyword = if array.named { "ARRAY" } else { "" };
772 format!("{keyword}[{}]", list_text(&array.elem, depth, context)?)
773 }
774 Expr::Interval(interval) => interval_text(interval, depth, context)?,
775 Expr::TypedString(typed) => {
776 if typed.uses_odbc_syntax {
777 return Err(CanonicalizeError::Unsupported(
778 "ODBC escape literals are not supported".to_string(),
779 ));
780 }
781 reject_lossy_quoting(&typed.value.value)?;
782 format!("{} {}", typed.data_type, typed.value)
783 }
784 Expr::Prefixed { prefix, value } => format!(
785 "{prefix} {}",
786 normalize_expr_inner(value, depth + 1, true, context)?
787 ),
788 Expr::CompoundFieldAccess { root, access_chain } => {
789 field_access_text(root, access_chain, depth, context)?
790 }
791 Expr::InUnnest { .. } => return Err(refused("IN UNNEST")),
793 Expr::Struct { .. } => return Err(refused("STRUCT")),
794 Expr::Named { .. } => return Err(refused("a named expression")),
795 Expr::Dictionary(_) => return Err(refused("a dictionary literal")),
796 Expr::Map(_) => return Err(refused("a map literal")),
797 Expr::Lambda(_) => return Err(refused("a lambda")),
798 Expr::OuterJoin(_) => return Err(refused("the (+) outer join marker")),
799 Expr::Prior(_) => return Err(refused("PRIOR")),
800 Expr::GroupingSets(_) | Expr::Cube(_) | Expr::Rollup(_) => {
801 return Err(refused("a grouping set"));
802 }
803 Expr::Wildcard(_) | Expr::QualifiedWildcard(..) => return Err(refused("a wildcard")),
804 };
805 Ok(if tight_parent && !encloses_itself(expr) {
806 format!("({text})")
807 } else {
808 text
809 })
810}
811
812#[derive(Clone, Copy)]
814enum OperandOrder {
815 Written,
817 Sorted,
819 Compared,
822 Mirrored(&'static str),
824}
825
826fn infix_text(
829 left: &Expr,
830 operator: &'static str,
831 right: &Expr,
832 order: OperandOrder,
833 depth: usize,
834 context: &Canonicalizer<'_>,
835) -> Result<String, CanonicalizeError> {
836 let swappable =
839 context.collation_is_symmetric || is_literal(left, context) || is_literal(right, context);
840 let left = normalize_expr_inner(left, depth + 1, true, context)?;
841 let right = normalize_expr_inner(right, depth + 1, true, context)?;
842 let (left, operator, right) = match order {
843 OperandOrder::Sorted if left > right => (right, operator, left),
844 OperandOrder::Compared if swappable && left > right => (right, operator, left),
845 OperandOrder::Mirrored(mirrored)
848 if (swappable && left > right) || (left == right && mirrored < operator) =>
849 {
850 (right, mirrored, left)
851 }
852 _ => (left, operator, right),
853 };
854 Ok(format!("({left} {operator} {right})"))
855}
856
857fn postfix_text(
859 operand: &Expr,
860 suffix: &str,
861 depth: usize,
862 context: &Canonicalizer<'_>,
863) -> Result<String, CanonicalizeError> {
864 let operand = normalize_expr_inner(operand, depth + 1, true, context)?;
865 Ok(format!("{operand} {suffix}"))
866}
867
868fn match_operator(negated: bool, operator: &str) -> String {
870 if negated {
871 format!("NOT {operator}")
872 } else {
873 operator.to_string()
874 }
875}
876
877fn pattern_match_text(
880 subject: &Expr,
881 operator: &str,
882 any: bool,
883 pattern: &Expr,
884 escape: Option<&Expr>,
885 depth: usize,
886 context: &Canonicalizer<'_>,
887) -> Result<String, CanonicalizeError> {
888 let subject = normalize_expr_inner(subject, depth + 1, true, context)?;
889 let pattern = if any {
890 format!("ANY {}", argument_text(pattern, depth, context)?)
891 } else {
892 normalize_expr_inner(pattern, depth + 1, true, context)?
893 };
894 let escape = match escape {
895 Some(escape) => format!(
896 " ESCAPE {}",
897 normalize_expr_inner(escape, depth + 1, true, context)?
898 ),
899 None => String::new(),
900 };
901 Ok(format!("{subject} {operator} {pattern}{escape}"))
902}
903
904fn quantified_text(
906 left: &Expr,
907 operator: &BinaryOperator,
908 quantifier: &str,
909 right: &Expr,
910 depth: usize,
911 context: &Canonicalizer<'_>,
912) -> Result<String, CanonicalizeError> {
913 let left = normalize_expr_inner(left, depth + 1, true, context)?;
914 let operator = operator_text(operator)?;
915 let right = argument_text(right, depth, context)?;
916 Ok(format!("{left} {operator} {quantifier}{right}"))
917}
918
919fn argument_text(
922 argument: &Expr,
923 depth: usize,
924 context: &Canonicalizer<'_>,
925) -> Result<String, CanonicalizeError> {
926 let text = normalize_expr_inner(argument, depth + 1, false, context)?;
927 let mut inner = argument;
928 while let Expr::Nested(nested) = inner {
929 inner = nested;
930 }
931 Ok(if matches!(inner, Expr::Tuple(_) | Expr::Subquery(_)) {
932 text
933 } else {
934 format!("({text})")
935 })
936}
937
938fn reject_quantifier_name(name: &ObjectName) -> Result<(), CanonicalizeError> {
943 let quantifier = match name.0.as_slice() {
944 [part] => part.as_ident().is_some_and(|ident| {
945 ident.quote_style.is_none()
946 && ["ANY", "SOME", "ALL"]
947 .iter()
948 .any(|keyword| ident.value.eq_ignore_ascii_case(keyword))
949 }),
950 _ => false,
951 };
952 if quantifier {
953 Err(CanonicalizeError::Unsupported(format!(
954 "Function named {name} reads back as a quantifier"
955 )))
956 } else {
957 Ok(())
958 }
959}
960
961fn qualified_name_text<'i>(
963 parts: impl ExactSizeIterator<Item = &'i Ident>,
964 place: NamePlace,
965 context: &Canonicalizer<'_>,
966) -> Result<String, CanonicalizeError> {
967 let column = parts.len().saturating_sub(1);
968 Ok(parts
969 .enumerate()
970 .map(|(index, part)| {
971 let folding = if index == column {
972 context.folding
973 } else {
974 context.qualifier_folding
975 };
976 identifier_text(part, folding, place, context)
977 })
978 .collect::<Result<Vec<_>, _>>()?
979 .join("."))
980}
981
982fn open_list(items: &mut [String], context: &Canonicalizer<'_>) {
989 if items
990 .first()
991 .is_none_or(|head| !starts_with_query(head, context))
992 {
993 return;
994 }
995 let head = items.iter_mut().position(|item| {
996 if !starts_with_query(item, context) {
997 return true;
998 }
999 let enclosed = format!("({item})");
1000 let can_lead = !starts_with_query(&enclosed, context);
1001 if can_lead {
1002 *item = enclosed;
1003 }
1004 can_lead
1005 });
1006 if let Some(head) = head {
1007 items[..=head].rotate_right(1);
1008 }
1009}
1010
1011fn starts_with_query(text: &str, context: &Canonicalizer<'_>) -> bool {
1015 let first_word = text
1016 .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
1017 .next()
1018 .unwrap_or_default();
1019 (text.starts_with('(') || is_keyword(first_word))
1020 && Parser::new(context.dialect)
1021 .try_with_sql(text)
1022 .and_then(|mut parser| parser.parse_query())
1023 .is_ok()
1024}
1025
1026fn refused(form: &str) -> CanonicalizeError {
1028 CanonicalizeError::Unsupported(format!("{form} is not supported in a predicate"))
1029}
1030
1031fn subquery_text(
1038 query: &Query,
1039 depth: usize,
1040 context: &Canonicalizer<'_>,
1041) -> Result<String, CanonicalizeError> {
1042 let unsupported = |part: &str| {
1043 CanonicalizeError::Unsupported(format!("{part} in a subquery is not supported"))
1044 };
1045 let select = single_table_select(query)?;
1046 if query.order_by.is_some() {
1047 return Err(unsupported("ORDER BY"));
1048 }
1049 if !matches!(&select.group_by, GroupByExpr::Expressions(exprs, modifiers) if exprs.is_empty() && modifiers.is_empty())
1050 {
1051 return Err(unsupported("GROUP BY"));
1052 }
1053 if select.having.is_some() {
1054 return Err(unsupported("HAVING"));
1055 }
1056 let TableFactor::Table {
1057 name,
1058 alias,
1059 args: None,
1060 with_hints,
1061 version: None,
1062 with_ordinality: false,
1063 partitions,
1064 json_path: None,
1065 sample: None,
1066 index_hints,
1067 } = &select.from[0].relation
1068 else {
1069 return Err(unsupported("a table option"));
1070 };
1071 if !with_hints.is_empty() || !partitions.is_empty() || !index_hints.is_empty() {
1072 return Err(unsupported("a table hint"));
1073 }
1074 let items = select
1075 .projection
1076 .iter()
1077 .map(|item| match item {
1078 SelectItem::UnnamedExpr(expr) => normalize_expr_inner(expr, depth + 1, false, context),
1079 SelectItem::Wildcard(options)
1080 if options.opt_ilike.is_none()
1081 && options.opt_exclude.is_none()
1082 && options.opt_except.is_none()
1083 && options.opt_replace.is_none()
1084 && options.opt_rename.is_none()
1085 && options.opt_alias.is_none() =>
1086 {
1087 Ok("*".to_string())
1088 }
1089 _ => Err(unsupported("an alias or qualified wildcard")),
1090 })
1091 .collect::<Result<Vec<_>, _>>()?
1092 .join(", ");
1093 let table = name
1094 .0
1095 .iter()
1096 .map(|part| match part.as_ident() {
1097 Some(part) => {
1098 identifier_text(part, context.qualifier_folding, NamePlace::Operand, context)
1099 }
1100 None => Err(unsupported("a computed table name")),
1101 })
1102 .collect::<Result<Vec<_>, _>>()?
1103 .join(".");
1104 let mut text = format!("SELECT {items} FROM {table}");
1105 if let Some(TableAlias {
1106 explicit: _,
1107 name,
1108 columns,
1109 at,
1110 }) = alias
1111 {
1112 if !columns.is_empty() || at.is_some() {
1113 return Err(unsupported("a table alias with columns or an index"));
1114 }
1115 text.push(' ');
1116 text.push_str(&identifier_text(
1117 name,
1118 context.qualifier_folding,
1119 NamePlace::Operand,
1120 context,
1121 )?);
1122 }
1123 if let Some(filter) = select
1125 .selection
1126 .as_ref()
1127 .filter(|filter| !(context.true_is_reserved && is_true(filter)))
1128 {
1129 text.push_str(" WHERE ");
1130 text.push_str(&normalize_expr_inner(filter, depth + 1, false, context)?);
1131 }
1132 Ok(text)
1133}
1134
1135fn function_text(
1137 function: &Function,
1138 depth: usize,
1139 context: &Canonicalizer<'_>,
1140) -> Result<String, CanonicalizeError> {
1141 let Function {
1142 name,
1143 uses_odbc_syntax,
1144 parameters,
1145 args,
1146 filter,
1147 null_treatment,
1148 over,
1149 within_group,
1150 } = function;
1151 reject_quantifier_name(name)?;
1152 let plain = !*uses_odbc_syntax
1153 && matches!(parameters, FunctionArguments::None)
1154 && filter.is_none()
1155 && null_treatment.is_none()
1156 && over.is_none()
1157 && within_group.is_empty();
1158 let unsupported =
1159 || CanonicalizeError::Unsupported(format!("Call to {name} is not a plain function call"));
1160 if !plain {
1161 return Err(unsupported());
1162 }
1163 let name = object_name_text(name, NamePlace::Function, context)?;
1164 match args {
1165 FunctionArguments::None => Ok(name),
1166 FunctionArguments::Subquery(_) => Err(unsupported()),
1167 FunctionArguments::List(list) => {
1168 if list.duplicate_treatment.is_some() || !list.clauses.is_empty() {
1169 return Err(unsupported());
1170 }
1171 let args = list
1172 .args
1173 .iter()
1174 .map(|arg| match arg {
1175 FunctionArg::Unnamed(FunctionArgExpr::Expr(arg)) => {
1178 normalize_expr_inner(arg, depth + 1, true, context)
1179 }
1180 _ => Err(unsupported()),
1181 })
1182 .collect::<Result<Vec<_>, _>>()?;
1183 Ok(format!("{name}({})", args.join(", ")))
1184 }
1185 }
1186}
1187
1188fn list_text(
1190 items: &[Expr],
1191 depth: usize,
1192 context: &Canonicalizer<'_>,
1193) -> Result<String, CanonicalizeError> {
1194 Ok(items
1195 .iter()
1196 .map(|item| normalize_expr_inner(item, depth + 1, false, context))
1197 .collect::<Result<Vec<_>, _>>()?
1198 .join(", "))
1199}
1200
1201fn rounding_text(
1203 name: &str,
1204 expr: &Expr,
1205 field: &CeilFloorKind,
1206 depth: usize,
1207 context: &Canonicalizer<'_>,
1208) -> Result<String, CanonicalizeError> {
1209 let expr = normalize_expr_inner(expr, depth + 1, false, context)?;
1210 Ok(match field {
1211 CeilFloorKind::DateTimeField(DateTimeField::NoDateTime) => format!("{name}({expr})"),
1212 CeilFloorKind::DateTimeField(field) => format!("{name}({expr} TO {field})"),
1213 CeilFloorKind::Scale(scale) => format!("{name}({expr}, {scale})"),
1214 })
1215}
1216
1217fn interval_text(
1219 interval: &Interval,
1220 depth: usize,
1221 context: &Canonicalizer<'_>,
1222) -> Result<String, CanonicalizeError> {
1223 let Interval {
1224 value,
1225 leading_field,
1226 leading_precision,
1227 last_field,
1228 fractional_seconds_precision,
1229 } = interval;
1230 let mut text = format!(
1231 "INTERVAL {}",
1232 normalize_expr_inner(value, depth + 1, true, context)?
1233 );
1234 if let (Some(DateTimeField::Second), Some(leading), Some(fractional)) = (
1235 leading_field,
1236 leading_precision,
1237 fractional_seconds_precision,
1238 ) {
1239 text.push_str(&format!(" SECOND ({leading}, {fractional})"));
1240 return Ok(text);
1241 }
1242 if let Some(leading_field) = leading_field {
1243 text.push_str(&format!(" {leading_field}"));
1244 }
1245 if let Some(leading_precision) = leading_precision {
1246 text.push_str(&format!(" ({leading_precision})"));
1247 }
1248 if let Some(last_field) = last_field {
1249 text.push_str(&format!(" TO {last_field}"));
1250 }
1251 if let Some(fractional) = fractional_seconds_precision {
1252 text.push_str(&format!(" ({fractional})"));
1253 }
1254 Ok(text)
1255}
1256
1257fn field_access_text(
1263 root: &Expr,
1264 access_chain: &[AccessExpr],
1265 depth: usize,
1266 context: &Canonicalizer<'_>,
1267) -> Result<String, CanonicalizeError> {
1268 let mut text = match root {
1269 Expr::Nested(inner) => format!(
1270 "({})",
1271 normalize_expr_inner(inner, depth + 1, false, context)?
1272 ),
1273 Expr::Identifier(name) => {
1274 identifier_text(name, context.qualifier_folding, NamePlace::Operand, context)?
1275 }
1276 Expr::Value(_) => return Err(literal_access()),
1277 root => normalize_expr_inner(root, depth + 1, true, context)?,
1278 };
1279 for access in access_chain {
1280 match access {
1281 AccessExpr::Dot(Expr::Identifier(name)) => {
1282 text.push('.');
1283 text.push_str(&identifier_text(
1284 name,
1285 context.qualifier_folding,
1286 NamePlace::Operand,
1287 context,
1288 )?);
1289 }
1290 AccessExpr::Dot(Expr::Value(_)) => return Err(literal_access()),
1291 AccessExpr::Dot(field) => {
1292 text.push('.');
1293 text.push_str(&normalize_expr_inner(field, depth + 1, true, context)?);
1294 }
1295 AccessExpr::Subscript(Subscript::Index { index }) => {
1296 text.push('[');
1297 text.push_str(&normalize_expr_inner(index, depth + 1, false, context)?);
1298 text.push(']');
1299 }
1300 AccessExpr::Subscript(Subscript::Slice {
1301 lower_bound,
1302 upper_bound,
1303 stride,
1304 }) => {
1305 text.push('[');
1306 if let Some(lower_bound) = lower_bound {
1307 text.push_str(&normalize_expr_inner(
1308 lower_bound,
1309 depth + 1,
1310 false,
1311 context,
1312 )?);
1313 }
1314 text.push(':');
1315 if let Some(upper_bound) = upper_bound {
1316 text.push_str(&normalize_expr_inner(
1317 upper_bound,
1318 depth + 1,
1319 false,
1320 context,
1321 )?);
1322 }
1323 if let Some(stride) = stride {
1324 text.push(':');
1325 text.push_str(&normalize_expr_inner(stride, depth + 1, false, context)?);
1326 }
1327 text.push(']');
1328 }
1329 }
1330 }
1331 Ok(text)
1332}
1333
1334fn literal_access() -> CanonicalizeError {
1337 CanonicalizeError::Unsupported("A field access on a literal is not supported".to_string())
1338}
1339
1340fn is_literal(expr: &Expr, context: &Canonicalizer<'_>) -> bool {
1345 match expr {
1346 Expr::Nested(inner) => is_literal(inner, context),
1347 Expr::UnaryOp {
1348 op: UnaryOperator::Minus | UnaryOperator::Plus,
1349 expr,
1350 } => is_literal(expr, context),
1351 Expr::Value(value) => context.true_is_reserved || !matches!(value.value, Value::Boolean(_)),
1352 _ => false,
1353 }
1354}
1355
1356fn is_true(expr: &Expr) -> bool {
1358 match expr {
1359 Expr::Nested(inner) => is_true(inner),
1360 Expr::Value(value) => matches!(value.value, Value::Boolean(true)),
1361 _ => false,
1362 }
1363}
1364
1365fn is_null(expr: &Expr) -> bool {
1367 match expr {
1368 Expr::Nested(inner) => is_null(inner),
1369 Expr::Value(value) => matches!(value.value, Value::Null),
1370 _ => false,
1371 }
1372}
1373
1374fn object_name_text(
1376 name: &ObjectName,
1377 place: NamePlace,
1378 context: &Canonicalizer<'_>,
1379) -> Result<String, CanonicalizeError> {
1380 let parts = name
1381 .0
1382 .iter()
1383 .map(|part| {
1384 part.as_ident().ok_or_else(|| {
1385 CanonicalizeError::Unsupported(format!("Computed name part: {part}"))
1386 })
1387 })
1388 .collect::<Result<Vec<_>, _>>()?;
1389 qualified_name_text(parts.into_iter(), place, context)
1390}
1391
1392const fn encloses_itself(expr: &Expr) -> bool {
1400 match expr {
1401 Expr::UnaryOp { op, .. } => !matches!(op, UnaryOperator::Not),
1403 Expr::Exists { negated, .. } => !*negated,
1404 Expr::Identifier(_)
1405 | Expr::CompoundIdentifier(_)
1406 | Expr::CompoundFieldAccess { .. }
1407 | Expr::Value(_)
1408 | Expr::TypedString(_)
1409 | Expr::Prefixed { .. }
1410 | Expr::Nested(_)
1411 | Expr::BinaryOp { .. }
1412 | Expr::IsDistinctFrom(..)
1413 | Expr::IsNotDistinctFrom(..)
1414 | Expr::Function(_)
1415 | Expr::Cast { .. }
1416 | Expr::Convert { .. }
1417 | Expr::Extract { .. }
1418 | Expr::Ceil { .. }
1419 | Expr::Floor { .. }
1420 | Expr::Position { .. }
1421 | Expr::Substring { .. }
1422 | Expr::Trim { .. }
1423 | Expr::Overlay { .. }
1424 | Expr::Case { .. }
1425 | Expr::Subquery(_)
1426 | Expr::Tuple(_)
1427 | Expr::Array(_)
1428 | Expr::MatchAgainst { .. }
1429 | Expr::Wildcard(_)
1430 | Expr::QualifiedWildcard(..) => true,
1431 Expr::IsNull(_)
1432 | Expr::IsNotNull(_)
1433 | Expr::IsTrue(_)
1434 | Expr::IsNotTrue(_)
1435 | Expr::IsFalse(_)
1436 | Expr::IsNotFalse(_)
1437 | Expr::IsUnknown(_)
1438 | Expr::IsNotUnknown(_)
1439 | Expr::IsJson { .. }
1440 | Expr::IsNormalized { .. }
1441 | Expr::InList { .. }
1442 | Expr::InSubquery { .. }
1443 | Expr::InUnnest { .. }
1444 | Expr::Between { .. }
1445 | Expr::Like { .. }
1446 | Expr::ILike { .. }
1447 | Expr::SimilarTo { .. }
1448 | Expr::RLike { .. }
1449 | Expr::AnyOp { .. }
1450 | Expr::AllOp { .. }
1451 | Expr::MemberOf(_)
1452 | Expr::AtTimeZone { .. }
1453 | Expr::Collate { .. }
1454 | Expr::Interval(_)
1455 | Expr::JsonAccess { .. }
1456 | Expr::GroupingSets(_)
1457 | Expr::Cube(_)
1458 | Expr::Rollup(_)
1459 | Expr::Struct { .. }
1460 | Expr::Named { .. }
1461 | Expr::Dictionary(_)
1462 | Expr::Map(_)
1463 | Expr::Lambda(_)
1464 | Expr::OuterJoin(_)
1465 | Expr::Prior(_) => false,
1466 }
1467}
1468
1469#[derive(Clone, Copy, Eq, PartialEq)]
1471enum Folding {
1472 LowerUnquoted,
1474 UpperUnquoted,
1476 CaseInsensitive,
1478 Exact,
1480}
1481
1482pub struct Canonicalizer<'a> {
1487 dialect: &'a dyn Dialect,
1488 folding: Folding,
1489 qualifier_folding: Folding,
1490 true_is_reserved: bool,
1492 plus_is_numeric: bool,
1494 collation_is_symmetric: bool,
1496}
1497
1498impl<'a> Canonicalizer<'a> {
1499 #[must_use]
1501 pub fn new(dialect: &'a dyn Dialect) -> Self {
1502 let folding = if dialect.is::<PostgreSqlDialect>() {
1503 Folding::LowerUnquoted
1504 } else if dialect.is::<AnsiDialect>() {
1505 Folding::UpperUnquoted
1506 } else if dialect.is::<MySqlDialect>() || dialect.is::<SQLiteDialect>() {
1507 Folding::CaseInsensitive
1508 } else {
1509 Folding::Exact
1510 };
1511 let qualifier_folding = if dialect.is::<MySqlDialect>() {
1514 Folding::Exact
1515 } else {
1516 folding
1517 };
1518 let true_is_reserved = dialect.is::<PostgreSqlDialect>()
1519 || dialect.is::<MySqlDialect>()
1520 || dialect.is::<AnsiDialect>();
1521 let plus_is_numeric = true_is_reserved || dialect.is::<SQLiteDialect>();
1522 let collation_is_symmetric = true_is_reserved;
1525 Self {
1526 dialect,
1527 folding,
1528 qualifier_folding,
1529 true_is_reserved,
1530 plus_is_numeric,
1531 collation_is_symmetric,
1532 }
1533 }
1534}
1535
1536#[derive(Clone, Copy, Eq, PartialEq)]
1538enum NamePlace {
1539 Operand,
1541 Function,
1544}
1545
1546fn identifier_text(
1549 identifier: &Ident,
1550 folding: Folding,
1551 place: NamePlace,
1552 context: &Canonicalizer<'_>,
1553) -> Result<String, CanonicalizeError> {
1554 let quoted = identifier.quote_style.is_some();
1555 let folding = if identifier.value.is_ascii() {
1558 folding
1559 } else {
1560 Folding::Exact
1561 };
1562 let resolved = match folding {
1563 Folding::LowerUnquoted if !quoted => identifier.value.to_ascii_lowercase(),
1564 Folding::UpperUnquoted if !quoted => identifier.value.to_ascii_uppercase(),
1565 Folding::CaseInsensitive => identifier.value.to_ascii_lowercase(),
1566 _ => identifier.value.clone(),
1567 };
1568
1569 if bare_spelling_is_faithful(&resolved, quoted, folding, place) {
1570 return Ok(resolved);
1571 }
1572 if !quoted && folding == Folding::Exact {
1575 return Err(CanonicalizeError::Unsupported(format!(
1576 "Name {resolved} reads as a keyword out of its place"
1577 )));
1578 }
1579 let quote = identifier
1582 .quote_style
1583 .or_else(|| context.dialect.identifier_quote_style(&resolved))
1584 .unwrap_or('"');
1585 let mut delimited = String::with_capacity(resolved.len() + 2);
1586 delimited.push(quote);
1587 for character in resolved.chars() {
1588 if character == quote {
1589 delimited.push(quote);
1590 }
1591 delimited.push(character);
1592 }
1593 delimited.push(quote);
1594 Ok(delimited)
1595}
1596
1597fn bare_spelling_is_faithful(name: &str, quoted: bool, folding: Folding, place: NamePlace) -> bool {
1602 if !quoted {
1603 return place == NamePlace::Function || !starts_expression_syntax(name);
1608 }
1609 if place == NamePlace::Function || !is_plain_non_keyword(name) {
1611 return false;
1612 }
1613 match folding {
1614 Folding::LowerUnquoted => !name.bytes().any(|byte| byte.is_ascii_uppercase()),
1617 Folding::UpperUnquoted => !name.bytes().any(|byte| byte.is_ascii_lowercase()),
1618 Folding::CaseInsensitive => true,
1619 Folding::Exact => false,
1620 }
1621}
1622
1623fn is_keyword(name: &str) -> bool {
1625 let upper = || name.bytes().map(|byte| byte.to_ascii_uppercase());
1626 ALL_KEYWORDS
1627 .binary_search_by(|keyword| keyword.bytes().cmp(upper()))
1628 .is_ok()
1629}
1630
1631const EXPRESSION_KEYWORDS: &[&str] = &[
1636 "ARRAY",
1637 "BOX",
1638 "CASE",
1639 "CAST",
1640 "CEIL",
1641 "CIRCLE",
1642 "CONVERT",
1643 "CURRENT_CATALOG",
1644 "CURRENT_DATE",
1645 "CURRENT_TIME",
1646 "CURRENT_TIMESTAMP",
1647 "CURRENT_USER",
1648 "EXISTS",
1649 "EXTRACT",
1650 "FALSE",
1651 "FLOOR",
1652 "INTERVAL",
1653 "LAMBDA",
1654 "LINE",
1655 "LOCALTIME",
1656 "LOCALTIMESTAMP",
1657 "LSEG",
1658 "MAP",
1659 "MATCH",
1660 "NOT",
1661 "NULL",
1662 "OVERLAY",
1663 "PATH",
1664 "POINT",
1665 "POLYGON",
1666 "POSITION",
1667 "PRIOR",
1668 "SAFE_CAST",
1669 "SELECT",
1670 "SESSION_USER",
1671 "STRUCT",
1672 "SUBSTR",
1673 "SUBSTRING",
1674 "TRIM",
1675 "TRUE",
1676 "TRY_CAST",
1677 "TRY_CONVERT",
1678 "USER",
1679 "WITH",
1680];
1681
1682fn starts_expression_syntax(name: &str) -> bool {
1684 EXPRESSION_KEYWORDS
1685 .iter()
1686 .any(|keyword| keyword.eq_ignore_ascii_case(name))
1687}
1688
1689fn is_plain_non_keyword(name: &str) -> bool {
1696 let mut characters = name.chars();
1697 let Some(first) = characters.next() else {
1698 return false;
1699 };
1700 if !(first.is_ascii_alphabetic() || first == '_')
1701 || !characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
1702 {
1703 return false;
1704 }
1705 !is_keyword(name)
1706}
1707
1708fn reject_lossy_quoting(value: &Value) -> Result<(), CanonicalizeError> {
1710 let intact = match value {
1711 Value::SingleQuotedString(text) | Value::NationalStringLiteral(text) => {
1712 quoting_survives_printing(text, '\'')
1713 }
1714 Value::DoubleQuotedString(text) => quoting_survives_printing(text, '"'),
1715 _ => true,
1716 };
1717 if intact {
1718 Ok(())
1719 } else {
1720 Err(CanonicalizeError::NotRoundTrippable(
1721 "string literal".to_string(),
1722 ))
1723 }
1724}
1725
1726fn quoting_survives_printing(text: &str, quote: char) -> bool {
1731 let mut characters = text.chars().peekable();
1732 let mut previous = char::default();
1733 while let Some(character) = characters.next() {
1734 if character == quote
1735 && (previous == '\\' || characters.peek().is_some_and(|next| *next == quote))
1736 {
1737 return false;
1738 }
1739 previous = character;
1740 }
1741 true
1742}
1743
1744fn collect_flat_children<'a>(expr: &'a Expr, operator: &BinaryOperator) -> Vec<&'a Expr> {
1745 match expr {
1746 Expr::Nested(inner) => collect_flat_children(inner, operator),
1747 Expr::BinaryOp { left, op, right } if op == operator => {
1748 let mut children = collect_flat_children(left, operator);
1749 children.extend(collect_flat_children(right, operator));
1750 children
1751 }
1752 _ => vec![expr],
1753 }
1754}
1755
1756const fn operand_order(operator: &BinaryOperator, context: &Canonicalizer<'_>) -> OperandOrder {
1760 match operator {
1761 BinaryOperator::Eq | BinaryOperator::NotEq | BinaryOperator::Spaceship => {
1762 OperandOrder::Compared
1763 }
1764 BinaryOperator::Plus | BinaryOperator::Multiply if context.plus_is_numeric => {
1766 OperandOrder::Sorted
1767 }
1768 BinaryOperator::Lt => OperandOrder::Mirrored(">"),
1769 BinaryOperator::Gt => OperandOrder::Mirrored("<"),
1770 BinaryOperator::LtEq => OperandOrder::Mirrored(">="),
1771 BinaryOperator::GtEq => OperandOrder::Mirrored("<="),
1772 _ => OperandOrder::Written,
1773 }
1774}
1775
1776fn operator_text(operator: &BinaryOperator) -> Result<&'static str, CanonicalizeError> {
1777 match operator {
1778 BinaryOperator::And => Ok("AND"),
1779 BinaryOperator::Or => Ok("OR"),
1780 BinaryOperator::Eq => Ok("="),
1781 BinaryOperator::Spaceship => Ok("<=>"),
1782 BinaryOperator::NotEq => Ok("!="),
1783 BinaryOperator::Lt => Ok("<"),
1784 BinaryOperator::LtEq => Ok("<="),
1785 BinaryOperator::Gt => Ok(">"),
1786 BinaryOperator::GtEq => Ok(">="),
1787 BinaryOperator::Plus => Ok("+"),
1788 BinaryOperator::Minus => Ok("-"),
1789 BinaryOperator::Multiply => Ok("*"),
1790 BinaryOperator::Divide => Ok("/"),
1791 BinaryOperator::Modulo => Ok("%"),
1792 other => Err(CanonicalizeError::Unsupported(format!(
1793 "Unsupported binary operator: {other}"
1794 ))),
1795 }
1796}
1797
1798fn unary_operator_text(operator: &UnaryOperator) -> Result<&'static str, CanonicalizeError> {
1799 match operator {
1800 UnaryOperator::Not => Ok("NOT"),
1801 UnaryOperator::Plus => Ok("+"),
1802 UnaryOperator::Minus => Ok("-"),
1803 other => Err(CanonicalizeError::Unsupported(format!(
1804 "Unsupported unary operator: {other}"
1805 ))),
1806 }
1807}