Skip to content

[ClickHouse] Add support for WITH FILL to OrderByExpr #1330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 9 additions & 8 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ pub use self::operator::{BinaryOperator, UnaryOperator};
pub use self::query::{
AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml,
GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint,
JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType,
MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr,
NonBlock, Offset, OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem,
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, SymbolDefinition, Table,
TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode,
Values, WildcardAdditionalOptions, With,
GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem, Interpolation,
InterpolationArg, Join, JoinConstraint, JoinOperator, JsonTableColumn,
JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern,
MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset,
OffsetRows, OrderByExpr, PivotValueSource, Query, RenameSelectItem, RepetitionQuantifier,
ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr,
SetOperator, SetQuantifier, SymbolDefinition, Table, TableAlias, TableFactor, TableVersion,
TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With,
WithFill,
};
pub use self::value::{
escape_double_quote_string, escape_quoted_string, DateTimeField, DollarQuotedString,
Expand Down
80 changes: 80 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,14 @@ pub struct OrderByExpr {
pub asc: Option<bool>,
/// Optional `NULLS FIRST` or `NULLS LAST`
pub nulls_first: Option<bool>,
/// Optional: `WITH FILL`
/// Supported by [ClickHouse syntax]: <https://clickhouse.com/docs/en/sql-reference/statements/select/order-by#order-by-expr-with-fill-modifier>
pub with_fill: Option<WithFill>,
/// Optional: `INTERPOLATE`
/// Supported by [ClickHouse syntax]
///
/// [ClickHouse syntax]: <https://clickhouse.com/docs/en/sql-reference/statements/select/order-by#order-by-expr-with-fill-modifier>
pub interpolate: Option<InterpolationArg>,
}

impl fmt::Display for OrderByExpr {
Expand All @@ -1642,6 +1650,78 @@ impl fmt::Display for OrderByExpr {
Some(false) => write!(f, " NULLS LAST")?,
None => (),
}
if let Some(ref with_fill) = self.with_fill {
write!(f, " {}", with_fill)?
}
if let Some(ref interpolate) = self.interpolate {
match interpolate {
InterpolationArg::NoBody => write!(f, " INTERPOLATE")?,
InterpolationArg::EmptyBody => write!(f, " INTERPOLATE ()")?,
InterpolationArg::Columns(columns) => {
write!(f, " INTERPOLATE ({})", display_comma_separated(columns))?;
}
}
}
Ok(())
}
}

/// ClickHouse `WITH FILL` modifier for `ORDER BY` clause.
/// Supported by [ClickHouse syntax]
///
/// [ClickHouse syntax]: <https://clickhouse.com/docs/en/sql-reference/statements/select/order-by#order-by-expr-with-fill-modifier>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct WithFill {
pub from: Option<Expr>,
pub to: Option<Expr>,
pub step: Option<Expr>,
}

impl fmt::Display for WithFill {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "WITH FILL")?;
if let Some(ref from) = self.from {
write!(f, " FROM {}", from)?;
}
if let Some(ref to) = self.to {
write!(f, " TO {}", to)?;
}
if let Some(ref step) = self.step {
write!(f, " STEP {}", step)?;
}
Ok(())
}
}

/// ClickHouse `INTERPOLATE` clause for use in `ORDER BY` clause when using `WITH FILL` modifier.
/// Supported by [ClickHouse syntax]
///
/// [ClickHouse syntax]: <https://clickhouse.com/docs/en/sql-reference/statements/select/order-by#order-by-expr-with-fill-modifier>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct Interpolation {
pub column: Expr,
pub formula: Option<Expr>,
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum InterpolationArg {
NoBody,
EmptyBody,
Columns(Vec<Interpolation>),
}

