blob: 7cf30cce9f76adfc1ce90caed7757dc3d9c6bb66 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#![warn(clippy::all)]
//! Test SQL syntax specific to ClickHouse.
#[macro_use]
mod test_utils;
use helpers::attached_token::AttachedToken;
use sqlparser::tokenizer::Span;
use test_utils::*;
use sqlparser::ast::Expr::{BinaryOp, Identifier};
use sqlparser::ast::SelectItem::UnnamedExpr;
use sqlparser::ast::TableFactor::Table;
use sqlparser::ast::Value::Boolean;
use sqlparser::ast::*;
use sqlparser::dialect::ClickHouseDialect;
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::ParserError::ParserError;
#[test]
fn parse_map_access_expr() {
let sql = r#"SELECT string_values[indexOf(string_names, 'endpoint')] FROM foos WHERE id = 'test' AND string_value[indexOf(string_name, 'app')] <> 'foo'"#;
let select = clickhouse().verified_only_select(sql);
assert_eq!(
Select {
select_token: AttachedToken::empty(),
optimizer_hints: vec![],
distinct: None,
select_modifiers: None,
top: None,
top_before_distinct: false,
projection: vec![UnnamedExpr(Expr::CompoundFieldAccess {
root: Box::new(Identifier(Ident {
value: "string_values".to_string(),
quote_style: None,
span: Span::empty(),
})),
access_chain: vec![AccessExpr::Subscript(Subscript::Index {
index: call(
"indexOf",
[
Expr::Identifier(Ident::new("string_names")),
Expr::value(Value::SingleQuotedString("endpoint".to_string()))
]
),
})],
})],
exclude: None,
into: None,
from: vec![TableWithJoins {
relation: table_from_name(ObjectName::from(vec![Ident::new("foos")])),
joins: vec![],
}],
lateral_views: vec![],
prewhere: None,
selection: Some(BinaryOp {
left: Box::new(BinaryOp {
left: Box::new(Identifier(Ident::new("id"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::value(Value::SingleQuotedString("test".to_string()))),
}),
op: BinaryOperator::And,
right: Box::new(BinaryOp {
left: Box::new(Expr::CompoundFieldAccess {
root: Box::new(Identifier(Ident::new("string_value"))),
access_chain: vec![AccessExpr::Subscript(Subscript::Index {
index: call(
"indexOf",
[
Expr::Identifier(Ident::new("string_name")),
Expr::value(Value::SingleQuotedString("app".to_string()))
]
),
})],
}),
op: BinaryOperator::NotEq,
right: Box::new(Expr::value(Value::SingleQuotedString("foo".to_string()))),
}),
}),
group_by: GroupByExpr::Expressions(vec![], vec![]),
cluster_by: vec![],
distribute_by: vec![],
sort_by: vec![],
having: None,
named_window: vec![],
window_before_qualify: false,
qualify: None,
value_table_mode: None,
connect_by: vec![],
flavor: SelectFlavor::Standard,
},
select
);
}
#[test]
fn parse_array_expr() {
let sql = "SELECT ['1', '2'] FROM test";
let select = clickhouse().verified_only_select(sql);
assert_eq!(
&Expr::Array(Array {
elem: vec![
Expr::value(Value::SingleQuotedString("1".to_string())),
Expr::value(Value::SingleQuotedString("2".to_string())),
],
named: false,
}),
expr_from_projection(only(&select.projection))
)
}
#[test]
fn parse_array_fn() {
let sql = "SELECT array(x1, x2) FROM foo";
let select = clickhouse().verified_only_select(sql);
assert_eq!(
&call(
"array",
[
Expr::Identifier(Ident::new("x1")),
Expr::Identifier(Ident::new("x2"))
]
),
expr_from_projection(only(&select.projection))
);
}
#[test]
fn parse_kill() {
let stmt = clickhouse().verified_stmt("KILL MUTATION 5");
assert_eq!(
stmt,
Statement::Kill {
modifier: Some(KillType::Mutation),
id: 5,
}
);
}
#[test]
fn parse_delimited_identifiers() {
// check that quoted identifiers in any position remain quoted after serialization
let select = clickhouse().verified_only_select(
r#"SELECT "alias"."bar baz", "myfun"(), "simple id" AS "column alias" FROM "a table" AS "alias""#,
);
// check FROM
match only(select.from).relation {
TableFactor::Table {
name,
alias,
args,
with_hints,
version,
..
} => {
assert_eq!(
ObjectName::from(vec![Ident::with_quote('"', "a table")]),
name
);
assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name);
assert!(args.is_none());
assert!(with_hints.is_empty());
assert!(version.is_none());
}
_ => panic!("Expecting TableFactor::Table"),
}
// check SELECT
assert_eq!(3, select.projection.len());
assert_eq!(
&Expr::CompoundIdentifier(vec![
Ident::with_quote('"', "alias"),
Ident::with_quote('"', "bar baz"),
]),
expr_from_projection(&select.projection[0]),
);
assert_eq!(
&Expr::Function(Function {
name: ObjectName::from(vec![Ident::with_quote('"', "myfun")]),
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
args: FunctionArguments::List(FunctionArgumentList {
duplicate_treatment: None,
args: vec![],
clauses: vec![],
}),
null_treatment: None,
filter: None,
over: None,
within_group: vec![],
}),
expr_from_projection(&select.projection[1]),
);
match &select.projection[2] {
SelectItem::ExprWithAlias { expr, alias } => {
assert_eq!(&Expr::Identifier(Ident::with_quote('"', "simple id")), expr);
assert_eq!(&Ident::with_quote('"', "column alias"), alias);
}
_ => panic!("Expected ExprWithAlias"),
}
clickhouse().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#);
clickhouse().verified_stmt(r#"ALTER TABLE foo ADD CONSTRAINT "bar" PRIMARY KEY (baz)"#);
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
}
#[test]
fn parse_create_table() {
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY ("x")"#);
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x""#);
clickhouse().verified_stmt(
r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY "x" AS SELECT * FROM "t" WHERE true"#,
);
clickhouse().one_statement_parses_to(
"CREATE TABLE x (a int) ENGINE = MergeTree() ORDER BY a",
"CREATE TABLE x (a INT) ENGINE = MergeTree ORDER BY a",
);
}
#[test]
fn parse_create_table_partition_by_after_order_by() {
// ClickHouse DDL places PARTITION BY after ORDER BY.
// MergeTree() is canonicalized to MergeTree and type names are uppercased.
clickhouse().one_statement_parses_to(
concat!(
"CREATE TABLE IF NOT EXISTS \"MyTable\" (`col1` Int64, `col2` Int32) ",
"ENGINE = MergeTree() ",
"PRIMARY KEY (toDate(toDateTime(`col2`)), `col1`, `col2`) ",
"ORDER BY (toDate(toDateTime(`col2`)), `col1`, `col2`) ",
"PARTITION BY col1 % 64"
),
concat!(
"CREATE TABLE IF NOT EXISTS \"MyTable\" (`col1` INT64, `col2` Int32) ",
"ENGINE = MergeTree ",
"PRIMARY KEY (toDate(toDateTime(`col2`)), `col1`, `col2`) ",
"ORDER BY (toDate(toDateTime(`col2`)), `col1`, `col2`) ",
"PARTITION BY col1 % 64"
),
);
// PARTITION BY after ORDER BY works with both ClickHouseDialect and GenericDialect
clickhouse_and_generic()
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a");
// Arithmetic expression in PARTITION BY (roundtrip)
clickhouse_and_generic()
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a % 64");
// AST: partition_by is populated with the correct expression
match clickhouse_and_generic()
.verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY a % 64")
{
Statement::CreateTable(CreateTable { partition_by, .. }) => {
assert_eq!(
partition_by,
Some(Box::new(BinaryOp {
left: Box::new(Identifier(Ident::new("a"))),
op: BinaryOperator::Modulo,
right: Box::new(Expr::Value(
Value::Number("64".parse().unwrap(), false).with_empty_span(),
)),
}))
);
}
_ => unreachable!(),
}
// Function call expression in PARTITION BY (ClickHouse-specific function)
clickhouse().verified_stmt(
"CREATE TABLE t (d DATE) ENGINE = MergeTree ORDER BY d PARTITION BY toYYYYMM(d)",
);
// Negative: PARTITION BY with no expression should fail
clickhouse_and_generic()
.parse_sql_statements("CREATE TABLE t (a INT) ENGINE = MergeTree ORDER BY a PARTITION BY")
.expect_err("PARTITION BY with no expression should fail");
}
#[test]
fn parse_insert_into_function() {
clickhouse().verified_stmt(r#"INSERT INTO TABLE FUNCTION remote('localhost', default.simple_table) VALUES (100, 'inserted via remote()')"#);
clickhouse().verified_stmt(r#"INSERT INTO FUNCTION remote('localhost', default.simple_table) VALUES (100, 'inserted via remote()')"#);
}
#[test]
fn parse_alter_table_attach_and_detach_partition() {
for operation in &["ATTACH", "DETACH"] {
match clickhouse_and_generic()
.verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str())
{
Statement::AlterTable(alter_table) => {
pretty_assertions::assert_eq!("t0", alter_table.name.to_string());
pretty_assertions::assert_eq!(
alter_table.operations[0],
if operation == &"ATTACH" {
AlterTableOperation::AttachPartition {
partition: Partition::Expr(Identifier(Ident::new("part"))),
}
} else {
AlterTableOperation::DetachPartition {
partition: Partition::Expr(Identifier(Ident::new("part"))),
}
}
);
}
_ => unreachable!(),
}
match clickhouse_and_generic()
.verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str())
{
Statement::AlterTable(AlterTable {
name, operations, ..
}) => {
pretty_assertions::assert_eq!("t1", name.to_string());
pretty_assertions::assert_eq!(
operations[0],
if operation == &"ATTACH" {
AlterTableOperation::AttachPartition {
partition: Partition::Part(Identifier(Ident::new("part"))),
}
} else {
AlterTableOperation::DetachPartition {
partition: Partition::Part(Identifier(Ident::new("part"))),
}
}
);
}
_ => unreachable!(),
}
// negative cases
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(format!("ALTER TABLE t0 {operation} PARTITION").as_str())
.unwrap_err(),
ParserError("Expected: an expression, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(format!("ALTER TABLE t0 {operation} PART").as_str())
.unwrap_err(),
ParserError("Expected: an expression, found: EOF".to_string())
);
}
}
#[test]
fn parse_alter_table_add_projection() {
match clickhouse_and_generic().verified_stmt(concat!(
"ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name",
" (SELECT a, b GROUP BY a ORDER BY b)",
)) {
Statement::AlterTable(AlterTable {
name, operations, ..
}) => {
assert_eq!(name, ObjectName::from(vec!["t0".into()]));
assert_eq!(1, operations.len());
assert_eq!(
operations[0],
AlterTableOperation::AddProjection {
if_not_exists: true,
name: "my_name".into(),
select: ProjectionSelect {
projection: vec![
UnnamedExpr(Identifier(Ident::new("a"))),
UnnamedExpr(Identifier(Ident::new("b"))),
],
group_by: Some(GroupByExpr::Expressions(
vec![Identifier(Ident::new("a"))],
vec![]
)),
order_by: Some(OrderBy {
kind: OrderByKind::Expressions(vec![OrderByExpr {
expr: Identifier(Ident::new("b")),
options: OrderByOptions {
sort: None,
nulls_first: None,
},
with_fill: None,
}]),
interpolate: None,
}),
}
}
)
}
_ => unreachable!(),
}
// leave out IF NOT EXISTS is allowed
clickhouse_and_generic()
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a ORDER BY b)");
// leave out GROUP BY is allowed
clickhouse_and_generic()
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b ORDER BY b)");
// leave out ORDER BY is allowed
clickhouse_and_generic()
.verified_stmt("ALTER TABLE t0 ADD PROJECTION my_name (SELECT a, b GROUP BY a)");
// missing select query is not allowed
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name")
.unwrap_err(),
ParserError("Expected: (, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name ()")
.unwrap_err(),
ParserError("Expected: SELECT, found: )".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("ALTER TABLE t0 ADD PROJECTION my_name (SELECT)")
.unwrap_err(),
ParserError("Expected: an expression, found: )".to_string())
);
}
#[test]
fn parse_alter_table_drop_projection() {
match clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION IF EXISTS my_name")
{
Statement::AlterTable(AlterTable {
name, operations, ..
}) => {
assert_eq!(name, ObjectName::from(vec!["t0".into()]));
assert_eq!(1, operations.len());
assert_eq!(
operations[0],
AlterTableOperation::DropProjection {
if_exists: true,
name: "my_name".into(),
}
)
}
_ => unreachable!(),
}
// allow to skip `IF EXISTS`
clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION my_name");
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("ALTER TABLE t0 DROP PROJECTION")
.unwrap_err(),
ParserError("Expected: identifier, found: EOF".to_string())
);
}
#[test]
fn parse_alter_table_clear_and_materialize_projection() {
for keyword in ["CLEAR", "MATERIALIZE"] {
match clickhouse_and_generic().verified_stmt(
format!("ALTER TABLE t0 {keyword} PROJECTION IF EXISTS my_name IN PARTITION p0",)
.as_str(),
) {
Statement::AlterTable(AlterTable {
name, operations, ..
}) => {
assert_eq!(name, ObjectName::from(vec!["t0".into()]));
assert_eq!(1, operations.len());
assert_eq!(
operations[0],
if keyword == "CLEAR" {
AlterTableOperation::ClearProjection {
if_exists: true,
name: "my_name".into(),
partition: Some(Ident::new("p0")),
}
} else {
AlterTableOperation::MaterializeProjection {
if_exists: true,
name: "my_name".into(),
partition: Some(Ident::new("p0")),
}
}
)
}
_ => unreachable!(),
}
// allow to skip `IF EXISTS`
clickhouse_and_generic().verified_stmt(
format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN PARTITION p0",).as_str(),
);
// allow to skip `IN PARTITION partition_name`
clickhouse_and_generic()
.verified_stmt(format!("ALTER TABLE t0 {keyword} PROJECTION my_name",).as_str());
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(format!("ALTER TABLE t0 {keyword} PROJECTION",).as_str())
.unwrap_err(),
ParserError("Expected: identifier, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(
format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN PARTITION",).as_str()
)
.unwrap_err(),
ParserError("Expected: identifier, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(
format!("ALTER TABLE t0 {keyword} PROJECTION my_name IN",).as_str()
)
.unwrap_err(),
ParserError("Expected: end of statement, found: IN".to_string())
);
}
}
#[test]
fn parse_optimize_table() {
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE db.t0");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 ON CLUSTER 'cluster'");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 ON CLUSTER 'cluster' FINAL");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 FINAL DEDUPLICATE");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 DEDUPLICATE");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 DEDUPLICATE BY id");
clickhouse_and_generic().verified_stmt("OPTIMIZE TABLE t0 FINAL DEDUPLICATE BY id");
clickhouse_and_generic()
.verified_stmt("OPTIMIZE TABLE t0 PARTITION tuple('2023-04-22') DEDUPLICATE BY id");
match clickhouse_and_generic().verified_stmt(
"OPTIMIZE TABLE t0 ON CLUSTER cluster PARTITION ID '2024-07' FINAL DEDUPLICATE BY id",
) {
Statement::OptimizeTable {
name,
on_cluster,
partition,
include_final,
deduplicate,
..
} => {
assert_eq!(name.to_string(), "t0");
assert_eq!(on_cluster, Some(Ident::new("cluster")));
assert_eq!(
partition,
Some(Partition::Identifier(Ident::with_quote('\'', "2024-07")))
);
assert!(include_final);
assert_eq!(
deduplicate,
Some(Deduplicate::ByExpression(Identifier(Ident::new("id"))))
);
}
_ => unreachable!(),
}
// negative cases
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("OPTIMIZE TABLE t0 DEDUPLICATE BY")
.unwrap_err(),
ParserError("Expected: an expression, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("OPTIMIZE TABLE t0 PARTITION")
.unwrap_err(),
ParserError("Expected: an expression, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements("OPTIMIZE TABLE t0 PARTITION ID")
.unwrap_err(),
ParserError("Expected: identifier, found: EOF".to_string())
);
}
fn column_def(name: Ident, data_type: DataType) -> ColumnDef {
ColumnDef {
name,
data_type,
options: vec![],
}
}
#[test]
fn parse_clickhouse_data_types() {
let sql = concat!(
"CREATE TABLE table (",
"a1 UInt8, a2 UInt16, a3 UInt32, a4 UInt64, a5 UInt128, a6 UInt256,",
" b1 Int8, b2 Int16, b3 Int32, b4 Int64, b5 Int128, b6 Int256,",
" c1 Float32, c2 Float64,",
" d1 Date32, d2 DateTime64(3), d3 DateTime64(3, 'UTC'),",
" e1 FixedString(255),",
" f1 LowCardinality(Int32)",
") ORDER BY (a1)",
);
// ClickHouse has a case-sensitive definition of data type, but canonical representation is not
let canonical_sql = sql
.replace(" Int8", " INT8")
.replace(" Int64", " INT64")
.replace(" Float64", " FLOAT64");
match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) {
Statement::CreateTable(CreateTable { name, columns, .. }) => {
assert_eq!(name, ObjectName::from(vec!["table".into()]));
assert_eq!(
columns,
vec![
column_def("a1".into(), DataType::UInt8),
column_def("a2".into(), DataType::UInt16),
column_def("a3".into(), DataType::UInt32),
column_def("a4".into(), DataType::UInt64),
column_def("a5".into(), DataType::UInt128),
column_def("a6".into(), DataType::UInt256),
column_def("b1".into(), DataType::Int8(None)),
column_def("b2".into(), DataType::Int16),
column_def("b3".into(), DataType::Int32),
column_def("b4".into(), DataType::Int64),
column_def("b5".into(), DataType::Int128),
column_def("b6".into(), DataType::Int256),
column_def("c1".into(), DataType::Float32),
column_def("c2".into(), DataType::Float64),
column_def("d1".into(), DataType::Date32),
column_def("d2".into(), DataType::Datetime64(3, None)),
column_def("d3".into(), DataType::Datetime64(3, Some("UTC".into()))),
column_def("e1".into(), DataType::FixedString(255)),
column_def(
"f1".into(),
DataType::LowCardinality(Box::new(DataType::Int32))
),
]
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_table_with_nullable() {
let sql = r#"CREATE TABLE table (k UInt8, `a` Nullable(String), `b` Nullable(DateTime64(9, 'UTC')), c Nullable(DateTime64(9)), d Date32 NULL) ENGINE = MergeTree ORDER BY (`k`)"#;
// ClickHouse has a case-sensitive definition of data type, but canonical representation is not
let canonical_sql = sql.replace("String", "STRING");
match clickhouse_and_generic().one_statement_parses_to(sql, &canonical_sql) {
Statement::CreateTable(CreateTable { name, columns, .. }) => {
assert_eq!(name, ObjectName::from(vec!["table".into()]));
assert_eq!(
columns,
vec![
column_def("k".into(), DataType::UInt8),
column_def(
Ident::with_quote('`', "a"),
DataType::Nullable(Box::new(DataType::String(None)))
),
column_def(
Ident::with_quote('`', "b"),
DataType::Nullable(Box::new(DataType::Datetime64(
9,
Some("UTC".to_string())
)))
),
column_def(
"c".into(),
DataType::Nullable(Box::new(DataType::Datetime64(9, None)))
),
ColumnDef {
name: "d".into(),
data_type: DataType::Date32,
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Null
}],
}
]
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_table_with_nested_data_types() {
let sql = concat!(
"CREATE TABLE table (",
" i Nested(a Array(Int16), b LowCardinality(String)),",
" k Array(Tuple(FixedString(128), Int128)),",
" l Tuple(a DateTime64(9), b Array(UUID)),",
" m Map(String, UInt16)",
") ENGINE=MergeTree ORDER BY (k)"
);
match clickhouse().one_statement_parses_to(sql, "") {
Statement::CreateTable(CreateTable { name, columns, .. }) => {
assert_eq!(name, ObjectName::from(vec!["table".into()]));
assert_eq!(
columns,
vec![
ColumnDef {
name: Ident::new("i"),
data_type: DataType::Nested(vec![
column_def(
"a".into(),
DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new(
DataType::Int16
),))
),
column_def(
"b".into(),
DataType::LowCardinality(Box::new(DataType::String(None)))
)
]),
options: vec![],
},
ColumnDef {
name: Ident::new("k"),
data_type: DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new(
DataType::Tuple(vec![
StructField {
field_name: None,
field_type: DataType::FixedString(128),
options: None,
},
StructField {
field_name: None,
field_type: DataType::Int128,
options: None,
}
])
))),
options: vec![],
},
ColumnDef {
name: Ident::new("l"),
data_type: DataType::Tuple(vec![
StructField {
field_name: Some("a".into()),
field_type: DataType::Datetime64(9, None),
options: None,
},
StructField {
field_name: Some("b".into()),
field_type: DataType::Array(ArrayElemTypeDef::Parenthesis(
Box::new(DataType::Uuid)
)),
options: None,
},
]),
options: vec![],
},
ColumnDef {
name: Ident::new("m"),
data_type: DataType::Map(
Box::new(DataType::String(None)),
Box::new(DataType::UInt16),
MapBracketKind::Parentheses
),
options: vec![],
},
]
);
}
_ => unreachable!(),
}
}
#[test]
fn reject_angle_bracket_array_type() {
assert_eq!(
clickhouse()
.parse_sql_statements("CREATE TABLE t (a ARRAY<INT>)")
.unwrap_err(),
ParserError("Expected: (, found: <".to_string())
);
}
#[test]
fn parse_create_table_with_primary_key() {
match clickhouse_and_generic().verified_stmt(concat!(
r#"CREATE TABLE db.table (`i` INT, `k` INT)"#,
" ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')",
" PRIMARY KEY tuple(i)",
" ORDER BY tuple(i)",
)) {
Statement::CreateTable(CreateTable {
name,
columns,
table_options,
primary_key,
order_by,
..
}) => {
assert_eq!(name.to_string(), "db.table");
assert_eq!(
vec![
ColumnDef {
name: Ident::with_quote('`', "i"),
data_type: DataType::Int(None),
options: vec![],
},
ColumnDef {
name: Ident::with_quote('`', "k"),
data_type: DataType::Int(None),
options: vec![],
},
],
columns
);
let plain_options = match table_options {
CreateTableOptions::Plain(options) => options,
_ => unreachable!(),
};
assert!(plain_options.contains(&SqlOption::NamedParenthesizedList(
NamedParenthesizedList {
key: Ident::new("ENGINE"),
name: Some(Ident::new("SharedMergeTree")),
values: vec![
Ident::with_quote('\'', "/clickhouse/tables/{uuid}/{shard}"),
Ident::with_quote('\'', "{replica}"),
]
}
)));
fn assert_function(actual: &Function, name: &str, arg: &str) -> bool {
assert_eq!(actual.name, ObjectName::from(vec![Ident::new(name)]));
assert_eq!(
actual.args,
FunctionArguments::List(FunctionArgumentList {
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(
Ident::new(arg)
)),)],
duplicate_treatment: None,
clauses: vec![],
})
);
true
}
match primary_key.unwrap().as_ref() {
Expr::Function(primary_key) => {
assert!(assert_function(primary_key, "tuple", "i"));
}
_ => panic!("unexpected primary key type"),
}
match order_by {
Some(OneOrManyWithParens::One(Expr::Function(order_by))) => {
assert!(assert_function(&order_by, "tuple", "i"));
}
_ => panic!("unexpected order by type"),
};
}
_ => unreachable!(),
}
clickhouse_and_generic()
.parse_sql_statements(concat!(
r#"CREATE TABLE db.table (`i` Int, `k` Int)"#,
" ORDER BY tuple(i), tuple(k)",
))
.expect_err("ORDER BY supports one expression with tuple");
}
#[test]
fn parse_create_table_with_variant_default_expressions() {
let sql = concat!(
"CREATE TABLE table (",
"a DATETIME MATERIALIZED now(),",
" b DATETIME EPHEMERAL now(),",
" c DATETIME EPHEMERAL,",
" d STRING ALIAS toString(c)",
") ENGINE = MergeTree"
);
match clickhouse_and_generic().verified_stmt(sql) {
Statement::CreateTable(CreateTable { columns, .. }) => {
assert_eq!(
columns,
vec![
ColumnDef {
name: Ident::new("a"),
data_type: DataType::Datetime(None),
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Materialized(Expr::Function(Function {
name: ObjectName::from(vec![Ident::new("now")]),
uses_odbc_syntax: false,
args: FunctionArguments::List(FunctionArgumentList {
args: vec![],
duplicate_treatment: None,
clauses: vec![],
}),
parameters: FunctionArguments::None,
null_treatment: None,
filter: None,
over: None,
within_group: vec![],
}))
}],
},
ColumnDef {
name: Ident::new("b"),
data_type: DataType::Datetime(None),
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Ephemeral(Some(Expr::Function(Function {
name: ObjectName::from(vec![Ident::new("now")]),
uses_odbc_syntax: false,
args: FunctionArguments::List(FunctionArgumentList {
args: vec![],
duplicate_treatment: None,
clauses: vec![],
}),
parameters: FunctionArguments::None,
null_treatment: None,
filter: None,
over: None,
within_group: vec![],
})))
}],
},
ColumnDef {
name: Ident::new("c"),
data_type: DataType::Datetime(None),
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Ephemeral(None)
}],
},
ColumnDef {
name: Ident::new("d"),
data_type: DataType::String(None),
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Alias(Expr::Function(Function {
name: ObjectName::from(vec![Ident::new("toString")]),
uses_odbc_syntax: false,
args: FunctionArguments::List(FunctionArgumentList {
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(
Identifier(Ident::new("c"))
))],
duplicate_treatment: None,
clauses: vec![],
}),
parameters: FunctionArguments::None,
null_treatment: None,
filter: None,
over: None,
within_group: vec![],
}))
}],
}
]
)
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_view_with_fields_data_types() {
match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) {
Statement::CreateView(CreateView { name, columns, .. }) => {
assert_eq!(name, ObjectName::from(vec!["v".into()]));
assert_eq!(
columns,
vec![
ViewColumnDef {
name: "i".into(),
data_type: Some(DataType::Custom(
ObjectName::from(vec![Ident {
value: "int".into(),
quote_style: Some('"'),
span: Span::empty(),
}]),
vec![]
)),
options: None,
},
ViewColumnDef {
name: "f".into(),
data_type: Some(DataType::Custom(
ObjectName::from(vec![Ident {
value: "String".into(),
quote_style: Some('"'),
span: Span::empty(),
}]),
vec![]
)),
options: None,
},
]
);
}
_ => unreachable!(),
}
clickhouse()
.parse_sql_statements(r#"CREATE VIEW v (i, f) AS SELECT * FROM t"#)
.expect_err("CREATE VIEW with fields and without data types should be invalid");
}
#[test]
fn parse_double_equal() {
clickhouse().one_statement_parses_to(
r#"SELECT foo FROM bar WHERE buz == 'buz'"#,
r#"SELECT foo FROM bar WHERE buz = 'buz'"#,
);
}
#[test]
fn parse_limit_by() {
clickhouse_and_generic().verified_stmt(
r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset"#,
);
clickhouse_and_generic().verified_stmt(
r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset, toStartOfDay(created_at)"#,
);
clickhouse_and_generic().parse_sql_statements(
r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC BY asset, toStartOfDay(created_at)"#,
).expect_err("BY without LIMIT");
clickhouse_and_generic()
.parse_sql_statements("SELECT * FROM T OFFSET 5 BY foo")
.expect_err("BY with OFFSET but without LIMIT");
}
#[test]
fn parse_settings_in_query() {
fn check_settings(sql: &str, expected: Vec<Setting>) {
match clickhouse_and_generic().verified_stmt(sql) {
Statement::Query(q) => {
assert_eq!(q.settings, Some(expected));
}
_ => unreachable!(),
}
}
for (sql, expected_settings) in [
(
r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#,
vec![
Setting {
key: Ident::new("max_threads"),
value: Expr::value(number("1")),
},
Setting {
key: Ident::new("max_block_size"),
value: Expr::value(number("10000")),
},
],
),
(
r#"SELECT * FROM t SETTINGS additional_table_filters = {'table_1': 'x != 2'}"#,
vec![Setting {
key: Ident::new("additional_table_filters"),
value: Expr::Dictionary(vec![DictionaryField {
key: Ident::with_quote('\'', "table_1"),
value: Expr::value(single_quoted_string("x != 2")).into(),
}]),
}],
),
(
r#"SELECT * FROM t SETTINGS additional_result_filter = 'x != 2', query_plan_optimize_lazy_materialization = false"#,
vec![
Setting {
key: Ident::new("additional_result_filter"),
value: Expr::value(single_quoted_string("x != 2")),
},
Setting {
key: Ident::new("query_plan_optimize_lazy_materialization"),
value: Expr::value(Boolean(false)),
},
],
),
] {
check_settings(sql, expected_settings);
}
let invalid_cases = vec![
("SELECT * FROM t SETTINGS a", "Expected: =, found: EOF"),
(
"SELECT * FROM t SETTINGS a=",
"Expected: an expression, found: EOF",
),
("SELECT * FROM t SETTINGS a=1, b", "Expected: =, found: EOF"),
(
"SELECT * FROM t SETTINGS a=1, b=",
"Expected: an expression, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {",
"Expected: identifier, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b'",
"Expected: :, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b': ",
"Expected: an expression, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c',}",
"Expected: identifier, found: }",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c', 'd'}",
"Expected: :, found: }",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c', 'd': }",
"Expected: an expression, found: }",
),
(
"SELECT * FROM t SETTINGS a = {ANY(b)}",
"Expected: :, found: (",
),
];
for (sql, error_msg) in invalid_cases {
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError(error_msg.to_string())
);
}
}
#[test]
fn parse_select_star_except() {
clickhouse().verified_stmt("SELECT * EXCEPT (prev_status) FROM anomalies");
}
#[test]
fn parse_select_parametric_function() {
match clickhouse_and_generic().verified_stmt("SELECT HISTOGRAM(0.5, 0.6)(x, y) FROM t") {
Statement::Query(query) => {
let projection: &Vec<SelectItem> = query.body.as_select().unwrap().projection.as_ref();
assert_eq!(projection.len(), 1);
match &projection[0] {
UnnamedExpr(Expr::Function(f)) => {
let args = match &f.args {
FunctionArguments::List(ref args) => args,
_ => unreachable!(),
};
assert_eq!(args.args.len(), 2);
assert_eq!(
args.args[0],
FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(Ident::from("x"))))
);
assert_eq!(
args.args[1],
FunctionArg::Unnamed(FunctionArgExpr::Expr(Identifier(Ident::from("y"))))
);
let parameters = match f.parameters {
FunctionArguments::List(ref args) => args,
_ => unreachable!(),
};
assert_eq!(parameters.args.len(), 2);
assert_eq!(
parameters.args[0],
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(
(Value::Number("0.5".parse().unwrap(), false)).with_empty_span()
)))
);
assert_eq!(
parameters.args[1],
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(
(Value::Number("0.6".parse().unwrap(), false)).with_empty_span()
)))
);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}
#[test]
fn parse_select_star_except_no_parens() {
clickhouse().one_statement_parses_to(
"SELECT * EXCEPT prev_status FROM anomalies",
"SELECT * EXCEPT (prev_status) FROM anomalies",
);
}
#[test]
fn parse_create_materialized_view() {
// example sql
// https://clickhouse.com/docs/en/guides/developer/cascading-materialized-views
let sql = concat!(
"CREATE MATERIALIZED VIEW analytics.monthly_aggregated_data_mv ",
"TO analytics.monthly_aggregated_data ",
"AS SELECT toDate(toStartOfMonth(event_time)) ",
"AS month, domain_name, sumState(count_views) ",
"AS sumCountViews FROM analytics.hourly_data ",
"GROUP BY domain_name, month"
);
clickhouse_and_generic().verified_stmt(sql);
}
#[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!(
OrderBy {
kind: OrderByKind::Expressions(vec![
OrderByExpr {
expr: Expr::Identifier(Ident::new("fname")),
options: OrderByOptions {
sort: Some(OrderBySort::Asc),
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"))),
}),
},
OrderByExpr {
expr: Expr::Identifier(Ident::new("lname")),
options: OrderByOptions {
sort: Some(OrderBySort::Desc),
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(Interpolate {
exprs: Some(vec![InterpolateExpr {
column: Ident::new("col1"),
expr: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col1"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::value(number("1"))),
}),
}])
})
},
select.order_by.expect("ORDER BY expected")
);
assert_eq!(
select.limit_clause,
Some(LimitClause::LimitOffset {
limit: Some(Expr::value(number("2"))),
offset: None,
limit_by: vec![]
})
);
}
#[test]
fn parse_select_order_by_with_fill_interpolate_multi_interpolates() {
let sql = "SELECT id, fname, lname FROM customer ORDER BY fname WITH FILL \
INTERPOLATE (col1 AS col1 + 1) INTERPOLATE (col2 AS col2 + 2)";
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("ORDER BY only accepts a single INTERPOLATE clause");
}
#[test]
fn parse_select_order_by_with_fill_interpolate_multi_with_fill_interpolates() {
let sql = "SELECT id, fname, lname FROM customer \
ORDER BY \
fname WITH FILL INTERPOLATE (col1 AS col1 + 1), \
lname WITH FILL INTERPOLATE (col2 AS col2 + 2)";
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("ORDER BY only accepts a single INTERPOLATE clause");
}
#[test]
fn parse_select_order_by_interpolate_not_last() {
let sql = "SELECT id, fname, lname FROM customer \
ORDER BY \
fname INTERPOLATE (col2 AS col2 + 2),
lname";
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("ORDER BY INTERPOLATE must be in the last position");
}
#[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"))),
})
.as_ref(),
match select.order_by.expect("ORDER BY expected").kind {
OrderByKind::Expressions(ref exprs) => exprs[0].with_fill.as_ref(),
_ => None,
}
);
}
#[test]
fn parse_with_fill_missing_single_argument() {
let sql = "SELECT id, fname, lname FROM customer ORDER BY \
fname WITH FILL FROM TO 20";
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("WITH FILL requires expressions for all arguments");
}
#[test]
fn parse_with_fill_multiple_incomplete_arguments() {
let sql = "SELECT id, fname, lname FROM customer ORDER BY \
fname WITH FILL FROM TO 20, lname WITH FILL FROM TO STEP 1";
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("WITH FILL requires expressions for all arguments");
}
#[test]
fn parse_interpolate_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(Interpolate {
exprs: Some(vec![
InterpolateExpr {
column: Ident::new("col1"),
expr: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col1"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::value(number("1"))),
}),
},
InterpolateExpr {
column: Ident::new("col2"),
expr: Some(Expr::Identifier(Ident::new("col3"))),
},
InterpolateExpr {
column: Ident::new("col4"),
expr: Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col4"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::value(number("4"))),
}),
},
])
})
.as_ref(),
select
.order_by
.expect("ORDER BY expected")
.interpolate
.as_ref()
);
}
#[test]
fn parse_interpolate_without_body() {
let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(Interpolate { exprs: None }).as_ref(),
select
.order_by
.expect("ORDER BY expected")
.interpolate
.as_ref()
);
}
#[test]
fn parse_interpolate_with_empty_body() {
let sql = "SELECT fname FROM customer ORDER BY fname WITH FILL INTERPOLATE ()";
let select = clickhouse().verified_query(sql);
assert_eq!(
Some(Interpolate {
exprs: Some(vec![])
})
.as_ref(),
select
.order_by
.expect("ORDER BY expected")
.interpolate
.as_ref()
);
}
#[test]
fn test_prewhere() {
match clickhouse_and_generic().verified_stmt("SELECT * FROM t PREWHERE x = 1 WHERE y = 2") {
Statement::Query(query) => {
let prewhere = query.body.as_select().unwrap().prewhere.as_ref();
assert_eq!(
prewhere,
Some(&BinaryOp {
left: Box::new(Identifier(Ident::new("x"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(
(Value::Number("1".parse().unwrap(), false)).with_empty_span()
)),
})
);
let selection = query.as_ref().body.as_select().unwrap().selection.as_ref();
assert_eq!(
selection,
Some(&BinaryOp {
left: Box::new(Identifier(Ident::new("y"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(
(Value::Number("2".parse().unwrap(), false)).with_empty_span()
)),
})
);
}
_ => unreachable!(),
}
match clickhouse_and_generic().verified_stmt("SELECT * FROM t PREWHERE x = 1 AND y = 2") {
Statement::Query(query) => {
let prewhere = query.body.as_select().unwrap().prewhere.as_ref();
assert_eq!(
prewhere,
Some(&BinaryOp {
left: Box::new(BinaryOp {
left: Box::new(Identifier(Ident::new("x"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(
(Value::Number("1".parse().unwrap(), false)).with_empty_span()
)),
}),
op: BinaryOperator::And,
right: Box::new(BinaryOp {
left: Box::new(Identifier(Ident::new("y"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(
(Value::Number("2".parse().unwrap(), false)).with_empty_span()
)),
}),
})
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_use() {
let valid_object_names = [
"mydb",
"SCHEMA",
"DATABASE",
"CATALOG",
"WAREHOUSE",
"DEFAULT",
];
let quote_styles = ['"', '`'];
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
clickhouse().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
);
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
)])))
);
}
}
}
#[test]
fn test_query_with_format_clause() {
let format_options = vec!["TabSeparated", "JSONCompact", "NULL"];
for format in &format_options {
let sql = format!("SELECT * FROM t FORMAT {format}");
match clickhouse_and_generic().verified_stmt(&sql) {
Statement::Query(query) => {
if *format == "NULL" {
assert_eq!(query.format_clause, Some(FormatClause::Null));
} else {
assert_eq!(
query.format_clause,
Some(FormatClause::Identifier(Ident::new(*format)))
);
}
}
_ => unreachable!(),
}
}
let invalid_cases = [
"SELECT * FROM t FORMAT",
"SELECT * FROM t FORMAT TabSeparated JSONCompact",
"SELECT * FROM t FORMAT TabSeparated TabSeparated",
];
for sql in &invalid_cases {
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("Expected: FORMAT {identifier}, found: ");
}
}
#[test]
fn test_insert_query_with_format_clause() {
let cases = [
r#"INSERT INTO tbl FORMAT JSONEachRow {"id": 1, "value": "foo"}, {"id": 2, "value": "bar"}"#,
r#"INSERT INTO tbl FORMAT JSONEachRow ["first", "second", "third"]"#,
r#"INSERT INTO tbl FORMAT JSONEachRow [{"first": 1}]"#,
r#"INSERT INTO tbl (foo) FORMAT JSONAsObject {"foo": {"bar": {"x": "y"}, "baz": 1}}"#,
r#"INSERT INTO tbl (foo, bar) FORMAT JSON {"foo": 1, "bar": 2}"#,
r#"INSERT INTO tbl FORMAT CSV col1, col2, col3"#,
r#"INSERT INTO tbl FORMAT LineAsString "I love apple", "I love banana", "I love orange""#,
r#"INSERT INTO tbl (foo) SETTINGS input_format_json_read_bools_as_numbers = true FORMAT JSONEachRow {"id": 1, "value": "foo"}"#,
r#"INSERT INTO tbl SETTINGS format_template_resultset = '/some/path/resultset.format', format_template_row = '/some/path/row.format' FORMAT Template"#,
r#"INSERT INTO tbl SETTINGS input_format_json_read_bools_as_numbers = true FORMAT JSONEachRow {"id": 1, "value": "foo"}"#,
];
for sql in &cases {
clickhouse().verified_stmt(sql);
}
}
#[test]
fn parse_create_table_on_commit_and_as_query() {
let sql = r#"CREATE LOCAL TEMPORARY TABLE test ON COMMIT PRESERVE ROWS AS SELECT 1"#;
match clickhouse_and_generic().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
on_commit,
query,
..
}) => {
assert_eq!(name.to_string(), "test");
assert_eq!(on_commit, Some(OnCommit::PreserveRows));
assert_eq!(
query.unwrap().body.as_select().unwrap().projection,
vec![UnnamedExpr(Expr::Value(
(Value::Number("1".parse().unwrap(), false)).with_empty_span()
))]
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_freeze_and_unfreeze_partition() {
// test cases without `WITH NAME`
for operation_name in &["FREEZE", "UNFREEZE"] {
let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14'");
let expected_partition = Partition::Expr(Expr::Value(
Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(),
));
match clickhouse_and_generic().verified_stmt(&sql) {
Statement::AlterTable(AlterTable { operations, .. }) => {
assert_eq!(operations.len(), 1);
let expected_operation = if operation_name == &"FREEZE" {
AlterTableOperation::FreezePartition {
partition: expected_partition,
with_name: None,
}
} else {
AlterTableOperation::UnfreezePartition {
partition: expected_partition,
with_name: None,
}
};
assert_eq!(operations[0], expected_operation);
}
_ => unreachable!(),
}
}
// test case with `WITH NAME`
for operation_name in &["FREEZE", "UNFREEZE"] {
let sql =
format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14' WITH NAME 'hello'");
match clickhouse_and_generic().verified_stmt(&sql) {
Statement::AlterTable(AlterTable { operations, .. }) => {
assert_eq!(operations.len(), 1);
let expected_partition = Partition::Expr(Expr::Value(
Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(),
));
let expected_operation = if operation_name == &"FREEZE" {
AlterTableOperation::FreezePartition {
partition: expected_partition,
with_name: Some(Ident::with_quote('\'', "hello")),
}
} else {
AlterTableOperation::UnfreezePartition {
partition: expected_partition,
with_name: Some(Ident::with_quote('\'', "hello")),
}
};
assert_eq!(operations[0], expected_operation);
}
_ => unreachable!(),
}
}
// negative cases
for operation_name in &["FREEZE", "UNFREEZE"] {
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(format!("ALTER TABLE t0 {operation_name} PARTITION").as_str())
.unwrap_err(),
ParserError("Expected: an expression, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(
format!("ALTER TABLE t0 {operation_name} PARTITION p0 WITH").as_str()
)
.unwrap_err(),
ParserError("Expected: NAME, found: EOF".to_string())
);
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(
format!("ALTER TABLE t0 {operation_name} PARTITION p0 WITH NAME").as_str()
)
.unwrap_err(),
ParserError("Expected: identifier, found: EOF".to_string())
);
}
}
#[test]
fn parse_select_table_function_settings() {
fn check_settings(sql: &str, expected: &TableFunctionArgs) {
match clickhouse_and_generic().verified_stmt(sql) {
Statement::Query(q) => {
let from = &q.body.as_select().unwrap().from;
assert_eq!(from.len(), 1);
assert_eq!(from[0].joins, vec![]);
match &from[0].relation {
Table { args, .. } => {
let args = args.as_ref().unwrap();
assert_eq!(args, expected);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}
check_settings(
"SELECT * FROM table_function(arg, SETTINGS s0 = 3, s1 = 's')",
&TableFunctionArgs {
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::Identifier("arg".into()),
))],
settings: Some(vec![
Setting {
key: "s0".into(),
value: Expr::value(number("3")),
},
Setting {
key: "s1".into(),
value: Expr::value(single_quoted_string("s")),
},
]),
},
);
check_settings(
r#"SELECT * FROM table_function(arg)"#,
&TableFunctionArgs {
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::Identifier("arg".into()),
))],
settings: None,
},
);
check_settings(
"SELECT * FROM table_function(SETTINGS s0 = 3, s1 = 's')",
&TableFunctionArgs {
args: vec![],
settings: Some(vec![
Setting {
key: "s0".into(),
value: Expr::value(number("3")),
},
Setting {
key: "s1".into(),
value: Expr::value(single_quoted_string("s")),
},
]),
},
);
let invalid_cases = vec![
"SELECT * FROM t(SETTINGS a)",
"SELECT * FROM t(SETTINGS a=)",
"SELECT * FROM t(SETTINGS a=1, b)",
"SELECT * FROM t(SETTINGS a=1, b=)",
];
for sql in invalid_cases {
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("Expected: SETTINGS key = value, found: ");
}
}
#[test]
fn explain_describe() {
clickhouse().verified_stmt("DESCRIBE test.table");
clickhouse().verified_stmt("DESCRIBE TABLE test.table");
}
#[test]
fn explain_desc() {
clickhouse().verified_stmt("DESC test.table");
clickhouse().verified_stmt("DESC TABLE test.table");
}
#[test]
fn parse_explain_table() {
match clickhouse().verified_stmt("EXPLAIN TABLE test_identifier") {
Statement::ExplainTable {
describe_alias,
hive_format,
has_table_keyword,
table_name,
} => {
pretty_assertions::assert_eq!(describe_alias, DescribeAlias::Explain);
pretty_assertions::assert_eq!(hive_format, None);
pretty_assertions::assert_eq!(has_table_keyword, true);
pretty_assertions::assert_eq!("test_identifier", table_name.to_string());
}
_ => panic!("Unexpected Statement, must be ExplainTable"),
}
}
#[test]
fn parse_table_sample() {
clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 0.1");
clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1000");
clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10");
clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2");
}
#[test]
fn test_parse_not_null_in_column_options() {
// In addition to DEFAULT and CHECK ClickHouse also supports MATERIALIZED, all of which
// can contain `IS NOT NULL` and thus `NOT NULL` as an alias.
let canonical = concat!(
"CREATE TABLE foo (",
"abc INT DEFAULT (42 IS NOT NULL) NOT NULL,",
" not_null BOOL MATERIALIZED (abc IS NOT NULL),",
" CHECK (abc IS NOT NULL)",
")",
);
clickhouse().verified_stmt(canonical);
clickhouse().one_statement_parses_to(
concat!(
"CREATE TABLE foo (",
"abc INT DEFAULT (42 NOT NULL) NOT NULL,",
" not_null BOOL MATERIALIZED (abc NOT NULL),",
" CHECK (abc NOT NULL)",
")",
),
canonical,
);
}
#[test]
fn parse_array_join() {
// ARRAY JOIN works with both ClickHouseDialect and GenericDialect (roundtrip)
clickhouse_and_generic().verified_stmt("SELECT x FROM t ARRAY JOIN arr AS x");
// AST: join_operator is the unit variant ArrayJoin (no constraint)
match clickhouse_and_generic().verified_stmt("SELECT x FROM t ARRAY JOIN arr AS x") {
Statement::Query(query) => {
let select = query.body.as_select().unwrap();
let join = &select.from[0].joins[0];
assert_eq!(join.join_operator, JoinOperator::ArrayJoin);
}
_ => unreachable!(),
}
// Combined: regular JOIN followed by ARRAY JOIN
clickhouse_and_generic()
.verified_stmt("SELECT x FROM t JOIN u ON t.id = u.id ARRAY JOIN arr AS x");
// Negative: ARRAY JOIN with no table expression should fail
clickhouse_and_generic()
.parse_sql_statements("SELECT x FROM t ARRAY JOIN")
.expect_err("ARRAY JOIN requires a table expression");
}
#[test]
fn parse_left_array_join() {
// LEFT ARRAY JOIN preserves rows with empty/null arrays (roundtrip)
clickhouse_and_generic().verified_stmt("SELECT x FROM t LEFT ARRAY JOIN arr AS x");
// AST: join_operator is LeftArrayJoin
match clickhouse_and_generic().verified_stmt("SELECT x FROM t LEFT ARRAY JOIN arr AS x") {
Statement::Query(query) => {
let select = query.body.as_select().unwrap();
let join = &select.from[0].joins[0];
assert_eq!(join.join_operator, JoinOperator::LeftArrayJoin);
}
_ => unreachable!(),
}
}
#[test]
fn parse_inner_array_join() {
// INNER ARRAY JOIN filters rows with empty/null arrays (roundtrip)
clickhouse_and_generic().verified_stmt("SELECT x FROM t INNER ARRAY JOIN arr AS x");
// AST: join_operator is InnerArrayJoin
match clickhouse_and_generic().verified_stmt("SELECT x FROM t INNER ARRAY JOIN arr AS x") {
Statement::Query(query) => {
let select = query.body.as_select().unwrap();
let join = &select.from[0].joins[0];
assert_eq!(join.join_operator, JoinOperator::InnerArrayJoin);
}
_ => unreachable!(),
}
}
#[test]
fn parse_in_unparenthesized_expr() {
// IN [expr] parses to IN ([expr]) and does not cause regressions
clickhouse().expr_parses_to("x IN 'a'", "x IN ('a')");
// The branch must not fire when the next token is `(` (regressions).
clickhouse().verified_expr("x IN (1, 2, 3)");
clickhouse().verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)");
}
#[test]
fn parse_in_unparenthesized_dictionary_placeholder() {
// IN [{placeholder:Type}] parses to IN ({placholder:Type})
clickhouse().expr_parses_to("x IN {ids:Array(UInt64)}", "x IN ({ids: Array(UInt64)})");
clickhouse().expr_parses_to(
"x NOT IN {ids:Array(UInt64)}",
"x NOT IN ({ids: Array(UInt64)})",
);
clickhouse().verified_expr("x IN ({ids: Array(UInt64)})");
// Precedence: the trailing `AND` is not swallowed.
clickhouse().verified_expr("x IN ({p: Array(UInt64)}) AND y = 1");
}
#[test]
fn parse_alter_table_column_position() {
clickhouse().verified_stmt("ALTER TABLE t ADD COLUMN c Nullable(UInt8) FIRST");
clickhouse().verified_stmt("ALTER TABLE t ADD COLUMN c UInt8 DEFAULT 0 AFTER a");
clickhouse().verified_stmt("ALTER TABLE t ON CLUSTER cl ADD COLUMN c UInt8 AFTER a");
clickhouse().verified_stmt("ALTER TABLE t MODIFY COLUMN c UInt16 FIRST");
clickhouse().verified_stmt("ALTER TABLE t ADD COLUMN c UInt8 AFTER `order`");
clickhouse().verified_stmt(r#"ALTER TABLE t ADD COLUMN c UInt8 AFTER "order""#);
match clickhouse()
.verified_stmt("ALTER TABLE t ADD COLUMN c UInt8 FIRST, ADD COLUMN d UInt8 AFTER c")
{
Statement::AlterTable(AlterTable { operations, .. }) => {
let positions: Vec<_> = operations
.into_iter()
.map(|op| match op {
AlterTableOperation::AddColumn {
column_position, ..
} => column_position,
_ => unreachable!(),
})
.collect();
assert_eq!(
positions,
vec![
Some(MySQLColumnPosition::First),
Some(MySQLColumnPosition::After(Ident::new("c"))),
]
);
}
_ => unreachable!(),
}
assert!(clickhouse()
.parse_sql_statements("ALTER TABLE t ADD COLUMN c UInt8 AFTER")
.is_err());
}
#[test]
fn parse_alter_table_modify_order_by() {
clickhouse().verified_stmt("ALTER TABLE events MODIFY ORDER BY (region, user.id)");
clickhouse_and_generic().verified_stmt("ALTER TABLE events MODIFY ORDER BY region");
clickhouse_and_generic().verified_stmt("ALTER TABLE events MODIFY ORDER BY ()");
clickhouse_and_generic().verified_stmt("ALTER TABLE events MODIFY ORDER BY tuple()");
clickhouse_and_generic()
.verified_stmt("ALTER TABLE events ON CLUSTER c MODIFY ORDER BY (a, toDate(ts))");
clickhouse_and_generic()
.verified_stmt("ALTER TABLE events ADD COLUMN b UInt8, MODIFY ORDER BY (a, b)");
// A column named `order` is still handled by MODIFY COLUMN.
clickhouse_and_generic().verified_stmt("ALTER TABLE events MODIFY COLUMN `order` UInt8");
match clickhouse_and_generic().verified_stmt("ALTER TABLE events MODIFY ORDER BY (a)") {
Statement::AlterTable(AlterTable { operations, .. }) => {
assert_eq!(
operations,
vec![AlterTableOperation::ModifyOrderBy {
order_by: OneOrManyWithParens::Many(vec![Expr::Identifier(Ident::new("a"))]),
}]
);
}
_ => unreachable!(),
}
for sql in [
"ALTER TABLE events MODIFY ORDER BY",
"ALTER TABLE events MODIFY ORDER BY (a",
"ALTER TABLE events MODIFY ORDER BY (,)",
"ALTER TABLE events MODIFY ORDER BY (a) b",
"CREATE TABLE events (a UInt8) ENGINE = MergeTree ORDER BY (a",
] {
assert!(
clickhouse_and_generic().parse_sql_statements(sql).is_err(),
"{sql}"
);
}
}
fn clickhouse() -> TestedDialects {
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
}
fn clickhouse_and_generic() -> TestedDialects {
TestedDialects::new(vec![
Box::new(ClickHouseDialect {}),
Box::new(GenericDialect {}),
])
}