diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 66f2cad3eb..b43d68f5ab 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2991,6 +2991,9 @@ pub struct CreateTable { /// Hive: Table clustering column list. /// pub clustered_by: Option, + /// DuckDB: Table sorting expressions. + /// + pub sorted_by: Option>, /// Postgres `INHERITs` clause, which contains the list of tables from which /// the new table inherits. /// @@ -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}")?; } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 84a93dd20c..f6d27fa5bc 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -127,6 +127,8 @@ pub struct CreateTableBuilder { pub cluster_by: Option>>, /// Optional `CLUSTERED BY` clause. pub clustered_by: Option, + /// Optional standalone `SORTED BY` expressions. + pub sorted_by: Option>, /// Optional parent tables (`INHERITS`). pub inherits: Option>, /// Optional partitioned table (`PARTITION OF`) @@ -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, @@ -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>) -> Self { + self.sorted_by = sorted_by; + self + } /// Set parent tables via `INHERITS`. pub fn inherits(mut self, inherits: Option>) -> Self { self.inherits = inherits; @@ -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, @@ -718,6 +727,7 @@ impl From 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, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cb06b78990..e1b9a874ff 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -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 @@ -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())), ) } } diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 2e3673bc4c..27b87455c4 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -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 } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ff83a4da61..41ea1d3c21 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -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. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5edc437145..ab5ffc046a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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) { @@ -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()?; @@ -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()? @@ -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) @@ -8925,6 +8935,19 @@ impl<'a> Parser<'a> { .build()) } + fn parse_optional_create_table_sorted_by(&mut self) -> Result>, 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 { match self.parse_one_of_keywords(&[Keyword::SET, Keyword::MULTISET]) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 29b060a82d..fec85463ee 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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()); +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a338ef7a82..39cc77074c 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -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 {})]) @@ -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(), @@ -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::(&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::(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)" + ); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4510f953e5..b15a8ad71a 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -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, @@ -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, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index dfd883eb4d..d609febb48 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -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,