From bb9561280f1e0c0624e58bd2b72c8121eceac22d Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Mon, 1 Jun 2026 15:47:26 +0300 Subject: [PATCH 1/4] Added support to parse xmlnamespace in mssql --- src/dialect/mod.rs | 12 ++++++++++++ src/dialect/mssql.rs | 4 ++++ src/parser/mod.rs | 31 +++++++++++++++++++++++++------ tests/sqlparser_mssql.rs | 23 +++++++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9b2ede40d2..611ba7310c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1767,6 +1767,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 `USING ` in `CREATE TABLE`. /// /// Example: diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 980b63d289..e07a7fc807 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -248,6 +248,10 @@ impl Dialect for MsSqlDialect { _ => None, } } + + fn supports_with_xmlnamespaces_clause(&self) -> bool { + true + } } impl MsSqlDialect { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3c61851930..604bf8ed54 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14108,12 +14108,31 @@ 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)?, + }) + } else { + None + } + } else { + Some(With { + with_token: with_token.clone().into(), + recursive: self.parse_keyword(Keyword::RECURSIVE), + cte_tables: self.parse_comma_separated(Parser::parse_cte)?, + }) + } } else { None }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6e866746d7..f7ec5254af 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2923,3 +2923,26 @@ fn parse_mssql_money_constants() { expr_from_projection(only(&select.projection)), ); } + +#[test] +fn parse_xmlnamespaces() { + let sql = r#"WITH XMLNAMESPACES ('urn:test' AS ns) +SELECT 1 AS [ns:Value] +FOR XML PATH('ns:Root');"#; + + tsql().parse_sql_statements(sql).unwrap(); + ms().parse_sql_statements(sql).unwrap(); + +} + +#[test] +fn parse_xmlnamespaces_with_cte() { + let sql = r#" +WITH XMLNAMESPACES ('urn:example' AS ns), t AS ( + SELECT 1 AS id +) +SELECT id FROM t +"#; + + tsql().parse_sql_statements(sql).unwrap(); +} \ No newline at end of file From 5d63c97de01c80f2c1f18e088f02de6ae29bd3cc Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Mon, 1 Jun 2026 16:59:26 +0300 Subject: [PATCH 2/4] Aligned tests to mssql --- tests/sqlparser_mssql.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index f7ec5254af..2a6eb67b78 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2930,7 +2930,6 @@ fn parse_xmlnamespaces() { SELECT 1 AS [ns:Value] FOR XML PATH('ns:Root');"#; - tsql().parse_sql_statements(sql).unwrap(); ms().parse_sql_statements(sql).unwrap(); } @@ -2944,5 +2943,5 @@ WITH XMLNAMESPACES ('urn:example' AS ns), t AS ( SELECT id FROM t "#; - tsql().parse_sql_statements(sql).unwrap(); + ms().parse_sql_statements(sql).unwrap(); } \ No newline at end of file From a903a0c4b87e327e321da72c3c17e674ebb1b8f2 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Wed, 29 Jul 2026 16:38:52 +0300 Subject: [PATCH 3/4] Added the namespace to AST and adjusted tests accordingly. --- src/ast/query.rs | 12 +++ src/ast/spans.rs | 1 + src/dialect/mssql.rs | 1 + src/parser/mod.rs | 11 +- tests/sqlparser_mssql.rs | 19 +--- tests/test_xmlnamespace_integration.rs | 133 +++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 tests/test_xmlnamespace_integration.rs diff --git a/src/ast/query.rs b/src/ast/query.rs index 1de0e0e9db..7946e58f83 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 0e328db433..c9efbe105a 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/mssql.rs b/src/dialect/mssql.rs index e07a7fc807..a6e9cf64c7 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -249,6 +249,7 @@ impl Dialect for MsSqlDialect { } } + // see: https://learn.microsoft.com/en-us/sql/t-sql/xml/with-xmlnamespaces fn supports_with_xmlnamespaces_clause(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 604bf8ed54..54bf6892ed 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14113,7 +14113,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::XMLNAMESPACES) { self.expect_token(&Token::LParen)?; - let _namespaces = + let namespaces = self.parse_comma_separated(Parser::parse_xml_namespace_definition)?; self.expect_token(&Token::RParen)?; @@ -14122,15 +14122,22 @@ impl<'a> Parser<'a> { 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 { - None + 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 { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2a6eb67b78..382fb185f0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2926,22 +2926,7 @@ fn parse_mssql_money_constants() { #[test] fn parse_xmlnamespaces() { - let sql = r#"WITH XMLNAMESPACES ('urn:test' AS ns) -SELECT 1 AS [ns:Value] -FOR XML PATH('ns:Root');"#; - - ms().parse_sql_statements(sql).unwrap(); + 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_xmlnamespaces_with_cte() { - let sql = r#" -WITH XMLNAMESPACES ('urn:example' AS ns), t AS ( - SELECT 1 AS id -) -SELECT id FROM t -"#; - - ms().parse_sql_statements(sql).unwrap(); -} \ No newline at end of file diff --git a/tests/test_xmlnamespace_integration.rs b/tests/test_xmlnamespace_integration.rs new file mode 100644 index 0000000000..58344d5671 --- /dev/null +++ b/tests/test_xmlnamespace_integration.rs @@ -0,0 +1,133 @@ +/// Test to verify XMLNAMESPACES parsing and AST storage +/// This demonstrates that the XMLNAMESPACES clause is now properly stored in the AST +/// instead of being dropped. +use sqlparser::ast::Statement; +use sqlparser::dialect::MsSqlDialect; +use sqlparser::parser::Parser; + +#[test] +fn test_xmlnamespaces_parsing_and_ast_storage() { + let dialect = MsSqlDialect {}; + let sql = r#" + WITH XMLNAMESPACES ('http://example.com' AS ex, 'http://other.com' AS ot) + SELECT 1 AS col + "#; + + let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + let ast = parser.parse_statements().unwrap(); + + assert_eq!(ast.len(), 1, "Should parse as a single statement"); + + match &ast[0] { + Statement::Query(query) => { + // Verify the WITH clause is present + assert!(query.with.is_some(), "Query should have WITH clause"); + + let with_clause = query.with.as_ref().unwrap(); + + // Verify xml_namespaces were captured + assert_eq!( + with_clause.xml_namespaces.len(), + 2, + "Should have 2 XML namespace definitions" + ); + + // Check first namespace + let first_ns = &with_clause.xml_namespaces[0]; + assert_eq!( + first_ns.name.value, "ex", + "First namespace alias should be 'ex'" + ); + + // Check second namespace + let second_ns = &with_clause.xml_namespaces[1]; + assert_eq!( + second_ns.name.value, "ot", + "Second namespace alias should be 'ot'" + ); + + // Verify CTEs are empty (no CTEs after XMLNAMESPACES in this example) + assert_eq!(with_clause.cte_tables.len(), 0, "Should have no CTE tables"); + + // Verify Display output includes XMLNAMESPACES + let display_output = format!("{}", with_clause); + assert!( + display_output.contains("XMLNAMESPACES"), + "Display output should include XMLNAMESPACES" + ); + + println!("✓ XMLNAMESPACES AST representation: {}", display_output); + } + _ => panic!("Expected Query statement"), + } +} + +#[test] +fn test_xmlnamespaces_with_ctes() { + let dialect = MsSqlDialect {}; + let sql = r#" + WITH XMLNAMESPACES ('http://example.com' AS ex), + cte1 AS (SELECT 1 AS col) + SELECT * FROM cte1 + "#; + + let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + let ast = parser.parse_statements().unwrap(); + + assert_eq!(ast.len(), 1, "Should parse as a single statement"); + + match &ast[0] { + Statement::Query(query) => { + let with_clause = query.with.as_ref().unwrap(); + + // Verify namespaces + assert_eq!( + with_clause.xml_namespaces.len(), + 1, + "Should have 1 XML namespace definition" + ); + + // Verify CTEs + assert_eq!(with_clause.cte_tables.len(), 1, "Should have 1 CTE table"); + assert_eq!( + with_clause.cte_tables[0].alias.name.value, "cte1", + "CTE name should be 'cte1'" + ); + + let display_output = format!("{}", with_clause); + println!("✓ XMLNAMESPACES with CTEs: {}", display_output); + assert!(display_output.contains("XMLNAMESPACES")); + assert!(display_output.contains("cte1")); + } + _ => panic!("Expected Query statement"), + } +} + +#[test] +fn test_xmlnamespaces_display_format() { + let dialect = MsSqlDialect {}; + let sql = r#" + WITH XMLNAMESPACES ('http://example.com' AS ex, 'http://other.com' AS ot), + my_cte AS (SELECT 1) + SELECT * FROM my_cte + "#; + + let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); + let ast = parser.parse_statements().unwrap(); + + match &ast[0] { + Statement::Query(query) => { + let with_clause = query.with.as_ref().unwrap(); + let display_output = format!("{}", with_clause); + + // Verify the order: XMLNAMESPACES comes first, then CTEs + assert!( + display_output.starts_with("WITH XMLNAMESPACES"), + "Display should start with 'WITH XMLNAMESPACES'" + ); + + println!("✓ Full display format: {}", display_output); + } + _ => panic!("Expected Query statement"), + } +} From 7d0b1841e1b7b41c58939f6e081f2a7cc3549253 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Sun, 2 Aug 2026 11:15:07 +0300 Subject: [PATCH 4/4] Remove extra test file --- tests/test_xmlnamespace_integration.rs | 133 ------------------------- 1 file changed, 133 deletions(-) delete mode 100644 tests/test_xmlnamespace_integration.rs diff --git a/tests/test_xmlnamespace_integration.rs b/tests/test_xmlnamespace_integration.rs deleted file mode 100644 index 58344d5671..0000000000 --- a/tests/test_xmlnamespace_integration.rs +++ /dev/null @@ -1,133 +0,0 @@ -/// Test to verify XMLNAMESPACES parsing and AST storage -/// This demonstrates that the XMLNAMESPACES clause is now properly stored in the AST -/// instead of being dropped. -use sqlparser::ast::Statement; -use sqlparser::dialect::MsSqlDialect; -use sqlparser::parser::Parser; - -#[test] -fn test_xmlnamespaces_parsing_and_ast_storage() { - let dialect = MsSqlDialect {}; - let sql = r#" - WITH XMLNAMESPACES ('http://example.com' AS ex, 'http://other.com' AS ot) - SELECT 1 AS col - "#; - - let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); - let ast = parser.parse_statements().unwrap(); - - assert_eq!(ast.len(), 1, "Should parse as a single statement"); - - match &ast[0] { - Statement::Query(query) => { - // Verify the WITH clause is present - assert!(query.with.is_some(), "Query should have WITH clause"); - - let with_clause = query.with.as_ref().unwrap(); - - // Verify xml_namespaces were captured - assert_eq!( - with_clause.xml_namespaces.len(), - 2, - "Should have 2 XML namespace definitions" - ); - - // Check first namespace - let first_ns = &with_clause.xml_namespaces[0]; - assert_eq!( - first_ns.name.value, "ex", - "First namespace alias should be 'ex'" - ); - - // Check second namespace - let second_ns = &with_clause.xml_namespaces[1]; - assert_eq!( - second_ns.name.value, "ot", - "Second namespace alias should be 'ot'" - ); - - // Verify CTEs are empty (no CTEs after XMLNAMESPACES in this example) - assert_eq!(with_clause.cte_tables.len(), 0, "Should have no CTE tables"); - - // Verify Display output includes XMLNAMESPACES - let display_output = format!("{}", with_clause); - assert!( - display_output.contains("XMLNAMESPACES"), - "Display output should include XMLNAMESPACES" - ); - - println!("✓ XMLNAMESPACES AST representation: {}", display_output); - } - _ => panic!("Expected Query statement"), - } -} - -#[test] -fn test_xmlnamespaces_with_ctes() { - let dialect = MsSqlDialect {}; - let sql = r#" - WITH XMLNAMESPACES ('http://example.com' AS ex), - cte1 AS (SELECT 1 AS col) - SELECT * FROM cte1 - "#; - - let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); - let ast = parser.parse_statements().unwrap(); - - assert_eq!(ast.len(), 1, "Should parse as a single statement"); - - match &ast[0] { - Statement::Query(query) => { - let with_clause = query.with.as_ref().unwrap(); - - // Verify namespaces - assert_eq!( - with_clause.xml_namespaces.len(), - 1, - "Should have 1 XML namespace definition" - ); - - // Verify CTEs - assert_eq!(with_clause.cte_tables.len(), 1, "Should have 1 CTE table"); - assert_eq!( - with_clause.cte_tables[0].alias.name.value, "cte1", - "CTE name should be 'cte1'" - ); - - let display_output = format!("{}", with_clause); - println!("✓ XMLNAMESPACES with CTEs: {}", display_output); - assert!(display_output.contains("XMLNAMESPACES")); - assert!(display_output.contains("cte1")); - } - _ => panic!("Expected Query statement"), - } -} - -#[test] -fn test_xmlnamespaces_display_format() { - let dialect = MsSqlDialect {}; - let sql = r#" - WITH XMLNAMESPACES ('http://example.com' AS ex, 'http://other.com' AS ot), - my_cte AS (SELECT 1) - SELECT * FROM my_cte - "#; - - let mut parser = Parser::new(&dialect).try_with_sql(sql).unwrap(); - let ast = parser.parse_statements().unwrap(); - - match &ast[0] { - Statement::Query(query) => { - let with_clause = query.with.as_ref().unwrap(); - let display_output = format!("{}", with_clause); - - // Verify the order: XMLNAMESPACES comes first, then CTEs - assert!( - display_output.starts_with("WITH XMLNAMESPACES"), - "Display should start with 'WITH XMLNAMESPACES'" - ); - - println!("✓ Full display format: {}", display_output); - } - _ => panic!("Expected Query statement"), - } -}