diff --git a/crates/pgls_pretty_print/src/normalize.rs b/crates/pgls_pretty_print/src/normalize.rs index 1e11005d0..6acc03d94 100644 --- a/crates/pgls_pretty_print/src/normalize.rs +++ b/crates/pgls_pretty_print/src/normalize.rs @@ -18,6 +18,7 @@ use pgls_query::{NodeEnum, NodeMut}; /// the original AST and the AST of reparsed formatted output. pub fn normalize_ast(node: &mut NodeEnum) { clear_location(node); + normalize_bool_expr_associativity(node); normalize_a_indirection(node); normalize_object_with_args(node); normalize_join_expr(node); @@ -27,6 +28,61 @@ pub fn normalize_ast(node: &mut NodeEnum) { normalize_function_body(node); } +/// Flatten nested boolean expressions which PostgreSQL represents differently +/// depending on redundant parentheses. +/// +/// Only same-operator AND and OR expressions are associative. NOT and mixed +/// AND/OR expressions retain their original tree shape because parentheses +/// affect their semantics. +fn normalize_bool_expr_associativity(node: &mut NodeEnum) { + let bool_exprs = node + .iter_mut() + .filter_map(|node| match node { + NodeMut::BoolExpr(bool_expr) => Some(bool_expr), + _ => None, + }) + .collect::>(); + + // Process descendants first. Flattening a parent moves its child nodes, so + // child pointers must not be used after their parent has been normalized. + for bool_expr in bool_exprs.into_iter().rev() { + unsafe { + flatten_bool_expr_args(&mut *bool_expr); + } + } +} + +fn flatten_bool_expr_args(bool_expr: &mut pgls_query::protobuf::BoolExpr) { + use pgls_query::protobuf::BoolExprType; + + if !matches!( + BoolExprType::try_from(bool_expr.boolop), + Ok(BoolExprType::AndExpr | BoolExprType::OrExpr) + ) { + return; + } + + let boolop = bool_expr.boolop; + let mut flattened = Vec::with_capacity(bool_expr.args.len()); + + for mut arg in std::mem::take(&mut bool_expr.args) { + let nested_args = match arg.node.as_mut() { + Some(NodeEnum::BoolExpr(nested)) if nested.boolop == boolop => { + Some(std::mem::take(&mut nested.args)) + } + _ => None, + }; + + if let Some(mut nested_args) = nested_args { + flattened.append(&mut nested_args); + } else { + flattened.push(arg); + } + } + + bool_expr.args = flattened; +} + /// Clear location fields in AST nodes. /// /// Location fields record the byte offset in the original source, diff --git a/crates/pgls_pretty_print/tests/data/multi/bool_expr_associativity.sql b/crates/pgls_pretty_print/tests/data/multi/bool_expr_associativity.sql new file mode 100644 index 000000000..43a7db1d3 --- /dev/null +++ b/crates/pgls_pretty_print/tests/data/multi/bool_expr_associativity.sql @@ -0,0 +1,19 @@ +CREATE TABLE demo_output AS +SELECT + * +FROM + demo +WHERE + ( + demo.accounting_class ~ '^[0-9]+$' + AND ( + demo.accounting_class >= '6000' + AND demo.accounting_class <= '6629' + ) + OR starts_with(demo.accounting_class, '71') + ) + AND demo.changed_at > demo.created_at; + +SELECT * FROM demo WHERE (a OR (b OR c)) AND d; + +SELECT * FROM demo WHERE a AND (b OR c); diff --git a/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_100.snap b/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_100.snap new file mode 100644 index 000000000..d3229f716 --- /dev/null +++ b/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_100.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgls_pretty_print/tests/tests.rs +input_file: crates/pgls_pretty_print/tests/data/multi/bool_expr_associativity.sql +--- +create table demo_output +as + select + * + from + demo + where + (demo.accounting_class ~ '^[0-9]+$' and + demo.accounting_class >= '6000' and + demo.accounting_class <= '6629' or + starts_with(demo.accounting_class, '71')) and + demo.changed_at > demo.created_at; + +select * from demo where (a or b or c) and d; + +select * from demo where a and (b or c); diff --git a/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_80.snap b/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_80.snap new file mode 100644 index 000000000..d3229f716 --- /dev/null +++ b/crates/pgls_pretty_print/tests/snapshots/multi/tests__bool_expr_associativity_80.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgls_pretty_print/tests/tests.rs +input_file: crates/pgls_pretty_print/tests/data/multi/bool_expr_associativity.sql +--- +create table demo_output +as + select + * + from + demo + where + (demo.accounting_class ~ '^[0-9]+$' and + demo.accounting_class >= '6000' and + demo.accounting_class <= '6629' or + starts_with(demo.accounting_class, '71')) and + demo.changed_at > demo.created_at; + +select * from demo where (a or b or c) and d; + +select * from demo where a and (b or c);