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>),
}

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