Skip to content

Commit 6589675

Browse files
committed
Allow WITH using scalars for ClickHouse dialect
1 parent 7c78d13 commit 6589675

8 files changed

Lines changed: 191 additions & 30 deletions

File tree

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub use self::query::{
108108
TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed,
109109
TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity,
110110
UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
111-
XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn,
111+
WithItem, XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn,
112112
XmlTableColumnOption,
113113
};
114114

src/ast/query.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -754,8 +754,9 @@ pub struct With {
754754
pub with_token: AttachedToken,
755755
/// Whether the `WITH` is recursive (`WITH RECURSIVE`).
756756
pub recursive: bool,
757-
/// The list of CTEs declared by this `WITH` clause.
758-
pub cte_tables: Vec<Cte>,
757+
/// The items declared by this `WITH` clause: traditional CTEs and,
758+
/// for dialects that support it, named expressions.
759+
pub items: Vec<WithItem>,
759760
}
760761

761762
impl fmt::Display for With {
@@ -764,11 +765,41 @@ impl fmt::Display for With {
764765
if self.recursive {
765766
f.write_str("RECURSIVE ")?;
766767
}
767-
display_comma_separated(&self.cte_tables).fmt(f)?;
768+
display_comma_separated(&self.items).fmt(f)?;
768769
Ok(())
769770
}
770771
}
771772

773+
/// A single item in a `WITH` clause.
774+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
775+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
776+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
777+
pub enum WithItem {
778+
/// A traditional common table expression: `name [(cols)] AS [MATERIALIZED] (query)`.
779+
Cte(Cte),
780+
/// `<expr> AS <alias>` — binds an expression (literal, scalar subquery,
781+
/// lambda, …) to a name visible in the surrounding query.
782+
///
783+
/// See ClickHouse's [common scalar expressions][1].
784+
///
785+
/// [1]: https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions
786+
Named {
787+
/// The expression bound to the alias.
788+
expr: Expr,
789+
/// The name the expression is bound to.
790+
alias: Ident,
791+
},
792+
}
793+
794+
impl fmt::Display for WithItem {
795+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
796+
match self {
797+
WithItem::Cte(cte) => cte.fmt(f),
798+
WithItem::Named { expr, alias } => write!(f, "{expr} AS {alias}"),
799+
}
800+
}
801+
}
802+
772803
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
773804
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
774805
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/spans.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use super::{
4747
ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript,
4848
SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject,
4949
TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef,
50-
WhileStatement, WildcardAdditionalOptions, With, WithFill,
50+
WhileStatement, WildcardAdditionalOptions, With, WithFill, WithItem,
5151
};
5252

5353
/// Given an iterator of spans, return the [Span::union] of all spans.
@@ -185,12 +185,21 @@ impl Spanned for With {
185185
let With {
186186
with_token,
187187
recursive: _, // bool
188-
cte_tables,
188+
items,
189189
} = self;
190190

191-
union_spans(
192-
core::iter::once(with_token.0.span).chain(cte_tables.iter().map(|item| item.span())),
193-
)
191+
union_spans(core::iter::once(with_token.0.span).chain(items.iter().map(|item| item.span())))
192+
}
193+
}
194+
195+
impl Spanned for WithItem {
196+
fn span(&self) -> Span {
197+
match self {
198+
WithItem::Cte(cte) => cte.span(),
199+
WithItem::Named { expr, alias } => {
200+
union_spans(core::iter::once(expr.span()).chain(core::iter::once(alias.span)))
201+
}
202+
}
194203
}
195204
}
196205

@@ -2716,8 +2725,12 @@ pub mod tests {
27162725
);
27172726

27182727
let query = test.0.parse_query().unwrap();
2719-
let cte_span = query.clone().with.unwrap().cte_tables[0].span();
2720-
let cte_query_span = query.clone().with.unwrap().cte_tables[0].query.span();
2728+
let cte = match &query.with.as_ref().unwrap().items[0] {
2729+
WithItem::Cte(cte) => cte,
2730+
_ => panic!("expected a CTE"),
2731+
};
2732+
let cte_span = cte.span();
2733+
let cte_query_span = cte.query.span();
27212734
let body_span = query.body.span();
27222735

27232736
// the WITH keyboard is part of the query

src/dialect/clickhouse.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,9 @@ impl Dialect for ClickHouseDialect {
153153
fn supports_comma_separated_trim(&self) -> bool {
154154
true
155155
}
156+
157+
/// See <https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions>
158+
fn supports_with_clause_scalar_expression(&self) -> bool {
159+
true
160+
}
156161
}

src/dialect/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,21 @@ pub trait Dialect: Debug + Any {
17451745
false
17461746
}
17471747

