Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expr>),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xmlconcat looks like a regular function syntax wise, is the parser unable to parse it today?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be parsed as a function, I added it to the enum nevertheless for easy of use, for the same reason that other functions like CEIL for instance appear there. Of course, open to discussion whether it is worth it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see, I think we should keep the parsing as is that it gets represented as a regular function vs making it a special case

}

impl Expr {
Expand Down Expand Up @@ -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))
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())),
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2430,9 +2430,44 @@ 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<Expr, ParserError> {
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<Option<Expr>, 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<Expr, ParserError> {
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<Function, ParserError> {
self.expect_token(&Token::LParen)?;

Expand Down
14 changes: 14 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18779,3 +18779,17 @@ 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());
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:?}"),
}
}
25 changes: 25 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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('<a/>', '<b/>', '<c/>')";
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<String> = 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!["<a/>", "<b/>", "<c/>"]);
}
other => panic!("Expected Expr::XmlConcat, got: {other:?}"),
}
}

#[test]
fn parse_xml_typed_string() {
// xml '...' should parse as a TypedString on PostgreSQL and Generic
Expand Down
Loading