impl fmt::Display for Interpolation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.column)?;
if let Some(ref formula) = self.formula {
write!(f, " AS {}", formula)?;
}
Ok(())
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ define_keywords!(
FILE,
FILES,
FILE_FORMAT,
FILL,
FILTER,
FIRST,
FIRST_VALUE,
Expand Down Expand Up @@ -382,6 +383,7 @@ define_keywords!(
INT64,
INT8,
INTEGER,
INTERPOLATE,
INTERSECT,
INTERSECTION,
INTERVAL,
Expand Down Expand Up @@ -678,6 +680,7 @@ define_keywords!(
STDDEV_SAMP,
STDIN,
STDOUT,
STEP,
STORAGE_INTEGRATION,
STORED,
STRICT,
Expand Down
73 changes: 73 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10408,13 +10408,86 @@ impl<'a> Parser<'a> {
None
};

let with_fill = if dialect_of!(self is ClickHouseDialect | GenericDialect)
&& self.parse_keywords(&[Keyword::WITH, Keyword::FILL])
{
Some(self.parse_with_fill()?)
} else {
None
};

let interpolate = if dialect_of!(self is ClickHouseDialect | GenericDialect)
&& self.parse_keyword(Keyword::INTERPOLATE)
{
if self.consume_token(&Token::LParen) {
if self.peek_token().token == Token::RParen {
// INTERPOLATE ()
self.next_token();
Some(InterpolationArg::EmptyBody)
} else {
// INTERPOLATE ( ... )
let interpolations = self.parse_interpolations()?;
self.expect_token(&Token::RParen)?;
Some(InterpolationArg::Columns(interpolations))
}
} else {
// INTERPOLATE
Some(InterpolationArg::NoBody)
}
} else {
None
};

Ok(OrderByExpr {
expr,
asc,
nulls_first,
with_fill,
interpolate,
})
}

// Parse a WITH FILL clause (ClickHouse dialect)
// that follow the WITH FILL keywords in a ORDER BY clause
pub fn parse_with_fill(&mut self) -> Result<WithFill, ParserError> {
let from = if self.parse_keyword(Keyword::FROM) {
Some(self.parse_expr()?)
} else {
None
};

let to = if self.parse_keyword(Keyword::TO) {
Some(self.parse_expr()?)
} else {
None
};

let step = if self.parse_keyword(Keyword::STEP) {
Some(self.parse_expr()?)
} else {
None
};

Ok(WithFill { from, to, step })
}

// Parse a set of comma seperated INTERPOLATE expressions (ClickHouse dialect)
// that follow the INTERPOLATE keyword in an ORDER BY clause with the WITH FILL modifier
pub fn parse_interpolations(&mut self) -> Result<Vec<Interpolation>, ParserError> {
self.parse_comma_separated(|p| p.parse_interpolation())
}

// Parse a INTERPOLATE expression (ClickHouse dialect)
pub fn parse_interpolation(&mut self) -> Result<Interpolation, ParserError> {
let column = self.parse_expr()?;
let formula = if self.parse_keyword(Keyword::AS) {
Some(self.parse_expr()?)
} else {
None
};
Ok(Interpolation { column, formula })
}

/// Parse a TOP clause, MSSQL equivalent of LIMIT,
/// that follows after `SELECT [DISTINCT]`.
pub fn parse_top(&mut self) -> Result<Top, ParserError> {
Expand Down
113 changes: 113 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,119 @@ fn parse_group_by_with_modifier() {
}
}

#[test]
fn parse_select_order_by_with_fill_interpolate() {
let sql = "SELECT id, fname, lname FROM customer WHERE id < 5 \
ORDER BY \
fname ASC NULLS FIRST WITH FILL FROM 10 TO 20 STEP 2, \
lname DESC NULLS LAST WITH FILL FROM 30 TO 40 STEP 3 \
INTERPOLATE (col1 AS col1 + 1) \
LIMIT 2";
let select = clickhouse().verified_query(sql);
assert_eq!(
vec![
OrderByExpr {
expr: Expr::Identifier(Ident::new("fname")),
asc: Some(true),
nulls_first: Some(true),
with_fill: Some(WithFill {
from: Some(Expr::Value(number("10"))),
to: Some(Expr::Value(number("20"))),
step: Some(Expr::Value(number("2"))),
}),
interpolate: None,
},
OrderByExpr {
expr: Expr::Identifier(Ident::new("lname")),
asc: Some(false),
nulls_first: Some(false),
with_fill: Some(WithFill {
from: Some(Expr::Value(number("30"))),
to: Some(Expr::Value(number("40"))),
step: Some(Expr::Value(number("3"))),
}),
interpolate: Some(InterpolationArg::Columns(vec![Interpolation {
column: Expr::Identifier(Ident::new("col1")),
formula: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col1"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::Value(number("1"))),
}),
}]))
},
],
select.order_by
);
assert_eq!(Some(Expr::Value(number("2"))), select.limit);
}

#[test]
fn parse_with_fill() {
let sql = "SELECT fname FROM customer \
ORDER BY fname WITH FILL FROM 10 TO 20 STEP 2";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(WithFill {
from: Some(Expr::Value(number("10"))),
to: Some(Expr::Value(number("20"))),
step: Some(Expr::Value(number("2"))),
}),
select.order_by[0].with_fill
);
}

#[test]
fn parse_interpolation_body_with_columns() {
let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL \
INTERPOLATE (col1 AS col1 + 1, col2 AS col3, col4 AS col4 + 4)";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(InterpolationArg::Columns(vec![
Interpolation {
column: Expr::Identifier(Ident::new("col1")),
formula: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col1"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::Value(number("1"))),
}),
},
Interpolation {
column: Expr::Identifier(Ident::new("col2")),
formula: Some(Expr::Identifier(Ident::new("col3"))),
},
Interpolation {
column: Expr::Identifier(Ident::new("col4")),
formula: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col4"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::Value(number("4"))),
}),
},
])),
select.order_by[0].interpolate
);
}

#[test]
fn parse_interpolation_without_body() {
let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(InterpolationArg::NoBody),
select.order_by[0].interpolate
);
}

#[test]
fn parse_interpolation_with_empty_body() {
let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE ()";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(InterpolationArg::EmptyBody),
select.order_by[0].interpolate
);
}

fn clickhouse() -> TestedDialects {
TestedDialects {
dialects: vec![Box::new(ClickHouseDialect {})],
Expand Down
Loading