From f9b311eec5bed20af5de8ac55369a419039f14a6 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 8 Jun 2026 22:23:49 +0200 Subject: [PATCH] Add XMLPARSE support --- src/ast/mod.rs | 22 ++++++++++++++++-- src/parser/mod.rs | 39 ++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 45 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 15 +++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 447d89bb4..8d893df9e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7960,6 +7960,9 @@ pub enum FunctionArgOperator { Colon, /// function(arg1 VALUE value1) Value, + /// function(arg1 value1), with no operator between the name and the value, + /// as in PostgreSQL `XMLPARSE(DOCUMENT value)` + Space, } impl fmt::Display for FunctionArgOperator { @@ -7970,6 +7973,7 @@ impl fmt::Display for FunctionArgOperator { FunctionArgOperator::Assignment => f.write_str(":="), FunctionArgOperator::Colon => f.write_str(":"), FunctionArgOperator::Value => f.write_str("VALUE"), + FunctionArgOperator::Space => Ok(()), } } } @@ -8012,17 +8016,31 @@ impl fmt::Display for FunctionArg { name, arg, operator, - } => write!(f, "{name} {operator} {arg}"), + } => fmt_named_function_arg(f, name, operator, arg), FunctionArg::ExprNamed { name, arg, operator, - } => write!(f, "{name} {operator} {arg}"), + } => fmt_named_function_arg(f, name, operator, arg), FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"), } } } +/// `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( + f: &mut fmt::Formatter, + name: &impl fmt::Display, + operator: &FunctionArgOperator, + arg: &FunctionArgExpr, +) -> fmt::Result { + match operator { + FunctionArgOperator::Space => write!(f, "{name} {arg}"), + _ => write!(f, "{name} {operator} {arg}"), + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a7e641f98..b4a3fd619 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2527,8 +2527,47 @@ impl<'a> Parser<'a> { }) } + /// Parse `XMLPARSE({ DOCUMENT | CONTENT } value)`, whose mode keyword is + /// carried by the single argument as a name with no operator. Neither mode + /// word is reserved, so both arrive as plain words rather than keywords. + fn parse_xmlparse_call(&mut self, name: ObjectName) -> Result { + self.expect_token(&Token::LParen)?; + let is_mode = matches!(&self.peek_token_ref().token, Token::Word(word) + if word.quote_style.is_none() + && (word.value.eq_ignore_ascii_case("content") + || word.value.eq_ignore_ascii_case("document"))); + if !is_mode { + return self.expected_ref("CONTENT or DOCUMENT", self.peek_token_ref()); + } + let arg = FunctionArg::Named { + name: self.parse_identifier()?, + arg: FunctionArgExpr::Expr(self.parse_expr()?), + operator: FunctionArgOperator::Space, + }; + self.expect_token(&Token::RParen)?; + Ok(Function { + name, + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![arg], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }) + } + /// Parse a function call expression named by `name` and return it as an `Expr`. pub fn parse_function(&mut self, name: ObjectName) -> Result { + if self.dialect.supports_xml_expressions() + && Self::is_simple_unquoted_object_name(&name, "xmlparse") + { + return self.parse_xmlparse_call(name).map(Expr::Function); + } self.parse_function_call(name).map(Expr::Function) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b561f8935..f1215f31f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19014,6 +19014,51 @@ fn parse_aliased_function_args() { .is_err()); } +#[test] +fn parse_xmlparse() { + let dialects = all_dialects_where(|d| d.supports_xml_expressions()); + + for (sql, mode) in [ + ("SELECT xmlparse(content '')", "content"), + ("SELECT xmlparse(document '')", "document"), + ] { + let select = dialects.verified_only_select(sql); + match expr_from_projection(&select.projection[0]) { + Expr::Function(Function { + name, + args: FunctionArguments::List(list), + .. + }) => { + assert_eq!(name.to_string(), "xmlparse"); + assert_eq!( + list.args, + vec![FunctionArg::Named { + name: Ident::new(mode), + arg: FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("".to_string()).into() + )), + operator: FunctionArgOperator::Space, + }] + ); + } + expr => panic!("expected an XMLPARSE function call, got {expr:?}"), + } + } + + // XMLPARSE requires a CONTENT or DOCUMENT mode. + assert!(dialects + .parse_sql_statements("SELECT xmlparse('')") + .is_err()); + + // On dialects without XML support, `xmlparse` stays a regular function + // and the special `CONTENT ` syntax is rejected. + let others = all_dialects_except(|d| d.supports_xml_expressions()); + others.verified_only_select("SELECT xmlparse(1)"); + assert!(others + .parse_sql_statements("SELECT xmlparse(content '')") + .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 713d465a8..3d9fd708f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3951,6 +3951,21 @@ fn parse_xmlforest_aliased_arguments() { ); } +#[test] +fn parse_xmlparse() { + // The parser only distinguishes the two modes, so the corpus covers those + // plus a non-literal argument. + let statements = [ + "SELECT XMLPARSE(CONTENT '')", + "SELECT XMLPARSE(CONTENT 'x')", + "SELECT XMLPARSE(DOCUMENT 'x')", + "SELECT XMLPARSE(DOCUMENT col || '')", + ]; + for sql in statements { + pg().verified_stmt(sql); + } +} + #[test] fn parse_xml_typed_string() { // xml '...' should parse as a TypedString on PostgreSQL and Generic