1748+
/// Returns true if the dialect allows a `WITH` clause item to bind a
1749+
/// scalar (or otherwise non-CTE) expression to a name, with the form
1750+
/// `<expression> AS <identifier>` — alongside or instead of the
1751+
/// traditional `<identifier> AS (<subquery>)` CTE form.
1752+
///
1753+
/// For example, in ClickHouse:
1754+
/// ```sql
1755+
/// WITH 42 AS answer SELECT answer FROM t
1756+
/// ```
1757+
///
1758+
/// [ClickHouse](https://clickhouse.com/docs/sql-reference/statements/select/with#common-scalar-expressions)
1759+
fn supports_with_clause_scalar_expression(&self) -> bool {
1760+
false
1761+
}
1762+
17481763
/// Returns true if the dialect supports parenthesized multi-column
17491764
/// aliases in SELECT items. For example:
17501765
/// ```sql

src/parser/mod.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14105,7 +14105,7 @@ impl<'a> Parser<'a> {
1410514105
Some(With {
1410614106
with_token: with_token.clone().into(),
1410714107
recursive: self.parse_keyword(Keyword::RECURSIVE),
14108-
cte_tables: self.parse_comma_separated(Parser::parse_cte)?,
14108+
items: self.parse_comma_separated(Parser::parse_with_item)?,
1410914109
})
1411014110
} else {
1411114111
None
@@ -14639,6 +14639,33 @@ impl<'a> Parser<'a> {
1463914639
Ok(cte)
1464014640
}
1464114641

14642+
/// Parse a single item in a `WITH` clause.
14643+
///
14644+
/// In standard SQL this is always a CTE (`name [(cols)] AS (query)`).
14645+
/// Dialects that enable [`Dialect::supports_with_clause_scalar_expression`]
14646+
/// — currently only ClickHouse — also accept the reversed form
14647+
/// `<expression> AS <identifier>`, which can be freely interleaved with
14648+
/// CTEs in the same comma-separated list.
14649+
pub fn parse_with_item(&mut self) -> Result<WithItem, ParserError> {
14650+
if !self.dialect.supports_with_clause_scalar_expression() {
14651+
return self.parse_cte().map(WithItem::Cte);
14652+
}
14653+
14654+
// CTE form must start with an identifier. If the leading token
14655+
// can't begin one (e.g. `42`, `(SELECT …)`, `(x, y) -> …`), this
14656+
// is unambiguously the named-expression form.
14657+
if matches!(self.peek_token().token, Token::Word(_)) {
14658+
if let Some(cte) = self.maybe_parse(|p| p.parse_cte())? {
14659+
return Ok(WithItem::Cte(cte));
14660+
}
14661+
}
14662+
14663+
let expr = self.parse_expr()?;
14664+
self.expect_keyword(Keyword::AS)?;
14665+
let alias = self.parse_identifier()?;
14666+
Ok(WithItem::Named { expr, alias })
14667+
}
14668+
1464214669
/// Parse a "query body", which is an expression with roughly the
1464314670
/// following grammar:
1464414671
/// ```sql

tests/sqlparser_clickhouse.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,70 @@ fn parse_inner_array_join() {
18451845
}
18461846
}
18471847

1848+
#[test]
1849+
fn parse_with_clause_named_expression() {
1850+
// Plain literal scalar.
1851+
clickhouse().verified_stmt("WITH 42 AS answer SELECT answer FROM t");
1852+
1853+
// String literal scalar from the ClickHouse docs.
1854+
clickhouse().verified_stmt(
1855+
"WITH '2019-08-01 15:23:00' AS ts_upper_bound SELECT * FROM hits \
1856+
WHERE EventDate = toDate(ts_upper_bound) AND EventTime <= ts_upper_bound",
1857+
);
1858+
1859+
// Aggregate function call as a named expression.
1860+
clickhouse().verified_stmt(
1861+
"WITH sum(bytes) AS s SELECT formatReadableSize(s), \"table\" \
1862+
FROM system.parts GROUP BY \"table\" ORDER BY s",
1863+
);
1864+
1865+
// Scalar subquery as the bound expression.
1866+
clickhouse().verified_stmt(
1867+
"WITH (SELECT sum(bytes) FROM system.parts WHERE active) AS total_disk_usage \
1868+
SELECT (sum(bytes) / total_disk_usage) * 100 AS table_disk_usage, \"table\" \
1869+
FROM system.parts GROUP BY \"table\" ORDER BY table_disk_usage DESC LIMIT 10",
1870+
);
1871+
1872+
// Bare-identifier scalar — disambiguation case (`name AS alias` looks like
1873+
// a CTE prefix but the missing `(` after `AS` makes it a named expression).
1874+
clickhouse().verified_stmt("WITH user_id AS uid SELECT uid FROM t");
1875+
1876+
// Mixing a named expression with a real CTE in the same WITH list.
1877+
clickhouse().verified_stmt("WITH 1 AS one, cte AS (SELECT 1) SELECT one FROM cte");
1878+
1879+
// Lambda as the bound expression (also taken from the docs).
1880+
clickhouse().verified_stmt(
1881+
"WITH '.txt' AS extension, (id, extension) -> concat(lower(id), extension) AS gen_name \
1882+
SELECT gen_name('test', '.sql') AS file_name",
1883+
);
1884+
}
1885+
1886+
#[test]
1887+
fn parse_with_clause_named_expression_ast() {
1888+
let query = clickhouse().verified_query("WITH 42 AS answer SELECT answer FROM t");
1889+
let with = query.with.as_ref().unwrap();
1890+
assert!(!with.recursive);
1891+
assert_eq!(with.items.len(), 1);
1892+
match &with.items[0] {
1893+
WithItem::Named { expr, alias } => {
1894+
assert_eq!(alias.value, "answer");
1895+
assert!(matches!(expr, Expr::Value(_)));
1896+
}
1897+
other => panic!("expected a named expression, got {other:?}"),
1898+
}
1899+
}
1900+
1901+
#[test]
1902+
fn parse_with_clause_named_expression_unsupported_in_other_dialects() {
1903+
// The named-expression form is only enabled for ClickHouse; other
1904+
// dialects should still reject `WITH 42 AS answer …`.
1905+
let res = sqlparser::parser::Parser::parse_sql(
1906+
&GenericDialect {},
1907+
"WITH 42 AS answer SELECT answer FROM t",
1908+
);
1909+
assert!(res.is_err(), "expected parse error, got {res:?}");
1910+
}
1911+
18481912
fn clickhouse() -> TestedDialects {
18491913
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
18501914
}

