Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
12 changes: 12 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,11 +756,23 @@ pub struct With {
pub recursive: bool,
/// The list of CTEs declared by this `WITH` clause.
pub cte_tables: Vec<Cte>,
/// Optional XML namespace definitions (`WITH XMLNAMESPACES (...)`).
pub xml_namespaces: Vec<XmlNamespaceDefinition>,
Comment on lines 757 to +760

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.

this feature looks similar to what was done here for clickhouse CSEs, such that I'm thinking we essentially want to introduce this feature in that style instead. is xml_namespaces looks like a regular expression (CSE) so that the enum changes might even be verbatim (similarly for the dialect method name)

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.

Indeed i use the WITH keyword as done with cte_tables, i hope i understood what you have meant.

}

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 ")?;
}
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <format>` in `CREATE TABLE`.
///
/// Example:
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

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
38 changes: 32 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14108,12 +14108,38 @@ 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();
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
};
Expand Down
7 changes: 7 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2923,3 +2923,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')");

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.

merged tests and used verifies_stmt() @iffyio

ms().verified_stmt("WITH XMLNAMESPACES ('urn:example' AS ns), t AS (SELECT 1 AS id) SELECT id FROM t");
}
133 changes: 133 additions & 0 deletions tests/test_xmlnamespace_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/// Test to verify XMLNAMESPACES parsing and AST storage

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.

hmm I don't think we should introduce a new file for this test. Also let's merge the tests and used either verified_stmt or one_statement_parses_to in tests as I mentioned in the previous review. one simplification of the tests introduced here is to drop the AST assertions, the PR doesn't introduce a new node so its overkill to have each test expiclitly assert the full AST. please have the tests follow existing conventions (this is introducing some println and manual display assertions patterns and is unclear why that's needed)

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.

you are correct, i removed the file.

/// 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"),
}
}