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/9] 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/9] 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/9] 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/9] 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"), - } -} From a7aa95bcd57a359af241ba59db3ec7a0ab72a7f6 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Sun, 30 Aug 2026 13:58:53 +0300 Subject: [PATCH 5/9] fix fmt issues --- src/dialect/mod.rs | 2 +- tests/sqlparser_mssql.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 48da3acf5e..8672be2612 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1815,7 +1815,7 @@ pub trait Dialect: Debug + Any { 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/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d3836c597d..e07de8eceb 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2928,9 +2928,12 @@ 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"); + 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] From 340bf94c5b7ce47ca1fd566e4f37e57e1b6dd621 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Thu, 8 Oct 2026 14:18:09 +0300 Subject: [PATCH 6/9] Absorbing clickhouse PR widening for XmlNamespaceDefinition --- src/ast/mod.rs | 4 +-- src/ast/query.rs | 49 +++++++++++++++++++++--------- src/ast/spans.rs | 38 ++++++++++++++++++----- src/dialect/clickhouse.rs | 5 +++ src/dialect/mod.rs | 12 ++++++++ src/parser/mod.rs | 64 ++++++++++++++++++++------------------- tests/sqlparser_common.rs | 42 ++++++++++++++----------- tests/sqlparser_mssql.rs | 18 +++++++++-- 8 files changed, 156 insertions(+), 76 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 20058b83ab..6e0425e7b3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -111,8 +111,8 @@ pub use self::query::{ TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, - UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, - XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, + UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, + WithExpression, WithFill, XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; diff --git a/src/ast/query.rs b/src/ast/query.rs index 29116ecace..f59e6c32fc 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -754,33 +754,52 @@ pub struct With { pub with_token: AttachedToken, /// Whether the `WITH` is recursive (`WITH RECURSIVE`). 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, + /// The expressions declared by this `WITH` clause. + pub exprs: 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 ")?; } - display_comma_separated(&self.cte_tables).fmt(f)?; + display_comma_separated(&self.exprs).fmt(f)?; Ok(()) } } +/// A single expression in a `WITH` clause. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum WithExpression { + /// A common table expression. + Cte(Cte), + /// A common scalar expression. + /// + /// [ClickHouse]: https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions + Cse(ExprWithAlias), + /// An XML namespace declaration list. + /// + /// [MSSQL]: https://learn.microsoft.com/en-us/sql/t-sql/xml/with-xmlnamespaces + XmlNamespaces(Vec), +} + +impl fmt::Display for WithExpression { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WithExpression::Cte(cte) => cte.fmt(f), + WithExpression::Cse(cse) => cse.fmt(f), + WithExpression::XmlNamespaces(namespaces) => write!( + f, + "XMLNAMESPACES ({})", + display_comma_separated(namespaces) + ), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b7a2ee2d76..f9bf1c7bca 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -47,7 +47,8 @@ use super::{ ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + WhileStatement, WildcardAdditionalOptions, With, WithExpression, WithFill, + XmlNamespaceDefinition, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -185,13 +186,30 @@ impl Spanned for With { let With { with_token, recursive: _, // bool - cte_tables, - xml_namespaces: _, // handled separately; no span tracking needed + exprs, } = self; - union_spans( - core::iter::once(with_token.0.span).chain(cte_tables.iter().map(|item| item.span())), - ) + union_spans(core::iter::once(with_token.0.span).chain(exprs.iter().map(|item| item.span()))) + } +} + +impl Spanned for WithExpression { + fn span(&self) -> Span { + match self { + WithExpression::Cte(cte) => cte.span(), + WithExpression::Cse(cse) => cse.span(), + WithExpression::XmlNamespaces(namespaces) => { + union_spans(namespaces.iter().map(|namespace| namespace.span())) + } + } + } +} + +impl Spanned for XmlNamespaceDefinition { + fn span(&self) -> Span { + let XmlNamespaceDefinition { uri, name } = self; + + union_spans(core::iter::once(uri.span()).chain(core::iter::once(name.span))) } } @@ -2753,8 +2771,12 @@ pub mod tests { ); let query = test.0.parse_query().unwrap(); - let cte_span = query.clone().with.unwrap().cte_tables[0].span(); - let cte_query_span = query.clone().with.unwrap().cte_tables[0].query.span(); + let cte = match &query.with.as_ref().unwrap().exprs[0] { + WithExpression::Cte(cte) => cte, + _ => panic!("expected a CTE"), + }; + let cte_span = cte.span(); + let cte_query_span = cte.query.span(); let body_span = query.body.span(); // the WITH keyboard is part of the query diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index c81d953d1b..3eb72dadea 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -162,6 +162,11 @@ impl Dialect for ClickHouseDialect { true } + /// See + fn supports_common_scalar_expressions(&self) -> bool { + true + } + fn supports_comma_separated_trim(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b5d36b2d1b..0be593f1bf 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1776,6 +1776,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports Common Scalar Expressions in a `WITH` clause. + /// + /// For example: + /// ```sql + /// WITH 42 AS answer SELECT answer FROM t + /// ``` + /// + /// [ClickHouse](https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions) + fn supports_common_scalar_expressions(&self) -> bool { + false + } + /// Returns true if the dialect supports the two-argument comma-separated /// form of the `TRIM` function: `TRIM(expr, characters)`. fn supports_comma_separated_trim(&self) -> bool { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a0ed3da081..818756bf24 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14545,37 +14545,11 @@ impl<'a> Parser<'a> { let _guard = self.recursion_counter.try_decrease()?; let with = if self.parse_keyword(Keyword::WITH) { 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![], - }) - } + Some(With { + with_token: with_token.clone().into(), + recursive: self.parse_keyword(Keyword::RECURSIVE), + exprs: self.parse_comma_separated(Parser::parse_with_expression)?, + }) } else { None }; @@ -15118,6 +15092,34 @@ impl<'a> Parser<'a> { Ok(cte) } + /// Parse a single expression in a `WITH` clause. + pub fn parse_with_expression(&mut self) -> Result { + 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)?; + return Ok(WithExpression::XmlNamespaces(namespaces)); + } + + if !self.dialect.supports_common_scalar_expressions() { + return self.parse_cte().map(WithExpression::Cte); + } + + if let Some(cte) = self.maybe_parse(|p| p.parse_cte())? { + return Ok(WithExpression::Cte(cte)); + } + + let expr = self.parse_expr()?; + self.expect_keyword(Keyword::AS)?; + let alias = self.parse_identifier()?; + Ok(WithExpression::Cse(ExprWithAlias { + expr, + alias: Some(alias), + })) + } + /// Parse a "query body", which is an expression with roughly the /// following grammar: /// ```sql diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2de6062b28..4a41278bea 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8140,18 +8140,21 @@ fn parse_ctes() { fn assert_ctes_in_select(expected: &[&str], sel: &Query) { for (i, exp) in expected.iter().enumerate() { - let Cte { alias, query, .. } = &sel.with.as_ref().unwrap().cte_tables[i]; - assert_eq!(*exp, query.to_string()); - assert_eq!(false, alias.explicit); + let cte = match &sel.with.as_ref().unwrap().exprs[i] { + WithExpression::Cte(cte) => cte, + other => panic!("expected a CTE, got {other:?}"), + }; + assert_eq!(*exp, cte.query.to_string()); + assert_eq!(false, cte.alias.explicit); assert_eq!( if i == 0 { Ident::new("a") } else { Ident::new("b") }, - alias.name + cte.alias.name ); - assert!(alias.columns.is_empty()); + assert!(cte.alias.columns.is_empty()); } } @@ -8184,26 +8187,29 @@ fn parse_ctes() { // CTE in a CTE... let sql = &format!("WITH outer_cte AS ({with}) SELECT * FROM outer_cte"); let select = verified_query(sql); - assert_ctes_in_select(&cte_sqls, &only(&select.with.unwrap().cte_tables).query); + let with = select.with.as_ref().unwrap(); + let outer_cte = match only(&with.exprs) { + WithExpression::Cte(cte) => cte, + other => panic!("expected a CTE, got {other:?}"), + }; + assert_ctes_in_select(&cte_sqls, &outer_cte.query); } #[test] fn parse_cte_renamed_columns() { let sql = "WITH cte (col1, col2) AS (SELECT foo, bar FROM baz) SELECT * FROM cte"; let query = all_dialects().verified_query(sql); + let with = query.with.unwrap(); + let cte = match with.exprs.first().unwrap() { + WithExpression::Cte(cte) => cte, + other => panic!("expected a CTE, got {other:?}"), + }; assert_eq!( vec![ TableAliasColumnDef::from_name("col1"), TableAliasColumnDef::from_name("col2") ], - query - .with - .unwrap() - .cte_tables - .first() - .unwrap() - .alias - .columns + cte.alias.columns ); } @@ -8217,8 +8223,8 @@ fn parse_recursive_cte() { let with = query.with.as_ref().unwrap(); assert!(with.recursive); - assert_eq!(with.cte_tables.len(), 1); - let expected = Cte { + assert_eq!(with.exprs.len(), 1); + let expected = WithExpression::Cte(Cte { alias: TableAlias { explicit: false, name: Ident { @@ -8233,8 +8239,8 @@ fn parse_recursive_cte() { from: None, materialized: None, closing_paren_token: AttachedToken::empty(), - }; - assert_eq!(with.cte_tables.first().unwrap(), &expected); + }); + assert_eq!(with.exprs.first().unwrap(), &expected); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index e07de8eceb..2aa32bd6ee 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2928,12 +2928,26 @@ fn parse_mssql_money_constants() { #[test] fn parse_xmlnamespaces() { - ms().verified_stmt( + let query = ms().verified_query( "WITH XMLNAMESPACES ('urn:test' AS ns) SELECT 1 AS [ns:Value] FOR XML PATH('ns:Root')", ); - ms().verified_stmt( + let with = query.with.as_ref().unwrap(); + assert_eq!(with.exprs.len(), 1); + match &with.exprs[0] { + WithExpression::XmlNamespaces(namespaces) => { + assert_eq!(namespaces.len(), 1); + assert_eq!(namespaces[0].to_string(), "'urn:test' AS ns"); + } + other => panic!("expected XMLNAMESPACES, got {other:?}"), + } + + let query = ms().verified_query( "WITH XMLNAMESPACES ('urn:example' AS ns), t AS (SELECT 1 AS id) SELECT id FROM t", ); + let with = query.with.as_ref().unwrap(); + assert_eq!(with.exprs.len(), 2); + assert!(matches!(with.exprs[0], WithExpression::XmlNamespaces(_))); + assert!(matches!(with.exprs[1], WithExpression::Cte(_))); } #[test] From bf95affc17fcb3f97dcef54d0060f764d0bb20bf Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Sun, 11 Oct 2026 12:15:43 +0300 Subject: [PATCH 7/9] fix coverage --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 8 +++----- tests/sqlparser_mssql.rs | 17 +++++++++++++---- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6e0425e7b3..2a87a0775c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -111,8 +111,8 @@ pub use self::query::{ TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, - UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, - WithExpression, WithFill, XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, + UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithExpression, + WithFill, XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; diff --git a/src/ast/query.rs b/src/ast/query.rs index f59e6c32fc..f6f665096c 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -791,11 +791,9 @@ impl fmt::Display for WithExpression { match self { WithExpression::Cte(cte) => cte.fmt(f), WithExpression::Cse(cse) => cse.fmt(f), - WithExpression::XmlNamespaces(namespaces) => write!( - f, - "XMLNAMESPACES ({})", - display_comma_separated(namespaces) - ), + WithExpression::XmlNamespaces(namespaces) => { + write!(f, "XMLNAMESPACES ({})", display_comma_separated(namespaces)) + } } } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2aa32bd6ee..f6c9df763a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -31,7 +31,7 @@ use sqlparser::ast::DataType::{Int, Text, Varbinary}; use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, MsSqlDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, MsSqlDialect}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] @@ -2928,15 +2928,23 @@ fn parse_mssql_money_constants() { #[test] fn parse_xmlnamespaces() { - let query = ms().verified_query( - "WITH XMLNAMESPACES ('urn:test' AS ns) SELECT 1 AS [ns:Value] FOR XML PATH('ns:Root')", - ); + let sql = + "WITH XMLNAMESPACES ('urn:test' AS ns) SELECT 1 AS [ns:Value] FOR XML PATH('ns:Root')"; + let query = ms().verified_query(sql); let with = query.with.as_ref().unwrap(); assert_eq!(with.exprs.len(), 1); + assert_eq!(with.to_string(), "WITH XMLNAMESPACES ('urn:test' AS ns)"); + assert_eq!( + with.exprs[0].to_string(), + "XMLNAMESPACES ('urn:test' AS ns)" + ); + assert_eq!(query.to_string(), sql); + assert_eq!(with.span(), Span::empty()); match &with.exprs[0] { WithExpression::XmlNamespaces(namespaces) => { assert_eq!(namespaces.len(), 1); assert_eq!(namespaces[0].to_string(), "'urn:test' AS ns"); + assert_eq!(namespaces[0].span(), Span::empty()); } other => panic!("expected XMLNAMESPACES, got {other:?}"), } @@ -2948,6 +2956,7 @@ fn parse_xmlnamespaces() { assert_eq!(with.exprs.len(), 2); assert!(matches!(with.exprs[0], WithExpression::XmlNamespaces(_))); assert!(matches!(with.exprs[1], WithExpression::Cte(_))); + assert!(!GenericDialect {}.supports_with_xmlnamespaces_clause()); } #[test] From 76ddeaca4f6cba325685857c55921e1b3bf50317 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Sun, 18 Oct 2026 12:43:19 +0300 Subject: [PATCH 8/9] Fixing fmt and test --- src/ast/spans.rs | 8 ++++---- tests/sqlparser_common.rs | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3251f40794..e7030e74ff 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -44,10 +44,10 @@ use super::{ OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, - SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithExpression, WithFill, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, + Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, + TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithExpression, WithFill, XmlNamespaceDefinition, }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 71f906d652..397e456f52 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20697,11 +20697,16 @@ fn parse_cte_materialization() { let sql = format!("WITH c AS {directive}(SELECT 1) SELECT * FROM c"); TestedDialects::new(vec![Box::new(DuckDbDialect {})]).verified_query(&sql); let query = all_dialects_where(|d| d.supports_cte_materialization()).verified_query(&sql); - assert_eq!(query.with.unwrap().cte_tables[0].materialized, expected); + let with = query.with.unwrap(); + let cte = match &with.exprs[0] { + WithExpression::Cte(cte) => cte, + other => panic!("expected a CTE, got {other:?}"), + }; + assert_eq!(cte.materialized, expected); if expected.is_some() { - all_dialects_where(|d| !d.supports_cte_materialization()) - .parse_sql_statements(&sql) - .unwrap_err(); + for dialect in all_dialects_where(|d| !d.supports_cte_materialization()).dialects { + Parser::parse_sql(dialect.as_ref(), &sql).unwrap_err(); + } } } } From 2ef94d2ca1cba0bfcf9d63a42d06c20eb018be61 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Mon, 26 Oct 2026 12:05:37 +0200 Subject: [PATCH 9/9] Fix cargo fmt --- src/dialect/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6f1fcaa519..f7b400bab8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2052,13 +2052,12 @@ pub trait Dialect: Debug + Any { 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 { false } - /// Returns true if the dialect supports `USING ` in `CREATE TABLE`. ///