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
7 changes: 7 additions & 0 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2991,6 +2991,9 @@ pub struct CreateTable {
/// Hive: Table clustering column list.
/// <https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable>
pub clustered_by: Option<ClusteredBy>,
/// DuckDB: Table sorting expressions.
/// <https://github.com/duckdb/duckdb/pull/20431>
pub sorted_by: Option<Vec<Expr>>,
/// Postgres `INHERITs` clause, which contains the list of tables from which
/// the new table inherits.
/// <https://www.postgresql.org/docs/current/ddl-inherit.html>
Expand Down Expand Up @@ -3215,6 +3218,10 @@ impl fmt::Display for CreateTable {
_ => (),
}

if let Some(sorted_by) = &self.sorted_by {
write!(f, " SORTED BY ({})", display_comma_separated(sorted_by))?;
}

if let Some(clustered_by) = &self.clustered_by {
write!(f, " {clustered_by}")?;
}
Expand Down
10 changes: 10 additions & 0 deletions src/ast/helpers/stmt_create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ pub struct CreateTableBuilder {
pub cluster_by: Option<WrappedCollection<Vec<Expr>>>,
/// Optional `CLUSTERED BY` clause.
pub clustered_by: Option<ClusteredBy>,
/// Optional standalone `SORTED BY` expressions.
pub sorted_by: Option<Vec<Expr>>,
/// Optional parent tables (`INHERITS`).
pub inherits: Option<Vec<ObjectName>>,
/// Optional partitioned table (`PARTITION OF`)
Expand Down Expand Up @@ -231,6 +233,7 @@ impl CreateTableBuilder {
partition_by: None,
cluster_by: None,
clustered_by: None,
sorted_by: None,
inherits: None,
partition_of: None,
for_values: None,
Expand Down Expand Up @@ -417,6 +420,11 @@ impl CreateTableBuilder {
self.clustered_by = clustered_by;
self
}
/// Set standalone `SORTED BY` expressions.
pub fn sorted_by(mut self, sorted_by: Option<Vec<Expr>>) -> Self {
self.sorted_by = sorted_by;
self
}
/// Set parent tables via `INHERITS`.
pub fn inherits(mut self, inherits: Option<Vec<ObjectName>>) -> Self {
self.inherits = inherits;
Expand Down Expand Up @@ -632,6 +640,7 @@ impl CreateTableBuilder {
partition_by: self.partition_by,
cluster_by: self.cluster_by,
clustered_by: self.clustered_by,
sorted_by: self.sorted_by,
inherits: self.inherits,
partition_of: self.partition_of,
for_values: self.for_values,
Expand Down Expand Up @@ -718,6 +727,7 @@ impl From<CreateTable> for CreateTableBuilder {
partition_by: table.partition_by,
cluster_by: table.cluster_by,
clustered_by: table.clustered_by,
sorted_by: table.sorted_by,
inherits: table.inherits,
partition_of: table.partition_of,
for_values: table.for_values,
Expand Down
6 changes: 4 additions & 2 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,8 @@ impl Spanned for CreateTable {
partition_by: _, // todo, BigQuery specific
cluster_by: _, // todo, BigQuery specific
clustered_by: _, // todo, Hive specific
inherits: _, // todo, PostgreSQL specific
sorted_by,
inherits: _, // todo, PostgreSQL specific
partition_of,
for_values,
strict: _, // bool
Expand Down Expand Up @@ -624,7 +625,8 @@ impl Spanned for CreateTable {
.chain(query.iter().map(|i| i.span()))
.chain(clone.iter().map(|i| i.span()))
.chain(partition_of.iter().map(|i| i.span()))
.chain(for_values.iter().map(|i| i.span())),
.chain(for_values.iter().map(|i| i.span()))
.chain(sorted_by.iter().flatten().map(|i| i.span())),
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub struct DuckDbDialect;

// In most cases the redshift dialect is identical to [`PostgresSqlDialect`].
impl Dialect for DuckDbDialect {
fn supports_create_table_sorted_by(&self) -> bool {
true
}

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

/// Returns true if the dialect supports standalone `SORTED BY` expressions in `CREATE TABLE`.
fn supports_create_table_sorted_by(&self) -> bool {
false
}

/// Returns true if the dialect supports MySQL-specific SELECT modifiers
/// like `HIGH_PRIORITY`, `STRAIGHT_JOIN`, `SQL_SMALL_RESULT`, etc.
///
Expand Down
23 changes: 23 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8756,6 +8756,7 @@ impl<'a> Parser<'a> {
};

// parse optional column list (schema)
let has_columns = self.peek_token_ref().token == Token::LParen;
let (columns, constraints) = self.parse_columns()?;
let comment_after_column_def =
if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) {
Expand Down Expand Up @@ -8785,7 +8786,11 @@ impl<'a> Parser<'a> {
// SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE`
let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]);

let mut sorted_by = self.parse_optional_create_table_sorted_by()?;
let hive_distribution = self.parse_hive_distribution()?;
if sorted_by.is_none() {
sorted_by = self.parse_optional_create_table_sorted_by()?;
}
let clustered_by = self.parse_optional_clustered_by()?;
let hive_formats = self.parse_hive_formats()?;

Expand Down Expand Up @@ -8879,6 +8884,10 @@ impl<'a> Parser<'a> {
None
};

if query.is_none() && !has_columns && sorted_by.is_some() {
return self.expected_ref("AS query or a table schema", self.peek_token_ref());
}

// `WITH DATA` clause only applies if there is a query body.
let with_data = if query.is_some() {
self.maybe_parse_with_data()?
Expand Down Expand Up @@ -8909,6 +8918,7 @@ impl<'a> Parser<'a> {
.on_commit(on_commit)
.on_cluster(on_cluster)
.clustered_by(clustered_by)
.sorted_by(sorted_by)
.partition_by(partition_by)
.cluster_by(create_table_config.cluster_by)
.inherits(create_table_config.inherits)
Expand All @@ -8925,6 +8935,19 @@ impl<'a> Parser<'a> {
.build())
}

fn parse_optional_create_table_sorted_by(&mut self) -> Result<Option<Vec<Expr>>, ParserError> {
if self.dialect.supports_create_table_sorted_by()
&& self.parse_keywords(&[Keyword::SORTED, Keyword::BY])
{
self.expect_token(&Token::LParen)?;
let expressions = self.parse_comma_separated(Parser::parse_expr)?;
self.expect_token(&Token::RParen)?;
Ok(Some(expressions))
} else {
Ok(None)
}
}

/// Parse `MULTISET` table-kind prefix on `CREATE TABLE`.
fn maybe_parse_multiset(&mut self) -> Option<bool> {
match self.parse_one_of_keywords(&[Keyword::SET, Keyword::MULTISET]) {
Expand Down
20 changes: 20 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20015,3 +20015,23 @@ fn parse_function_arg_call_chain_no_exponential_blowup() {
rx.recv_timeout(Duration::from_secs(5))
.expect("parser should reject this quickly, not loop exponentially");
}

#[test]
fn create_table_sorted_by_dialect_isolation() {
all_dialects_where(|dialect| !dialect.supports_create_table_sorted_by())
.one_of_identical_results(|dialect| {
assert!(
Parser::parse_sql(dialect, "CREATE TABLE t (id INTEGER) SORTED BY (id)").is_err()
)
});
let hive = TestedDialects::new(vec![Box::new(HiveDialect {})]);
let Statement::CreateTable(table) = hive.verified_stmt(
"CREATE TABLE t (id INT) PARTITIONED BY (category STRING) CLUSTERED BY (id) SORTED BY (id DESC) INTO 4 BUCKETS",
) else { unreachable!() };
assert!(matches!(
table.hive_distribution,
HiveDistributionStyle::PARTITIONED { .. }
));
assert!(table.sorted_by.is_none());
assert!(table.clustered_by.unwrap().sorted_by.is_some());
}
161 changes: 159 additions & 2 deletions tests/sqlparser_duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
mod test_utils;

use helpers::attached_token::AttachedToken;
use sqlparser::tokenizer::Span;
use sqlparser::tokenizer::{Location, Span};
use test_utils::*;

use sqlparser::ast::*;
use sqlparser::dialect::{DuckDbDialect, GenericDialect};
use sqlparser::parser::ParserError;
use sqlparser::parser::{Parser, ParserError};

fn duckdb() -> TestedDialects {
TestedDialects::new(vec![Box::new(DuckDbDialect {})])
Expand Down Expand Up @@ -764,6 +764,7 @@ fn test_duckdb_union_datatype() {
partition_by: Default::default(),
cluster_by: Default::default(),
clustered_by: Default::default(),
sorted_by: Default::default(),
inherits: Default::default(),
partition_of: Default::default(),
for_values: Default::default(),
Expand Down Expand Up @@ -910,3 +911,159 @@ fn test_duckdb_lambda_function() {
let sql_transform = "SELECT list_transform([1, 2, 3], lambda x : x * 2)";
duckdb().verified_stmt(sql_transform);
}

#[test]
fn create_table_sorted_by_round_trip() {
for sql in [
"CREATE TABLE events (id INTEGER, category VARCHAR) SORTED BY (id, lower(category), id + 1)",
"CREATE TABLE events (id INTEGER) SORTED BY (id) WITH (format = 'parquet')",
"CREATE TABLE events SORTED BY (id) WITH (format = 'parquet') AS SELECT 1 AS id",
"CREATE TABLE events PARTITIONED BY (id) SORTED BY (abs(id)) AS SELECT 1 AS id",
] {
let statement = duckdb().verified_stmt(sql);
assert_eq!(statement, duckdb().verified_stmt(&statement.to_string()));
}
for (sql, canonical) in [
(
"CREATE TABLE events (id INTEGER) SORTED BY (id) PARTITIONED BY (id)",
"CREATE TABLE events (id INTEGER) PARTITIONED BY (id) SORTED BY (id)",
),
(
"CREATE TABLE events (id INTEGER,) SORTED /* sort */ BY (id + 1,); -- end",
"CREATE TABLE events (id INTEGER) SORTED BY (id + 1)",
),
] {
duckdb().one_statement_parses_to(sql, canonical);
}
}

#[test]
fn create_table_sorted_by_ast_and_builder() {
let sql = "CREATE TABLE events (id INTEGER) SORTED BY (id, id + 1)";
let Statement::CreateTable(table) = duckdb().verified_stmt(sql) else {
panic!("expected CREATE TABLE")
};
let expressions = vec![
Expr::Identifier(Ident::new("id")),
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("id"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::Value(number("1").into())),
},
];
assert_eq!(table.sorted_by, Some(expressions.clone()));
assert!(table.clustered_by.is_none());
assert!(table.order_by.is_none());
assert!(table.sortkey.is_none());
let rebuilt = helpers::stmt_create_table::CreateTableBuilder::from(table.clone()).build();
assert_eq!(table, rebuilt);
assert_eq!(rebuilt.to_string(), sql);
let built = helpers::stmt_create_table::CreateTableBuilder::new(table.name)
.columns(table.columns)
.sorted_by(Some(expressions))
.build();
assert_eq!(built.sorted_by, rebuilt.sorted_by);
assert_eq!(built.to_string(), sql);
}

#[test]
fn create_table_sorted_by_span() {
let sql = "CREATE TABLE events (id INTEGER) SORTED BY (id + 1)";
let Statement::CreateTable(table) =
Parser::parse_sql(&DuckDbDialect {}, sql).unwrap().remove(0)
else {
unreachable!()
};
assert_eq!(
table.sorted_by.as_ref().unwrap()[0].span(),
Span::new(Location::new(1, 45), Location::new(1, 51))
);
assert_eq!(
table.span(),
Span::new(Location::new(1, 14), Location::new(1, 51))
);
}

#[test]
fn create_table_sorted_by_errors() {
for (tail, expected) in [
("SORTED BY ()", "Expected: an expression, found: )"),
("SORTED BY id", "Expected: (, found: id"),
("SORTED BY (id", "Expected: ), found: EOF"),
("SORTED BY (id ASC)", "Expected: ), found: ASC"),
("SORTED BY (id DESC)", "Expected: ), found: DESC"),
("SORTED BY (id NULLS FIRST)", "Expected: ), found: NULLS"),
("SORTED BY (id AS alias)", "Expected: ), found: AS"),
("SORTED BY (id alias)", "Expected: ), found: alias"),
(
"SORTED BY (id) SORTED BY (id)",
"Expected: end of statement, found: SORTED",
),
(
"SORTED BY (id) PARTITIONED BY (id) SORTED BY (id)",
"Expected: end of statement, found: SORTED",
),
(
"PARTITIONED BY (id) SORTED BY (id) PARTITIONED BY (id)",
"Expected: end of statement, found: PARTITIONED",
),
(
"WITH (format = 'parquet') SORTED BY (id)",
"Expected: end of statement, found: SORTED",
),
] {
assert_eq!(
duckdb()
.parse_sql_statements(&format!("CREATE TABLE events (id INTEGER) {tail}"))
.unwrap_err(),
ParserError::ParserError(expected.to_owned()),
);
}
assert_eq!(
duckdb()
.parse_sql_statements("CREATE TABLE events SORTED BY (id)")
.unwrap_err(),
ParserError::ParserError("Expected: AS query or a table schema, found: EOF".to_owned()),
);
}

#[test]
#[cfg(feature = "json_example")]
fn create_table_sorted_by_serialization() {
let statement = duckdb().verified_stmt("CREATE TABLE events (id INTEGER) SORTED BY (abs(id))");
let json = serde_json::to_string(&statement).unwrap();
assert_eq!(statement, serde_json::from_str::<Statement>(&json).unwrap());
let mut table = serde_json::to_value(
helpers::stmt_create_table::CreateTableBuilder::new(Ident::new("events").into()).build(),
)
.unwrap();
table.as_object_mut().unwrap().remove("sorted_by");
assert!(serde_json::from_value::<CreateTable>(table)
.unwrap()
.sorted_by
.is_none());
}

#[test]
#[cfg(feature = "visitor")]
fn create_table_sorted_by_visitors() {
use core::ops::ControlFlow;
let mut statement =
duckdb().verified_stmt("CREATE TABLE events (id INTEGER) SORTED BY (id + 1)");
let mut expressions = vec![];
let _ = visit_expressions(&statement, |expression| {
expressions.push(expression.to_string());
ControlFlow::<()>::Continue(())
});
assert_eq!(expressions, ["id + 1", "id", "1"]);
let _ = visit_expressions_mut(&mut statement, |expression| {
if let Expr::Identifier(ident) = expression {
ident.value = "value".to_owned();
}
ControlFlow::<()>::Continue(())
});
assert_eq!(
statement.to_string(),
"CREATE TABLE events (id INTEGER) SORTED BY (value + 1)"
);
}
2 changes: 2 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1982,6 +1982,7 @@ fn parse_create_table_with_valid_options() {
partition_by: None,
cluster_by: None,
clustered_by: None,
sorted_by: None,
inherits: None,
partition_of: None,
for_values: None,
Expand Down Expand Up @@ -2163,6 +2164,7 @@ fn parse_create_table_with_identity_column() {
partition_by: None,
cluster_by: None,
clustered_by: None,
sorted_by: None,
inherits: None,
partition_of: None,
for_values: None,
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7031,6 +7031,7 @@ fn parse_trigger_related_functions() {
partition_by: None,
cluster_by: None,
clustered_by: None,
sorted_by: None,
inherits: None,
partition_of: None,
for_values: None,
Expand Down