Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb95612
Added support to parse xmlnamespace in mssql
kfirSatori Jun 1, 2026
5d63c97
Aligned tests to mssql
kfirSatori Jun 1, 2026
a903a0c
Added the namespace to AST and adjusted tests accordingly.
kfirSatori Jul 29, 2026
2dbfcba
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Jul 30, 2026
7d0b184
Remove extra test file
kfirSatori Aug 2, 2026
0f9283b
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Aug 27, 2026
a7aa95b
fix fmt issues
kfirSatori Aug 30, 2026
5a1113a
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Sep 1, 2026
16baec9
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Sep 4, 2026
28e0d06
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Sep 7, 2026
939092b
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Sep 8, 2026
2eadfd0
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Sep 17, 2026
340bf94
Absorbing clickhouse PR widening for XmlNamespaceDefinition
kfirSatori Oct 8, 2026
bf95aff
fix coverage
kfirSatori Oct 11, 2026
5676850
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Oct 18, 2026
76ddeac
Fixing fmt and test
kfirSatori Oct 18, 2026
b7d0378
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Oct 19, 2026
5b4d595
Merge remote-tracking branch 'upstream/main' into add_xmlnamespace_su…
kfirSatori Oct 22, 2026
2ef94d2
Fix cargo fmt
kfirSatori Oct 26, 2026
00fbfa3
Merge branch 'main' into add_xmlnamespace_support_mssql
kfirSatori Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,8 @@ 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<Cte>,
/// The expressions declared by this `WITH` clause.
pub exprs: Vec<WithExpression>,
}

impl fmt::Display for With {
Expand All @@ -763,11 +763,40 @@ impl fmt::Display for With {
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<XmlNamespaceDefinition>),
}

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))]
Expand Down
37 changes: 30 additions & 7 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ use super::{
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, WithFill,
ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithExpression, WithFill,
XmlNamespaceDefinition,
};

/// Given an iterator of spans, return the [Span::union] of all spans.
Expand Down Expand Up @@ -186,12 +187,30 @@ impl Spanned for With {
let With {
with_token,
recursive: _, // bool
cte_tables,
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)))
}
}

Expand Down Expand Up @@ -2889,8 +2908,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
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ impl Dialect for ClickHouseDialect {
true
}

/// See <https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions>
fn supports_common_scalar_expressions(&self) -> bool {
true
}

fn supports_comma_separated_trim(&self) -> bool {
true
}
Expand Down
24 changes: 24 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,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 {
Expand Down Expand Up @@ -2145,6 +2157,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 {
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we include a link to the documentation of this syntax?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iffyio Added documentation link

true
}
}

impl MsSqlDialect {
Expand Down
32 changes: 30 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15238,11 +15238,11 @@ impl<'a> Parser<'a> {
pub fn parse_query(&mut self) -> Result<Box<Query>, ParserError> {
let _guard = self.recursion_counter.try_decrease()?;
let with = if self.parse_keyword(Keyword::WITH) {
let with_token = self.get_current_token();
let with_token = self.get_current_token().clone();
Some(With {
with_token: with_token.clone().into(),
recursive: self.parse_keyword(Keyword::RECURSIVE),
cte_tables: self.parse_comma_separated(Parser::parse_cte)?,
exprs: self.parse_comma_separated(Parser::parse_with_expression)?,
})
} else {
None
Expand Down Expand Up @@ -15786,6 +15786,34 @@ impl<'a> Parser<'a> {
Ok(cte)
}

/// Parse a single expression in a `WITH` clause.
pub fn parse_with_expression(&mut self) -> Result<WithExpression, ParserError> {
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
Expand Down
55 changes: 33 additions & 22 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8608,18 +8608,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());
}
}

Expand Down Expand Up @@ -8652,26 +8655,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
);
}

Expand All @@ -8685,8 +8691,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 {
Expand All @@ -8701,8 +8707,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]
Expand Down Expand Up @@ -21880,11 +21886,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();
}
}
}
}
Expand Down
35 changes: 34 additions & 1 deletion tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -2975,6 +2975,39 @@ fn parse_mssql_money_constants() {
);
}

#[test]
fn parse_xmlnamespaces() {
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:?}"),
}

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(_)));
assert!(!GenericDialect {}.supports_with_xmlnamespaces_clause());
}

#[test]
fn parse_bracket_quoted_function_argument_name() {
let Statement::DropFunction(drop) = ms().verified_stmt("DROP FUNCTION f([Role] INT)") else {
Expand Down
Loading