diff --git a/src/ast/query.rs b/src/ast/query.rs index c683d66c0..c50ffb062 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 e1fa0c752..801089834 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 f884b0549..313615333 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1794,6 +1794,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 2d51d4717..7fe1021a3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14501,12 +14501,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 3faf56f0d..261c793ce 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2925,3 +2925,10 @@ fn parse_mssql_money_constants() { expr_from_projection(only(&select.projection)), ); } + +#[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"); +}