diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..cb74c04063 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 4aa82c7c3b..7d65d71174 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -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, + /// The expressions declared by this `WITH` clause. + pub exprs: Vec, } impl fmt::Display for With { @@ -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), +} + +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 8267c71798..6c644f7cb5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -47,7 +47,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. @@ -185,12 +186,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))) } } @@ -2773,8 +2792,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 41a0e8be42..11b4c28cd6 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -198,6 +198,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 a2920e61eb..6f1fcaa519 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1985,6 +1985,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 { @@ -2029,11 +2041,24 @@ 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 { false } + /// Returns true if the dialect supports `USING ` in `CREATE TABLE`. /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 06a07b61d8..b9734ae7a9 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -252,6 +252,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 276b252ed2..2af0235c07 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14878,11 +14878,11 @@ 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(); + 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 @@ -15426,6 +15426,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 8f665b25c2..7f9532f07a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8149,18 +8149,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()); } } @@ -8193,26 +8196,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 ); } @@ -8226,8 +8232,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 { @@ -8242,8 +8248,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] @@ -20865,11 +20871,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(); + } } } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9bf015fc00..56561154c9 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] @@ -2969,6 +2969,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 {