1use pg_query::NodeEnum;
17use pg_query::protobuf::{self, CaseWhen, Node, ResTarget};
18
19use crate::ir::column_type::ColumnType;
20use crate::ir::default_expr::NormalizedExpr;
21use crate::parse::error::{ParseError, SourceLocation};
22
23pub fn from_pg_node(
28 node: &NodeEnum,
29 target_type: Option<&ColumnType>,
30 location: &SourceLocation,
31) -> Result<NormalizedExpr, ParseError> {
32 let normalized = strip_redundant_cast(node.clone(), target_type);
33 let normalized = strip_redundant_string_casts(normalized);
38 let raw = deparse_expr(&normalized).map_err(|e| ParseError::Structural {
39 location: location.clone(),
40 message: format!("could not deparse expression: {e}"),
41 })?;
42 let canonical_text = lowercase_keywords(&raw);
43 Ok(NormalizedExpr::from_text(canonical_text))
44}
45
46fn strip_redundant_cast(node: NodeEnum, target_type: Option<&ColumnType>) -> NodeEnum {
49 let Some(target) = target_type else {
50 return node;
51 };
52 let NodeEnum::TypeCast(cast) = &node else {
53 return node;
54 };
55 let Some(type_name) = cast.type_name.as_ref() else {
56 return node;
57 };
58 let Some(type_str) = render_type_name(type_name) else {
59 return node;
60 };
61 let Ok(parsed) = ColumnType::parse_from_pg_type_string(&type_str) else {
62 return node;
63 };
64 if &parsed != target {
65 return node;
66 }
67 cast.arg
69 .as_ref()
70 .and_then(|inner| inner.node.as_ref())
71 .cloned()
72 .unwrap_or(node)
73}
74
75#[allow(clippy::too_many_lines)]
92fn strip_redundant_string_casts(node: NodeEnum) -> NodeEnum {
93 match node {
94 NodeEnum::TypeCast(cast) => {
95 let is_string_literal =
97 cast.arg
98 .as_ref()
99 .and_then(|a| a.node.as_ref())
100 .is_some_and(|n| {
101 matches!(
102 n,
103 NodeEnum::AConst(c) if matches!(
104 c.val,
105 Some(protobuf::a_const::Val::Sval(_))
106 )
107 )
108 });
109
110 let is_text_family_no_typmod = cast
111 .type_name
112 .as_ref()
113 .and_then(|tn| render_type_name(tn).map(|s| (s, tn.typmods.is_empty())))
114 .is_some_and(|(type_str, no_typmod)| {
115 no_typmod
116 && matches!(
117 type_str.as_str(),
118 "text" | "bpchar" | "varchar" | "character varying" | "character"
119 )
120 });
121
122 if is_string_literal && is_text_family_no_typmod {
123 return cast.arg.and_then(|inner| inner.node).unwrap_or_else(|| {
126 NodeEnum::TypeCast(Box::new(protobuf::TypeCast {
127 arg: None,
128 type_name: None,
129 location: -1,
130 }))
131 });
132 }
133
134 let arg = cast.arg.map(|boxed_node| {
136 let inner = boxed_node.node.map(strip_redundant_string_casts);
137 Box::new(Node { node: inner })
138 });
139 NodeEnum::TypeCast(Box::new(protobuf::TypeCast {
140 arg,
141 type_name: cast.type_name,
142 location: cast.location,
143 }))
144 }
145
146 NodeEnum::AExpr(mut expr) => {
147 expr.lexpr = expr.lexpr.map(|boxed| {
148 let inner = boxed.node.map(strip_redundant_string_casts);
149 Box::new(Node { node: inner })
150 });
151 expr.rexpr = expr.rexpr.map(|boxed| {
152 let inner = boxed.node.map(strip_redundant_string_casts);
153 Box::new(Node { node: inner })
154 });
155 NodeEnum::AExpr(expr)
156 }
157
158 NodeEnum::BoolExpr(mut expr) => {
159 expr.args = expr
160 .args
161 .into_iter()
162 .map(|n| Node {
163 node: n.node.map(strip_redundant_string_casts),
164 })
165 .collect();
166 NodeEnum::BoolExpr(expr)
167 }
168
169 NodeEnum::FuncCall(mut call) => {
170 call.args = call
171 .args
172 .into_iter()
173 .map(|n| Node {
174 node: n.node.map(strip_redundant_string_casts),
175 })
176 .collect();
177 NodeEnum::FuncCall(call)
178 }
179
180 NodeEnum::NullTest(mut nt) => {
181 nt.arg = nt.arg.map(|boxed| {
182 let inner = boxed.node.map(strip_redundant_string_casts);
183 Box::new(Node { node: inner })
184 });
185 NodeEnum::NullTest(nt)
186 }
187
188 NodeEnum::BooleanTest(mut bt) => {
189 bt.arg = bt.arg.map(|boxed| {
190 let inner = boxed.node.map(strip_redundant_string_casts);
191 Box::new(Node { node: inner })
192 });
193 NodeEnum::BooleanTest(bt)
194 }
195
196 NodeEnum::CaseExpr(mut ce) => {
197 ce.arg = ce.arg.map(|boxed| {
198 let inner = boxed.node.map(strip_redundant_string_casts);
199 Box::new(Node { node: inner })
200 });
201 ce.args = ce
202 .args
203 .into_iter()
204 .map(|n| {
205 let node = n.node.map(|inner| {
206 if let NodeEnum::CaseWhen(mut cw) = inner {
208 cw.expr = cw.expr.map(|boxed| {
209 let e = boxed.node.map(strip_redundant_string_casts);
210 Box::new(Node { node: e })
211 });
212 cw.result = cw.result.map(|boxed| {
213 let r = boxed.node.map(strip_redundant_string_casts);
214 Box::new(Node { node: r })
215 });
216 NodeEnum::CaseWhen(Box::new(CaseWhen {
217 xpr: cw.xpr,
218 expr: cw.expr,
219 result: cw.result,
220 location: cw.location,
221 }))
222 } else {
223 strip_redundant_string_casts(inner)
224 }
225 });
226 Node { node }
227 })
228 .collect();
229 ce.defresult = ce.defresult.map(|boxed| {
230 let inner = boxed.node.map(strip_redundant_string_casts);
231 Box::new(Node { node: inner })
232 });
233 NodeEnum::CaseExpr(ce)
234 }
235
236 NodeEnum::CoalesceExpr(mut ce) => {
237 ce.args = ce
238 .args
239 .into_iter()
240 .map(|n| Node {
241 node: n.node.map(strip_redundant_string_casts),
242 })
243 .collect();
244 NodeEnum::CoalesceExpr(ce)
245 }
246
247 NodeEnum::MinMaxExpr(mut mm) => {
248 mm.args = mm
249 .args
250 .into_iter()
251 .map(|n| Node {
252 node: n.node.map(strip_redundant_string_casts),
253 })
254 .collect();
255 NodeEnum::MinMaxExpr(mm)
256 }
257
258 NodeEnum::List(mut list) => {
259 list.items = list
260 .items
261 .into_iter()
262 .map(|n| Node {
263 node: n.node.map(strip_redundant_string_casts),
264 })
265 .collect();
266 NodeEnum::List(list)
267 }
268
269 NodeEnum::RowExpr(mut re) => {
270 re.args = re
271 .args
272 .into_iter()
273 .map(|n| Node {
274 node: n.node.map(strip_redundant_string_casts),
275 })
276 .collect();
277 NodeEnum::RowExpr(re)
278 }
279
280 NodeEnum::AArrayExpr(mut ae) => {
281 ae.elements = ae
282 .elements
283 .into_iter()
284 .map(|n| Node {
285 node: n.node.map(strip_redundant_string_casts),
286 })
287 .collect();
288 NodeEnum::AArrayExpr(ae)
289 }
290
291 other => other,
293 }
294}
295
296fn render_type_name(type_name: &protobuf::TypeName) -> Option<String> {
299 let mut parts = Vec::with_capacity(type_name.names.len());
300 for n in &type_name.names {
301 let Some(NodeEnum::String(s)) = &n.node else {
302 return None;
303 };
304 parts.push(s.sval.clone());
305 }
306 if parts.is_empty() {
307 return None;
308 }
309 Some(parts.last().cloned().unwrap_or_default())
313}
314
315fn deparse_expr(node: &NodeEnum) -> Result<String, pg_query::Error> {
324 let mut scaffold = pg_query::parse("SELECT 1")?.protobuf;
325 let raw = scaffold
326 .stmts
327 .get_mut(0)
328 .ok_or_else(|| pg_query::Error::Parse("scaffold has no stmts".into()))?;
329 let select_node = raw
330 .stmt
331 .as_mut()
332 .ok_or_else(|| pg_query::Error::Parse("scaffold stmt is None".into()))?
333 .node
334 .as_mut()
335 .ok_or_else(|| pg_query::Error::Parse("scaffold node is None".into()))?;
336 let NodeEnum::SelectStmt(select) = select_node else {
337 return Err(pg_query::Error::Parse(
338 "scaffold parse did not yield SelectStmt".into(),
339 ));
340 };
341 select.target_list = vec![Node {
342 node: Some(NodeEnum::ResTarget(Box::new(ResTarget {
343 name: String::new(),
344 indirection: vec![],
345 val: Some(Box::new(Node {
346 node: Some(node.clone()),
347 })),
348 location: -1,
349 }))),
350 }];
351 let s = pg_query::deparse(&scaffold)?;
352 Ok(s.trim_start_matches("SELECT ").to_string())
353}
354
355const RESERVED_FUNC_KEYWORDS: &[&str] = &[
360 "AND", "OR", "NOT", "NULL", "TRUE", "FALSE", "IS", "IN", "LIKE", "BETWEEN", "CASE", "WHEN",
361 "THEN", "ELSE", "END", "CAST", "AS", "DISTINCT", "FROM", "WHERE", "ORDER", "BY", "GROUP",
362 "HAVING", "LIMIT", "OFFSET", "ASC", "DESC", "NULLS", "FIRST", "LAST", "WITH", "USING",
363 "COLLATE",
364];
365
366fn lowercase_keywords(s: &str) -> String {
369 let mut out = String::with_capacity(s.len());
370 let mut chars = s.chars().peekable();
371 while let Some(c) = chars.next() {
372 if c == '\'' {
373 out.push(c);
375 while let Some(&n) = chars.peek() {
376 chars.next();
377 out.push(n);
378 if n == '\'' {
379 if chars.peek() == Some(&'\'') {
380 if let Some(escaped) = chars.next() {
382 out.push(escaped);
383 }
384 continue;
385 }
386 break;
387 }
388 }
389 continue;
390 }
391 if c == '"' {
392 out.push(c);
393 for n in chars.by_ref() {
394 out.push(n);
395 if n == '"' {
396 break;
397 }
398 }
399 continue;
400 }
401 if c.is_ascii_alphabetic() || c == '_' {
402 let mut word = String::from(c);
403 while let Some(&n) = chars.peek() {
404 if n.is_ascii_alphanumeric() || n == '_' {
405 word.push(n);
406 chars.next();
407 } else {
408 break;
409 }
410 }
411 let upper = word.to_ascii_uppercase();
412 if RESERVED_FUNC_KEYWORDS.contains(&upper.as_str()) {
413 out.push_str(&word.to_ascii_lowercase());
414 } else {
415 out.push_str(&word);
416 }
417 continue;
418 }
419 out.push(c);
420 }
421 out
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use std::path::PathBuf;
428
429 fn loc() -> SourceLocation {
430 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
431 }
432
433 fn parse_expr(sql: &str) -> NodeEnum {
435 let full = format!("SELECT {sql}");
436 let parsed = pg_query::parse(&full).expect("expression parses");
437 let stmt = parsed.protobuf.stmts.into_iter().next().expect("one stmt");
438 let select = stmt.stmt.expect("stmt").node.expect("node");
439 let NodeEnum::SelectStmt(s) = select else {
440 panic!("expected SelectStmt, got {select:?}");
441 };
442 let target = s
443 .target_list
444 .into_iter()
445 .next()
446 .expect("target")
447 .node
448 .expect("target node");
449 let NodeEnum::ResTarget(rt) = target else {
450 panic!("expected ResTarget");
451 };
452 rt.val
453 .expect("val")
454 .node
455 .expect("inner node of ResTarget val")
456 }
457
458 #[test]
459 fn cast_to_target_integer_strips() {
460 let node = parse_expr("42 :: integer");
461 let n = from_pg_node(&node, Some(&ColumnType::Integer), &loc()).expect("normalizes");
462 assert_eq!(n.canonical_text, "42");
463 }
464
465 #[test]
466 fn cast_to_target_text_strips() {
467 let node = parse_expr("'foo' :: text");
468 let n = from_pg_node(&node, Some(&ColumnType::Text), &loc()).expect("normalizes");
469 assert_eq!(n.canonical_text, "'foo'");
470 }
471
472 #[test]
473 fn cast_to_other_type_kept() {
474 let node = parse_expr("42 :: integer");
475 let n = from_pg_node(&node, Some(&ColumnType::Text), &loc()).expect("normalizes");
476 assert!(
478 n.canonical_text.contains("integer") || n.canonical_text.contains("::"),
479 "got: {}",
480 n.canonical_text
481 );
482 }
483
484 #[test]
485 fn cast_with_no_target_kept() {
486 let node = parse_expr("42 :: integer");
487 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
488 assert!(
490 n.canonical_text.contains("integer") || n.canonical_text.contains("::"),
491 "got: {}",
492 n.canonical_text
493 );
494 }
495
496 #[test]
497 fn keywords_lowercased() {
498 let node = parse_expr("LOWER('FOO')");
501 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
502 assert!(
503 !n.canonical_text.contains("AS") && !n.canonical_text.contains("DISTINCT"),
504 "got: {}",
505 n.canonical_text
506 );
507 }
508
509 #[test]
510 fn lowercase_helper_preserves_quoted_strings() {
511 assert_eq!(lowercase_keywords("'AND OR'"), "'AND OR'");
512 assert_eq!(lowercase_keywords("foo 'AND' OR"), "foo 'AND' or");
513 assert_eq!(lowercase_keywords("AND OR"), "and or");
514 }
515
516 #[test]
517 fn lowercase_helper_handles_doubled_quote() {
518 assert_eq!(lowercase_keywords("'a''b'"), "'a''b'");
519 }
520
521 #[test]
522 fn lowercase_helper_preserves_quoted_identifiers() {
523 assert_eq!(lowercase_keywords("\"AND\""), "\"AND\"");
524 }
525
526 #[test]
527 fn hash_matches_text() {
528 let n = NormalizedExpr::from_text("now()");
529 let expected: [u8; 32] = blake3::hash(b"now()").into();
530 assert_eq!(n.ast_hash, expected);
531 }
532
533 #[test]
534 fn equivalent_casts_hash_equal() {
535 let a = parse_expr("42 :: integer");
536 let na = from_pg_node(&a, Some(&ColumnType::Integer), &loc()).unwrap();
537 let nb = NormalizedExpr::from_text("42");
538 assert_eq!(na.ast_hash, nb.ast_hash);
539 }
540
541 #[test]
546 fn nested_string_text_cast_stripped() {
547 let with_cast = parse_expr("email <> ''::text");
548 let without_cast = parse_expr("email <> ''");
549 let n_cast = from_pg_node(&with_cast, None, &loc()).expect("normalizes with cast");
550 let n_plain = from_pg_node(&without_cast, None, &loc()).expect("normalizes plain");
551 assert_eq!(
552 n_cast.canonical_text, n_plain.canonical_text,
553 "canonical texts differ: with_cast={:?} plain={:?}",
554 n_cast.canonical_text, n_plain.canonical_text,
555 );
556 }
557
558 #[test]
560 fn and_of_two_string_casts_stripped() {
561 let with_cast = parse_expr("a <> ''::text and b <> 'x'::text");
562 let without_cast = parse_expr("a <> '' and b <> 'x'");
563 let n_cast = from_pg_node(&with_cast, None, &loc()).expect("normalizes with cast");
564 let n_plain = from_pg_node(&without_cast, None, &loc()).expect("normalizes plain");
565 assert_eq!(
566 n_cast.canonical_text, n_plain.canonical_text,
567 "canonical texts differ: with_cast={:?} plain={:?}",
568 n_cast.canonical_text, n_plain.canonical_text,
569 );
570 }
571
572 #[test]
574 fn func_arg_string_cast_stripped() {
575 let with_cast = parse_expr("lower(name) <> 'x'::text");
576 let without_cast = parse_expr("lower(name) <> 'x'");
577 let n_cast = from_pg_node(&with_cast, None, &loc()).expect("normalizes with cast");
578 let n_plain = from_pg_node(&without_cast, None, &loc()).expect("normalizes plain");
579 assert_eq!(
580 n_cast.canonical_text, n_plain.canonical_text,
581 "canonical texts differ: with_cast={:?} plain={:?}",
582 n_cast.canonical_text, n_plain.canonical_text,
583 );
584 }
585
586 #[test]
589 fn cast_string_to_int_kept() {
590 let node = parse_expr("'5'::int");
591 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
592 assert_eq!(n.canonical_text, "'5'::int");
593 }
594
595 #[test]
597 fn cast_string_to_date_kept() {
598 let node = parse_expr("'2024-01-01'::date");
599 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
600 assert_eq!(n.canonical_text, "'2024-01-01'::date");
601 }
602
603 #[test]
605 fn cast_string_to_varchar_with_typmod_kept() {
606 let node = parse_expr("'x'::varchar(5)");
607 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
608 assert_eq!(n.canonical_text, "'x'::varchar(5)");
609 }
610
611 #[test]
618 fn cast_string_to_bare_character_kept_or_documented() {
619 let node = parse_expr("'x'::character");
620 let n = from_pg_node(&node, None, &loc()).expect("normalizes");
621 assert_eq!(n.canonical_text, "'x'::char(1)");
622 }
623}