From 31154372e90fd81ca1f7c24a3b394c17eccd9bb0 Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Mon, 14 Sep 2026 15:44:20 +0300 Subject: [PATCH] Postgres: Support XML functions (XMLELEMENT, XMLPI, XMLROOT, XMLSERIALIZE, XMLEXISTS) --- src/ast/mod.rs | 12 ++++ src/ast/query.rs | 26 +++++-- src/ast/spans.rs | 3 + src/keywords.rs | 1 + src/parser/mod.rs | 135 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_common.rs | 30 ++++++++ tests/sqlparser_postgres.rs | 61 ++++++++++++++++ 7 files changed, 255 insertions(+), 13 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 20058b83ab..91b7653ca0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8378,6 +8378,14 @@ pub enum FunctionArgumentClause { /// /// [`JSON_OBJECT`](https://www.postgresql.org/docs/current/functions-json.html#:~:text=json_object) JsonReturningClause(JsonReturningClause), + /// The `PASSING` clause, used e.g. in `XMLEXISTS`. + Passing(XmlPassingClause), + /// The `AS ` clause, used e.g. in `XMLSERIALIZE`. + As(DataType), + /// The `INDENT` clause in `XMLSERIALIZE`. + Indent, + /// The `NO INDENT` clause in `XMLSERIALIZE`. + NoIndent, } impl fmt::Display for FunctionArgumentClause { @@ -8398,6 +8406,10 @@ impl fmt::Display for FunctionArgumentClause { FunctionArgumentClause::JsonReturningClause(returning_clause) => { write!(f, "{returning_clause}") } + FunctionArgumentClause::Passing(passing) => write!(f, "{passing}"), + FunctionArgumentClause::As(data_type) => write!(f, "AS {data_type}"), + FunctionArgumentClause::Indent => write!(f, "INDENT"), + FunctionArgumentClause::NoIndent => write!(f, "NO INDENT"), } } } diff --git a/src/ast/query.rs b/src/ast/query.rs index 296e4e8ca6..93a7025b60 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2497,11 +2497,19 @@ impl fmt::Display for TableFactor { display_comma_separated(namespaces) )?; } - write!( - f, - "{row_expression}{passing} COLUMNS {columns})", - columns = display_comma_separated(columns) - )?; + if !passing.arguments.is_empty() { + write!( + f, + "{row_expression} {passing} COLUMNS {columns})", + columns = display_comma_separated(columns) + )?; + } else { + write!( + f, + "{row_expression} COLUMNS {columns})", + columns = display_comma_separated(columns) + )?; + } if let Some(alias) = alias { write!(f, " {alias}")?; } @@ -4338,11 +4346,15 @@ pub struct XmlPassingArgument { pub alias: Option, /// `true` if `BY VALUE` is specified for the argument. pub by_value: bool, + /// `true` if `BY REF` is specified for the argument. + pub by_ref: bool, } impl fmt::Display for XmlPassingArgument { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.by_value { + if self.by_ref { + write!(f, "BY REF ")?; + } else if self.by_value { write!(f, "BY VALUE ")?; } write!(f, "{}", self.expr)?; @@ -4366,7 +4378,7 @@ pub struct XmlPassingClause { impl fmt::Display for XmlPassingClause { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.arguments.is_empty() { - write!(f, " PASSING {}", display_comma_separated(&self.arguments))?; + write!(f, "PASSING {}", display_comma_separated(&self.arguments))?; } Ok(()) } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7acbd7d0b4..cb58ca79e9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1821,6 +1821,9 @@ impl Spanned for FunctionArgumentClause { FunctionArgumentClause::Separator(value) => value.span(), FunctionArgumentClause::JsonNullClause(_) => Span::empty(), FunctionArgumentClause::JsonReturningClause(_) => Span::empty(), + FunctionArgumentClause::Passing(_) => Span::empty(), + FunctionArgumentClause::As(_) => Span::empty(), + FunctionArgumentClause::Indent | FunctionArgumentClause::NoIndent => Span::empty(), } } } diff --git a/src/keywords.rs b/src/keywords.rs index 7b91eecba0..0d69a303cd 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -515,6 +515,7 @@ define_keywords!( INCLUDING, INCREMENT, INCREMENTAL, + INDENT, INDEX, INDICATOR, INHERIT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 15f135fffa..5653ad1aa4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2546,6 +2546,102 @@ impl<'a> Parser<'a> { }) } + /// Parse the argument list of `XMLELEMENT(NAME name [, XMLATTRIBUTES(...) ] [, content [, ...]])` + /// or `XMLPI(NAME name [, content ])`. + fn parse_xmlelement_or_xmlpi_argument_list( + &mut self, + ) -> Result { + let name_kw = self.parse_identifier()?; + let name_val = self.parse_identifier()?; + let mut args = vec![FunctionArg::Named { + name: name_kw, + arg: FunctionArgExpr::Expr(Expr::Identifier(name_val)), + operator: FunctionArgOperator::Space, + }]; + if self.consume_token(&Token::Comma) { + args.extend(self.parse_comma_separated(Parser::parse_function_args)?); + } + self.expect_token(&Token::RParen)?; + Ok(FunctionArgumentList { + duplicate_treatment: None, + args, + clauses: vec![], + }) + } + + /// Parse the argument list of `XMLROOT(xml, VERSION {text | NO VALUE} [, STANDALONE {YES | NO | NO VALUE} ])`. + fn parse_xmlroot_argument_list(&mut self) -> Result { + let xml_expr = self.parse_expr()?; + let mut args = vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(xml_expr))]; + self.expect_token(&Token::Comma)?; + let version_kw = self.parse_identifier()?; + let version_val = if self.parse_keywords(&[Keyword::NO, Keyword::VALUE]) { + Expr::Identifier(Ident::new("NO VALUE")) + } else { + self.parse_expr()? + }; + args.push(FunctionArg::Named { + name: version_kw, + arg: FunctionArgExpr::Expr(version_val), + operator: FunctionArgOperator::Space, + }); + if self.consume_token(&Token::Comma) { + let standalone_kw = self.parse_identifier()?; + let standalone_val = if self.parse_keywords(&[Keyword::NO, Keyword::VALUE]) { + Expr::Identifier(Ident::new("NO VALUE")) + } else { + Expr::Identifier(self.parse_identifier()?) + }; + args.push(FunctionArg::Named { + name: standalone_kw, + arg: FunctionArgExpr::Expr(standalone_val), + operator: FunctionArgOperator::Space, + }); + } + self.expect_token(&Token::RParen)?; + Ok(FunctionArgumentList { + duplicate_treatment: None, + args, + clauses: vec![], + }) + } + + /// Parse the argument list of `XMLSERIALIZE({ DOCUMENT | CONTENT } value AS type [ [ NO ] INDENT ])`. + fn parse_xmlserialize_argument_list(&mut self) -> Result { + let mode = self.parse_identifier()?; + let value = self.parse_expr()?; + self.expect_keyword_is(Keyword::AS)?; + let data_type = self.parse_data_type()?; + let mut clauses = vec![FunctionArgumentClause::As(data_type)]; + if self.parse_keywords(&[Keyword::NO, Keyword::INDENT]) { + clauses.push(FunctionArgumentClause::NoIndent); + } else if self.parse_keyword(Keyword::INDENT) { + clauses.push(FunctionArgumentClause::Indent); + } + self.expect_token(&Token::RParen)?; + Ok(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Named { + name: mode, + arg: FunctionArgExpr::Expr(value), + operator: FunctionArgOperator::Space, + }], + clauses, + }) + } + + /// Parse the argument list of `XMLEXISTS(text PASSING [BY {REF|VALUE}] xml [BY {REF|VALUE}])`. + fn parse_xmlexists_argument_list(&mut self) -> Result { + let xpath_expr = self.parse_expr()?; + let passing = self.parse_xml_passing_clause()?; + self.expect_token(&Token::RParen)?; + Ok(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(xpath_expr))], + clauses: vec![FunctionArgumentClause::Passing(passing)], + }) + } + /// Parse a function call expression named by `name` and return it as an `Expr`. pub fn parse_function(&mut self, name: ObjectName) -> Result { self.parse_function_call(name).map(Expr::Function) @@ -2571,10 +2667,22 @@ impl<'a> Parser<'a> { }); } - let mut args = if self.dialect.supports_xml_expressions() - && Self::is_simple_unquoted_object_name(&name, "xmlparse") - { - self.parse_xmlparse_argument_list()? + let mut args = if self.dialect.supports_xml_expressions() { + if Self::is_simple_unquoted_object_name(&name, "xmlparse") { + self.parse_xmlparse_argument_list()? + } else if Self::is_simple_unquoted_object_name(&name, "xmlelement") + || Self::is_simple_unquoted_object_name(&name, "xmlpi") + { + self.parse_xmlelement_or_xmlpi_argument_list()? + } else if Self::is_simple_unquoted_object_name(&name, "xmlroot") { + self.parse_xmlroot_argument_list()? + } else if Self::is_simple_unquoted_object_name(&name, "xmlserialize") { + self.parse_xmlserialize_argument_list()? + } else if Self::is_simple_unquoted_object_name(&name, "xmlexists") { + self.parse_xmlexists_argument_list()? + } else { + self.parse_function_argument_list()? + } } else { self.parse_function_argument_list()? }; @@ -17088,18 +17196,33 @@ impl<'a> Parser<'a> { let mut arguments = vec![]; if self.parse_keyword(Keyword::PASSING) { loop { - let by_value = - self.parse_keyword(Keyword::BY) && self.expect_keyword(Keyword::VALUE).is_ok(); + let mut by_ref = false; + let mut by_value = false; + if self.parse_keyword(Keyword::BY) { + if self.parse_keyword(Keyword::REF) { + by_ref = true; + } else if self.parse_keyword(Keyword::VALUE) { + by_value = true; + } + } let expr = self.parse_expr()?; let alias = if self.parse_keyword(Keyword::AS) { Some(self.parse_identifier()?) } else { None }; + if self.parse_keyword(Keyword::BY) { + if self.parse_keyword(Keyword::REF) { + by_ref = true; + } else if self.parse_keyword(Keyword::VALUE) { + by_value = true; + } + } arguments.push(XmlPassingArgument { expr, alias, by_value, + by_ref, }); if !self.consume_token(&Token::Comma) { break; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2de6062b28..6edeec468e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19837,6 +19837,36 @@ fn parse_xmlparse() { .is_err()); } +#[test] +fn parse_xml_functions() { + let dialects = all_dialects_where(|d| d.supports_xml_expressions()); + + dialects.verified_stmt("SELECT XMLELEMENT(NAME foo, 'bar')"); + dialects.verified_stmt("SELECT XMLELEMENT(NAME foo, 'bar'), * FROM customers"); + dialects.verified_stmt("SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('v' AS attr), 'bar')"); + dialects.verified_stmt(r#"SELECT XMLELEMENT(NAME "foo$bar", XMLATTRIBUTES('xyz' AS "a&b"))"#); + dialects.verified_stmt(r#"SELECT XMLPI(NAME php, 'echo "hello world";')"#); + dialects.verified_stmt("SELECT XMLPI(NAME php)"); + dialects.verified_stmt("SELECT XMLROOT(''::xml, VERSION '1.0')"); + dialects.verified_stmt("SELECT XMLROOT(''::xml, VERSION '1.0', STANDALONE YES)"); + dialects.verified_stmt("SELECT XMLROOT(''::xml, VERSION NO VALUE, STANDALONE NO VALUE)"); + dialects.verified_stmt("SELECT XMLSERIALIZE(DOCUMENT ''::xml AS TEXT)"); + dialects.verified_stmt("SELECT XMLSERIALIZE(CONTENT ''::xml AS VARCHAR(100) INDENT)"); + dialects.verified_stmt("SELECT XMLSERIALIZE(DOCUMENT ''::xml AS TEXT NO INDENT)"); + dialects.verified_stmt("SELECT XMLEXISTS('/a' PASSING BY REF '')"); + dialects.verified_stmt("SELECT XMLEXISTS('/a' PASSING '')"); + dialects.verified_stmt("SELECT XMLEXISTS('/a' PASSING BY VALUE '')"); + + let others = all_dialects_except(|d| d.supports_xml_expressions()); + others.verified_only_select("SELECT xmlelement(1)"); + assert!(others + .parse_sql_statements("SELECT xmlelement(NAME foo, 'bar')") + .is_err()); + assert!(others + .parse_sql_statements("SELECT xmlexists('/a' PASSING BY REF '')") + .is_err()); +} + /// Regression test for the 2^N parse-time blowup in `parse_compound_expr` on /// inputs like `IF a0.a1...aN.#`. The parse is run on a worker thread and the /// main thread asserts that it reports back within a generous timeout. Post-fix diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d71e49b27a..161d2c5c98 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4158,6 +4158,67 @@ fn parse_xmlparse() { } } +#[test] +fn parse_xmlelement() { + let statements = [ + "SELECT XMLELEMENT(NAME foo, 'bar')", + "SELECT XMLELEMENT(NAME foo, 'bar'), * FROM customers", + "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('v' AS attr), 'bar')", + r#"SELECT XMLELEMENT(NAME "foo$bar", XMLATTRIBUTES('xyz' AS "a&b"))"#, + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + +#[test] +fn parse_xmlpi() { + let statements = [ + "SELECT XMLPI(NAME php, 'echo \"hello world\";')", + "SELECT XMLPI(NAME php)", + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + +#[test] +fn parse_xmlroot() { + let statements = [ + "SELECT XMLROOT(''::xml, VERSION '1.0')", + "SELECT XMLROOT(''::xml, VERSION '1.0', STANDALONE YES)", + "SELECT XMLROOT(''::xml, VERSION NO VALUE, STANDALONE NO VALUE)", + "SELECT XMLROOT(''::xml, VERSION NO VALUE)", + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + +#[test] +fn parse_xmlserialize() { + let statements = [ + "SELECT XMLSERIALIZE(DOCUMENT ''::xml AS TEXT)", + "SELECT XMLSERIALIZE(CONTENT ''::xml AS VARCHAR(100) INDENT)", + "SELECT XMLSERIALIZE(DOCUMENT ''::xml AS TEXT NO INDENT)", + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + +#[test] +fn parse_xmlexists() { + let statements = [ + "SELECT XMLEXISTS('/a' PASSING BY REF '')", + "SELECT XMLEXISTS('/a' PASSING '')", + "SELECT XMLEXISTS('/a' PASSING BY VALUE '')", + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + #[test] fn parse_xml_typed_string() { // xml '...' should parse as a TypedString on PostgreSQL and Generic