This is an automated email from the ASF dual-hosted git repository.

LucaCappelletti94 pushed a commit to branch pg-postfix-factorial-spacing
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 0d001461fa512b64596837ccd196c3110d1c013e
Author: LucaCappelletti94 <[email protected]>
AuthorDate: Mon Sep 21 23:12:33 2026 +0200

    PostgreSQL: Fix spacing between adjacent postfix factorial operators
---
 src/ast/mod.rs              | 28 +++++++++++++++++++++++++++-
 tests/sqlparser_postgres.rs | 18 ++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index dcfd1a96..0755d7fa 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -1956,7 +1956,13 @@ impl fmt::Display for Expr {
                 )
             }
             Expr::UnaryOp { op, expr } => match op {
-                UnaryOperator::PGPostfixFactorial => write!(f, "{expr}{op}"),
+                UnaryOperator::PGPostfixFactorial => {
+                    if ends_with_operator_char(expr) {
+                        write!(f, "{expr} {op}")
+                    } else {
+                        write!(f, "{expr}{op}")
+                    }
+                }
                 UnaryOperator::Not
                 | UnaryOperator::BitwiseNot
                 | UnaryOperator::Hash
@@ -8109,6 +8115,26 @@ fn starts_with_operator_char(expr: &Expr) -> bool {
     first.0.is_some_and(|c| OPERATOR_CHARS.contains(c))
 }
 
+/// Whether `expr` renders with an operator character last. A postfix `!`
+/// must not abut one, since `!!` is the prefix factorial operator in
+/// PostgreSQL.
+fn ends_with_operator_char(expr: &Expr) -> bool {
+    use fmt::Write;
+    struct LastChar(Option<char>);
+    impl fmt::Write for LastChar {
+        fn write_str(&mut self, s: &str) -> fmt::Result {
+            if let Some(c) = s.chars().last() {
+                self.0 = Some(c);
+            }
+            Ok(())
+        }
+    }
+    let mut last = LastChar(None);
+    let _ = write!(last, "{expr}");
+    const OPERATOR_CHARS: &str = "+-*/<>=~!@%#^&|";
+    last.0.is_some_and(|c| OPERATOR_CHARS.contains(c))
+}
+
 /// `FunctionArgOperator::Space` has no token of its own, so the name and the
 /// value are separated by a single space instead.
 fn fmt_named_function_arg(
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index ab2d4b8e..543ff6aa 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -9984,3 +9984,21 @@ fn parse_unary_minus_before_pg_prefix_operators() {
     pg().verified_stmt("SELECT - @ 2");
     pg().one_statement_parses_to("SELECT - #x", "SELECT - # x");
 }
+
+#[test]
+fn parse_postfix_factorial_spacing() {
+    pg().verified_stmt("SELECT a!");
+    pg().verified_stmt("SELECT 5!");
+    pg().verified_stmt("SELECT (a!)!");
+    pg().verified_stmt("SELECT a! !");
+    pg().verified_stmt("SELECT a! ! !");
+    pg().verified_stmt("SELECT a! ! % 2");
+    pg().one_statement_parses_to("SELECT a! !%2", "SELECT a! ! % 2");
+    pg().one_statement_parses_to("SELECT -a, +b, a! !%2, a", "SELECT -a, +b, 
a! ! % 2, a");
+
+    let err = pg().parse_sql_statements("SELECT a!!").unwrap_err();
+    assert_eq!(
+        ParserError::ParserError("Expected: end of statement, found: 
!!".to_string()),
+        err
+    );
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to