Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
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 @@ -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.
Expand Down Expand Up @@ -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)))
}
}

Expand Down Expand Up @@ -2780,8 +2799,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 @@ -185,6 +185,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 @@ -1951,6 +1951,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 @@ -1990,6 +2002,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 @@ -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 {

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 @@ -14835,11 +14835,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 @@ -15383,6 +15383,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 @@ -8153,18 +8153,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 @@ -8197,26 +8200,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 @@ -8230,8 +8236,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 @@ -8246,8 +8252,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 @@ -20739,11 +20745,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 @@ -2967,6 +2967,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