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
15 changes: 15 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,13 @@ impl fmt::Display for Join {
self.relation,
suffix(constraint)
)),
JoinOperator::ArrayJoin => f.write_fmt(format_args!("ARRAY JOIN {}", self.relation)),
JoinOperator::LeftArrayJoin => {
f.write_fmt(format_args!("LEFT ARRAY JOIN {}", self.relation))
}
JoinOperator::InnerArrayJoin => {
f.write_fmt(format_args!("INNER ARRAY JOIN {}", self.relation))
}
}
}
}
Expand Down Expand Up @@ -2824,6 +2831,14 @@ pub enum JoinOperator {
///
/// See <https://dev.mysql.com/doc/refman/8.4/en/join.html>.
StraightJoin(JoinConstraint),
/// ClickHouse: `ARRAY JOIN` for unnesting arrays inline.
///
/// See <https://clickhouse.com/docs/en/sql-reference/statements/select/array-join>.
ArrayJoin,
/// ClickHouse: `LEFT ARRAY JOIN` for unnesting arrays inline (preserves rows with empty arrays).
LeftArrayJoin,
/// ClickHouse: `INNER ARRAY JOIN` for unnesting arrays inline (filters rows with empty arrays).
InnerArrayJoin,
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
Expand Down
3 changes: 3 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,9 @@ impl Spanned for JoinOperator {
JoinOperator::Anti(join_constraint) => join_constraint.span(),
JoinOperator::Semi(join_constraint) => join_constraint.span(),
JoinOperator::StraightJoin(join_constraint) => join_constraint.span(),
JoinOperator::ArrayJoin => Span::empty(),
JoinOperator::LeftArrayJoin => Span::empty(),
JoinOperator::InnerArrayJoin => Span::empty(),
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ impl Dialect for ClickHouseDialect {
true
}

fn supports_partition_by_after_order_by(&self) -> bool {
true
}

fn supports_array_join_syntax(&self) -> bool {
true
}

// ClickHouse uses this for some FORMAT expressions in `INSERT` context, e.g. when inserting
// with FORMAT JSONEachRow a raw JSON key-value expression is valid and expected.
//
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ impl Dialect for GenericDialect {
true
}

fn supports_partition_by_after_order_by(&self) -> bool {
true
}

fn supports_array_join_syntax(&self) -> bool {
true
}

fn supports_group_by_expr(&self) -> bool {
true
}
Expand Down
17 changes: 17 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,23 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports `PARTITION BY` appearing after `ORDER BY`
/// in a `CREATE TABLE` statement (in addition to the standard placement before `ORDER BY`).
///
/// ClickHouse DDL uses this ordering:
/// <https://clickhouse.com/docs/en/sql-reference/statements/create/table#partition-by>
fn supports_partition_by_after_order_by(&self) -> bool {
false
}

/// Returns true if the dialect supports ClickHouse-style `ARRAY JOIN` / `LEFT ARRAY JOIN` /
/// `INNER ARRAY JOIN` syntax for unnesting arrays inline.
///
/// <https://clickhouse.com/docs/en/sql-reference/statements/select/array-join>
fn supports_array_join_syntax(&self) -> bool {
false
}

/// Returns true if the dialects supports `group sets, roll up, or cube` expressions.
fn supports_group_by_expr(&self) -> bool {
false
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
Keyword::FOR,
// for MYSQL PARTITION SELECTION
Keyword::PARTITION,
// for Clickhouse ARRAY JOIN (ARRAY must not be parsed as a table alias)
Keyword::ARRAY,
// for Clickhouse PREWHERE
Keyword::PREWHERE,
Keyword::SETTINGS,
Expand Down
209 changes: 118 additions & 91 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,10 @@ impl<'a> Parser<'a> {
Token::EOF => break,

// end of statement
Token::Word(word) => {
if expecting_statement_delimiter && word.keyword == Keyword::END {
break;
}
Token::Word(word)
if expecting_statement_delimiter && word.keyword == Keyword::END =>
{
break;
}
_ => {}
}
Expand Down Expand Up @@ -1305,41 +1305,40 @@ impl<'a> Parser<'a> {

let next_token = self.next_token();
match next_token.token {
t @ (Token::Word(_) | Token::SingleQuotedString(_)) => {
if self.peek_token_ref().token == Token::Period {
let mut id_parts: Vec<Ident> = vec![match t {
Token::Word(w) => w.into_ident(next_token.span),
Token::SingleQuotedString(s) => Ident::with_quote('\'', s),
_ => {
return Err(ParserError::ParserError(
"Internal parser error: unexpected token type".to_string(),
))
t @ (Token::Word(_) | Token::SingleQuotedString(_))
if self.peek_token_ref().token == Token::Period =>
{
let mut id_parts: Vec<Ident> = vec![match t {
Token::Word(w) => w.into_ident(next_token.span),
Token::SingleQuotedString(s) => Ident::with_quote('\'', s),
_ => {
return Err(ParserError::ParserError(
"Internal parser error: unexpected token type".to_string(),
))
}
}];

while self.consume_token(&Token::Period) {
let next_token = self.next_token();
match next_token.token {
Token::Word(w) => id_parts.push(w.into_ident(next_token.span)),
Token::SingleQuotedString(s) => {
// SQLite has single-quoted identifiers
id_parts.push(Ident::with_quote('\'', s))
}
}];

while self.consume_token(&Token::Period) {
let next_token = self.next_token();
match next_token.token {
Token::Word(w) => id_parts.push(w.into_ident(next_token.span)),
Token::SingleQuotedString(s) => {
// SQLite has single-quoted identifiers
id_parts.push(Ident::with_quote('\'', s))
}
Token::Placeholder(s) => {
// Snowflake uses $1, $2, etc. for positional column references
// in staged data queries like: SELECT t.$1 FROM @stage t
id_parts.push(Ident::new(s))
}
Token::Mul => {
return Ok(Expr::QualifiedWildcard(
ObjectName::from(id_parts),
AttachedToken(next_token),
));
}
_ => {
return self
.expected("an identifier or a '*' after '.'", next_token);
}
Token::Placeholder(s) => {
// Snowflake uses $1, $2, etc. for positional column references
// in staged data queries like: SELECT t.$1 FROM @stage t
id_parts.push(Ident::new(s))
}
Token::Mul => {
return Ok(Expr::QualifiedWildcard(
ObjectName::from(id_parts),
AttachedToken(next_token),
));
}
_ => {
return self.expected("an identifier or a '*' after '.'", next_token);
}
}
}
Expand Down Expand Up @@ -5031,10 +5030,10 @@ impl<'a> Parser<'a> {
loop {
match &self.peek_nth_token_ref(0).token {
Token::EOF => break,
Token::Word(w) => {
if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) {
break;
}
Token::Word(w)
if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) =>
{
break;
}
_ => {}
}
Expand Down Expand Up @@ -8377,70 +8376,60 @@ impl<'a> Parser<'a> {
Keyword::LINES,
Keyword::NULL,
]) {
Some(Keyword::FIELDS) => {
if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) {
Some(Keyword::FIELDS)
if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) =>
{
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::FieldsTerminatedBy,
char: self.parse_identifier()?,
});

if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::FieldsTerminatedBy,
delimiter: HiveDelimiter::FieldsEscapedBy,
char: self.parse_identifier()?,
});

if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::FieldsEscapedBy,
char: self.parse_identifier()?,
});
}
} else {
break;
}
}
Some(Keyword::COLLECTION) => {
Some(Keyword::COLLECTION)
if self.parse_keywords(&[
Keyword::ITEMS,
Keyword::TERMINATED,
Keyword::BY,
]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::CollectionItemsTerminatedBy,
char: self.parse_identifier()?,
});
} else {
break;
}
]) =>
{
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::CollectionItemsTerminatedBy,
char: self.parse_identifier()?,
});
}
Some(Keyword::MAP) => {
Some(Keyword::MAP)
if self.parse_keywords(&[
Keyword::KEYS,
Keyword::TERMINATED,
Keyword::BY,
]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::MapKeysTerminatedBy,
char: self.parse_identifier()?,
});
} else {
break;
}
]) =>
{
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::MapKeysTerminatedBy,
char: self.parse_identifier()?,
});
}
Some(Keyword::LINES) => {
if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::LinesTerminatedBy,
char: self.parse_identifier()?,
});
} else {
break;
}
Some(Keyword::LINES)
if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) =>
{
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::LinesTerminatedBy,
char: self.parse_identifier()?,
});
}
Some(Keyword::NULL) => {
if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) {
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::NullDefinedAs,
char: self.parse_identifier()?,
});
} else {
break;
}
Some(Keyword::NULL)
if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) =>
{
row_delimiters.push(HiveRowDelimiter {
delimiter: HiveDelimiter::NullDefinedAs,
char: self.parse_identifier()?,
});
}
_ => {
break;
Expand Down Expand Up @@ -8564,6 +8553,17 @@ impl<'a> Parser<'a> {
None
};

// ClickHouse allows PARTITION BY after ORDER BY
// https://clickhouse.com/docs/en/sql-reference/statements/create/table#partition-by
let partition_by = if create_table_config.partition_by.is_none()
&& self.dialect.supports_partition_by_after_order_by()
&& self.parse_keywords(&[Keyword::PARTITION, Keyword::BY])
{
Some(Box::new(self.parse_expr()?))
} else {
create_table_config.partition_by
};

let on_commit = if self.parse_keywords(&[Keyword::ON, Keyword::COMMIT]) {
Some(self.parse_create_table_on_commit()?)
} else {
Expand Down Expand Up @@ -8634,7 +8634,7 @@ impl<'a> Parser<'a> {
.on_commit(on_commit)
.on_cluster(on_cluster)
.clustered_by(clustered_by)
.partition_by(create_table_config.partition_by)
.partition_by(partition_by)
.cluster_by(create_table_config.cluster_by)
.inherits(create_table_config.inherits)
.partition_of(partition_of)
Expand Down Expand Up @@ -15738,6 +15738,33 @@ impl<'a> Parser<'a> {
constraint: self.parse_join_constraint(false)?,
},
}
} else if self.dialect.supports_array_join_syntax()
&& self.parse_keywords(&[Keyword::INNER, Keyword::ARRAY, Keyword::JOIN])
{
// ClickHouse: INNER ARRAY JOIN
Join {
relation: self.parse_table_factor()?,
global,
join_operator: JoinOperator::InnerArrayJoin,
}
} else if self.dialect.supports_array_join_syntax()
&& self.parse_keywords(&[Keyword::LEFT, Keyword::ARRAY, Keyword::JOIN])
{
// ClickHouse: LEFT ARRAY JOIN
Join {
relation: self.parse_table_factor()?,
global,
join_operator: JoinOperator::LeftArrayJoin,
}
} else if self.dialect.supports_array_join_syntax()
&& self.parse_keywords(&[Keyword::ARRAY, Keyword::JOIN])
{
// ClickHouse: ARRAY JOIN
Join {
relation: self.parse_table_factor()?,
global,
join_operator: JoinOperator::ArrayJoin,
}
} else {
let natural = self.parse_keyword(Keyword::NATURAL);
let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token {
Expand Down
Loading