tests/sqlparser_common.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7854,18 +7854,21 @@ fn parse_ctes() {
78547854

78557855
fn assert_ctes_in_select(expected: &[&str], sel: &Query) {
78567856
for (i, exp) in expected.iter().enumerate() {
7857-
let Cte { alias, query, .. } = &sel.with.as_ref().unwrap().cte_tables[i];
7858-
assert_eq!(*exp, query.to_string());
7859-
assert_eq!(false, alias.explicit);
7857+
let cte = match &sel.with.as_ref().unwrap().items[i] {
7858+
WithItem::Cte(cte) => cte,
7859+
other => panic!("expected a CTE, got {other:?}"),
7860+
};
7861+
assert_eq!(*exp, cte.query.to_string());
7862+
assert_eq!(false, cte.alias.explicit);
78607863
assert_eq!(
78617864
if i == 0 {
78627865
Ident::new("a")
78637866
} else {
78647867
Ident::new("b")
78657868
},
7866-
alias.name
7869+
cte.alias.name
78677870
);
7868-
assert!(alias.columns.is_empty());
7871+
assert!(cte.alias.columns.is_empty());
78697872
}
78707873
}
78717874

@@ -7898,26 +7901,29 @@ fn parse_ctes() {
78987901
// CTE in a CTE...
78997902
let sql = &format!("WITH outer_cte AS ({with}) SELECT * FROM outer_cte");
79007903
let select = verified_query(sql);
7901-
assert_ctes_in_select(&cte_sqls, &only(&select.with.unwrap().cte_tables).query);
7904+
let with = select.with.as_ref().unwrap();
7905+
let outer_cte = match only(&with.items) {
7906+
WithItem::Cte(cte) => cte,
7907+
other => panic!("expected a CTE, got {other:?}"),
7908+
};
7909+
assert_ctes_in_select(&cte_sqls, &outer_cte.query);
79027910
}
79037911

79047912
#[test]
79057913
fn parse_cte_renamed_columns() {
79067914
let sql = "WITH cte (col1, col2) AS (SELECT foo, bar FROM baz) SELECT * FROM cte";
79077915
let query = all_dialects().verified_query(sql);
7916+
let with = query.with.unwrap();
7917+
let cte = match with.items.first().unwrap() {
7918+
WithItem::Cte(cte) => cte,
7919+
other => panic!("expected a CTE, got {other:?}"),
7920+
};
79087921
assert_eq!(
79097922
vec![
79107923
TableAliasColumnDef::from_name("col1"),
79117924
TableAliasColumnDef::from_name("col2")
79127925
],
7913-
query
7914-
.with
7915-
.unwrap()
7916-
.cte_tables
7917-
.first()
7918-
.unwrap()
7919-
.alias
7920-
.columns
7926+
cte.alias.columns
79217927
);
79227928
}
79237929

@@ -7931,8 +7937,8 @@ fn parse_recursive_cte() {
79317937

79327938
let with = query.with.as_ref().unwrap();
79337939
assert!(with.recursive);
7934-
assert_eq!(with.cte_tables.len(), 1);
7935-
let expected = Cte {
7940+
assert_eq!(with.items.len(), 1);
7941+
let expected = WithItem::Cte(Cte {
79367942
alias: TableAlias {
79377943
explicit: false,
79387944
name: Ident {
@@ -7947,8 +7953,8 @@ fn parse_recursive_cte() {
79477953
from: None,
79487954
materialized: None,
79497955
closing_paren_token: AttachedToken::empty(),
7950-
};
7951-
assert_eq!(with.cte_tables.first().unwrap(), &expected);
7956+
});
7957+
assert_eq!(with.items.first().unwrap(), &expected);
79527958
}
79537959

79547960
#[test]

0 commit comments

Comments
 (0)