diff --git a/src/ast/query.rs b/src/ast/query.rs index 2ada46a9f..25f0bad93 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -756,11 +756,23 @@ pub struct With { pub recursive: bool, /// The list of CTEs declared by this `WITH` clause. pub cte_tables: Vec, + /// Optional XML namespace definitions (`WITH XMLNAMESPACES (...)`). + pub xml_namespaces: Vec, } impl fmt::Display for With { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("WITH ")?; + if !self.xml_namespaces.is_empty() { + write!( + f, + "XMLNAMESPACES ({})", + display_comma_separated(&self.xml_namespaces) + )?; + if !self.cte_tables.is_empty() { + f.write_str(", ")?; + } + } if self.recursive { f.write_str("RECURSIVE ")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a34fe66d9..6bb2f0405 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -186,6 +186,7 @@ impl Spanned for With { with_token, recursive: _, // bool cte_tables, + xml_namespaces: _, // handled separately; no span tracking needed } = self; union_spans( diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f99cbe2ea..48da3acf5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1804,6 +1804,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports a leading `WITH XMLNAMESPACES (...)` + /// clause in queries. + /// + /// Example: + /// ```sql + /// WITH XMLNAMESPACES ('urn:example' AS ns) + /// SELECT 1 + /// ``` + fn supports_with_xmlnamespaces_clause(&self) -> bool { + false + } + /// Returns true if the dialect supports aliased function arguments, /// e.g. `XMLFOREST(a AS x)` in PostgreSQL. fn supports_aliased_function_args(&self) -> bool { diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 980b63d28..a6e9cf64c 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -248,6 +248,11 @@ impl Dialect for MsSqlDialect { _ => None, } } + + // see: https://learn.microsoft.com/en-us/sql/t-sql/xml/with-xmlnamespaces + fn supports_with_xmlnamespaces_clause(&self) -> bool { + true + } } impl MsSqlDialect { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 953453a22..1907bab44 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14531,12 +14531,38 @@ impl<'a> Parser<'a> { pub fn parse_query(&mut self) -> Result, ParserError> { let _guard = self.recursion_counter.try_decrease()?; let with = if self.parse_keyword(Keyword::WITH) { - let with_token = self.get_current_token(); - Some(With { - with_token: with_token.clone().into(), - recursive: self.parse_keyword(Keyword::RECURSIVE), - cte_tables: self.parse_comma_separated(Parser::parse_cte)?, - }) + let with_token = self.get_current_token().clone(); + if self.dialect.supports_with_xmlnamespaces_clause() + && self.parse_keyword(Keyword::XMLNAMESPACES) + { + self.expect_token(&Token::LParen)?; + let namespaces = + self.parse_comma_separated(Parser::parse_xml_namespace_definition)?; + self.expect_token(&Token::RParen)?; + + if self.consume_token(&Token::Comma) { + Some(With { + with_token: with_token.clone().into(), + recursive: self.parse_keyword(Keyword::RECURSIVE), + cte_tables: self.parse_comma_separated(Parser::parse_cte)?, + xml_namespaces: namespaces, + }) + } else { + Some(With { + with_token: with_token.clone().into(), + recursive: false, + cte_tables: vec![], + xml_namespaces: namespaces, + }) + } + } else { + Some(With { + with_token: with_token.clone().into(), + recursive: self.parse_keyword(Keyword::RECURSIVE), + cte_tables: self.parse_comma_separated(Parser::parse_cte)?, + xml_namespaces: vec![], + }) + } } else { None }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4510f953e..d3836c597 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2926,6 +2926,13 @@ fn parse_mssql_money_constants() { ); } +#[test] +fn parse_xmlnamespaces() { + + ms().verified_stmt("WITH XMLNAMESPACES ('urn:test' AS ns) SELECT 1 AS [ns:Value] FOR XML PATH('ns:Root')"); + ms().verified_stmt("WITH XMLNAMESPACES ('urn:example' AS ns), t AS (SELECT 1 AS id) SELECT id FROM t"); +} + #[test] fn parse_bracket_quoted_function_argument_name() { let Statement::DropFunction(drop) = ms().verified_stmt("DROP FUNCTION f([Role] INT)") else {