From 358a9fc9d80cd8c8365f669ee8640c2732d758b9 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 10 Apr 2026 20:31:40 +0200 Subject: [PATCH 1/4] Parse XMLCONCAT as a dedicated expression --- src/ast/mod.rs | 9 +++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7be23f4b3..23fb4b763 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1332,6 +1332,12 @@ pub enum Expr { Lambda(LambdaFunction), /// Checks membership of a value in a JSON array MemberOf(MemberOf), + /// PostgreSQL `XMLCONCAT(xml[, ...])` — concatenates a list of + /// individual XML values to create a single value containing an + /// XML content fragment. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/functions-xml.html#FUNCTIONS-PRODUCING-XML-XMLCONCAT) + XmlConcat(Vec), } impl Expr { @@ -2182,6 +2188,9 @@ impl fmt::Display for Expr { Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), Expr::MemberOf(member_of) => write!(f, "{member_of}"), + Expr::XmlConcat(exprs) => { + write!(f, "XMLCONCAT({})", display_comma_separated(exprs)) + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index bc131dd6d..3c33f3ee9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1651,6 +1651,7 @@ impl Spanned for Expr { Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), + Expr::XmlConcat(exprs) => union_spans(exprs.iter().map(|e| e.span())), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7e29bfa57..67d643294 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2430,9 +2430,47 @@ impl<'a> Parser<'a> { /// Parse a function call expression named by `name` and return it as an `Expr`. pub fn parse_function(&mut self, name: ObjectName) -> Result { + if let Some(expr) = self.maybe_parse_xml_function(&name)? { + return Ok(expr); + } self.parse_function_call(name).map(Expr::Function) } + /// If `name` is a PostgreSQL XML function and the current dialect + /// supports XML expressions, parse it as a dedicated [`Expr`] + /// variant rather than a generic function call. + /// + /// Returns `Ok(None)` when the name is not an XML function or the + /// dialect does not support XML expressions, in which case the + /// caller should fall back to the regular function-call parser. + fn maybe_parse_xml_function( + &mut self, + name: &ObjectName, + ) -> Result, ParserError> { + if !self.dialect.supports_xml_expressions() { + return Ok(None); + } + let [ObjectNamePart::Identifier(ident)] = name.0.as_slice() else { + return Ok(None); + }; + if ident.quote_style.is_some() { + return Ok(None); + } + if ident.value.eq_ignore_ascii_case("xmlconcat") { + return Ok(Some(self.parse_xmlconcat_expr()?)); + } + Ok(None) + } + + /// Parse the argument list of a PostgreSQL `XMLCONCAT` expression: + /// `(expr [, expr]...)`. + fn parse_xmlconcat_expr(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let exprs = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(Expr::XmlConcat(exprs)) + } + fn parse_function_call(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; From ecd9e875bcec6e99a7be44160aa6f7d457a7aec3 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 10 Apr 2026 20:31:55 +0200 Subject: [PATCH 2/4] Test XMLCONCAT parsing across dialects --- tests/sqlparser_common.rs | 17 +++++++++++++++++ tests/sqlparser_postgres.rs | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4db5edeb3..74243e9ee 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18779,3 +18779,20 @@ fn parse_non_pg_dialects_keep_xml_names_as_regular_identifiers() { let dialects = all_dialects_except(|d| d.supports_xml_expressions()); dialects.verified_only_select("SELECT xml FROM t"); } + +#[test] +fn parse_non_pg_dialects_keep_xml_names_as_regular_functions() { + // On dialects that do NOT support XML expressions, `xmlconcat(...)` + // should parse as a plain function call, not as `Expr::XmlConcat`. + let dialects = all_dialects_except(|d| d.supports_xml_expressions()); + for fn_name in ["xmlconcat"] { + let sql = format!("SELECT {fn_name}(1, 2)"); + let select = dialects.verified_only_select(&sql); + match expr_from_projection(&select.projection[0]) { + Expr::Function(func) => { + assert_eq!(func.name.to_string(), fn_name); + } + other => panic!("Expected Expr::Function for {fn_name}, got: {other:?}"), + } + } +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 67850987e..5f97a7879 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3750,6 +3750,31 @@ fn parse_on_commit() { pg_and_generic().verified_stmt("CREATE TEMPORARY TABLE table (COL INT) ON COMMIT DROP"); } +#[test] +fn parse_xmlconcat_expression() { + // XMLCONCAT should parse as a dedicated Expr::XmlConcat variant on + // PostgreSQL and Generic, preserving argument order. + let sql = "SELECT XMLCONCAT('', '', '')"; + let select = pg_and_generic().verified_only_select(sql); + match expr_from_projection(&select.projection[0]) { + Expr::XmlConcat(exprs) => { + assert_eq!(exprs.len(), 3); + let strings: Vec = exprs + .iter() + .map(|e| match e { + Expr::Value(v) => match &v.value { + Value::SingleQuotedString(s) => s.clone(), + other => panic!("Expected SingleQuotedString, got: {other:?}"), + }, + other => panic!("Expected Value, got: {other:?}"), + }) + .collect(); + assert_eq!(strings, vec!["", "", ""]); + } + other => panic!("Expected Expr::XmlConcat, got: {other:?}"), + } +} + #[test] fn parse_xml_typed_string() { // xml '...' should parse as a TypedString on PostgreSQL and Generic From 0621b8cf8ff513064e3be0f4f5d62fdba6cffc85 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 10 Apr 2026 20:33:14 +0200 Subject: [PATCH 3/4] fmt --- src/parser/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 67d643294..113e9c9b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2443,10 +2443,7 @@ impl<'a> Parser<'a> { /// Returns `Ok(None)` when the name is not an XML function or the /// dialect does not support XML expressions, in which case the /// caller should fall back to the regular function-call parser. - fn maybe_parse_xml_function( - &mut self, - name: &ObjectName, - ) -> Result, ParserError> { + fn maybe_parse_xml_function(&mut self, name: &ObjectName) -> Result, ParserError> { if !self.dialect.supports_xml_expressions() { return Ok(None); } From 695a2e2a643f2cccdca17dafdc8cedc45ac87718 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 10 Apr 2026 20:41:59 +0200 Subject: [PATCH 4/4] clippy fix --- tests/sqlparser_common.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 74243e9ee..f20383a2c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18785,14 +18785,11 @@ fn parse_non_pg_dialects_keep_xml_names_as_regular_functions() { // On dialects that do NOT support XML expressions, `xmlconcat(...)` // should parse as a plain function call, not as `Expr::XmlConcat`. let dialects = all_dialects_except(|d| d.supports_xml_expressions()); - for fn_name in ["xmlconcat"] { - let sql = format!("SELECT {fn_name}(1, 2)"); - let select = dialects.verified_only_select(&sql); - match expr_from_projection(&select.projection[0]) { - Expr::Function(func) => { - assert_eq!(func.name.to_string(), fn_name); - } - other => panic!("Expected Expr::Function for {fn_name}, got: {other:?}"), + let select = dialects.verified_only_select("SELECT xmlconcat(1, 2)"); + match expr_from_projection(&select.projection[0]) { + Expr::Function(func) => { + assert_eq!(func.name.to_string(), "xmlconcat"); } + other => panic!("Expected Expr::Function, got: {other:?}"), } }