From b374f0eaefd00355a69220499bb03b219b164706 Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Fri, 1 Dec 2023 18:43:01 +0100 Subject: [PATCH 1/4] Add support for BigQuery table and view options Extends the parser with BigQuery support for - Table/View level options - Column level options - Table creation configurations `CLUSTER BY`, `PARTITION BY` --- src/ast/ddl.rs | 35 +++++ src/ast/helpers/stmt_create_table.rs | 17 ++- src/ast/mod.rs | 91 ++++++++++++- src/parser/mod.rs | 98 +++++++++++++- tests/sqlparser_bigquery.rs | 183 +++++++++++++++++++++++++++ tests/sqlparser_common.rs | 79 +++++++----- tests/sqlparser_postgres.rs | 6 +- tests/sqlparser_sqlite.rs | 6 +- 8 files changed, 462 insertions(+), 53 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3192af8bb..8a785b9da 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -26,6 +26,7 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, + SqlOption, }; use crate::tokenizer::Token; @@ -527,6 +528,29 @@ impl fmt::Display for ColumnDef { } } +/// Column definition for a view. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ViewColumnDef { + pub name: Ident, + pub options: Option>, +} + +impl fmt::Display for ViewColumnDef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name)?; + if let Some(options) = self.options.as_ref() { + write!( + f, + " OPTIONS ({})", + display_comma_separated(options.as_slice()) + )?; + } + Ok(()) + } +} + /// An optionally-named `ColumnOption`: `[ CONSTRAINT ] `. /// /// Note that implementations are substantially more permissive than the ANSI @@ -601,6 +625,14 @@ pub enum ColumnOption { generation_expr: Option, generation_expr_mode: Option, }, + /// BigQuery specific: Explicit column options in a view [1] or table [2] + /// Syntax + /// ```sql + /// OPTIONS (description="field desc") + /// ``` + /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#view_column_option_list + /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_option_list + SqlOptions(Vec), } impl fmt::Display for ColumnOption { @@ -674,6 +706,9 @@ impl fmt::Display for ColumnOption { Ok(()) } } + SqlOptions(options) => { + write!(f, "OPTIONS ({})", display_comma_separated(options)) + } } } } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 17327e7f8..be31b10e1 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, Query, - SqlOption, Statement, TableConstraint, + BigQueryCreateTableConfiguration, ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, + Ident, ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, }; use crate::parser::ParserError; @@ -72,6 +72,7 @@ pub struct CreateTableBuilder { pub on_commit: Option, pub on_cluster: Option, pub order_by: Option>, + pub big_query_config: Option>, pub strict: bool, } @@ -105,6 +106,7 @@ impl CreateTableBuilder { on_commit: None, on_cluster: None, order_by: None, + big_query_config: None, strict: false, } } @@ -236,6 +238,14 @@ impl CreateTableBuilder { self } + pub fn big_query_config( + mut self, + big_query_config: Option>, + ) -> Self { + self.big_query_config = big_query_config; + self + } + pub fn strict(mut self, strict: bool) -> Self { self.strict = strict; self @@ -270,6 +280,7 @@ impl CreateTableBuilder { on_commit: self.on_commit, on_cluster: self.on_cluster, order_by: self.order_by, + big_query_config: self.big_query_config, strict: self.strict, } } @@ -310,6 +321,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, + big_query_config, strict, } => Ok(Self { or_replace, @@ -339,6 +351,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, + big_query_config, strict, }), _ => Err(ParserError::ParserError(format!( diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4769ea9bd..976aef50c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,7 +33,7 @@ pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, + UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ @@ -1364,6 +1364,38 @@ pub enum Password { NullPassword, } +/// Sql options of a `CREATE TABLE` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTableOptions { + None, + /// Options specified using the `WITH` keyword. + /// e.g. `WITH (description = "123")` + /// + /// + With(Vec), + /// Options specified using the `OPTIONS` keyword. + /// e.g. `OPTIONS (description = "123")` + /// + /// + Options(Vec), +} + +impl fmt::Display for CreateTableOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateTableOptions::With(with_options) => { + write!(f, "WITH ({})", display_comma_separated(with_options)) + } + CreateTableOptions::Options(options) => { + write!(f, "OPTIONS ({})", display_comma_separated(options)) + } + CreateTableOptions::None => Ok(()), + } + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1512,9 +1544,9 @@ pub enum Statement { materialized: bool, /// View name name: ObjectName, - columns: Vec, + columns: Vec, query: Box, - with_options: Vec, + options: CreateTableOptions, cluster_by: Vec, /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause with_no_schema_binding: bool, @@ -1560,6 +1592,9 @@ pub enum Statement { /// than empty (represented as ()), the latter meaning "no sorting". /// order_by: Option>, + /// BigQuery specific configuration during table creation. + /// + big_query_config: Option>, /// SQLite "STRICT" clause. /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. @@ -2499,7 +2534,7 @@ impl fmt::Display for Statement { columns, query, materialized, - with_options, + options, cluster_by, with_no_schema_binding, if_not_exists, @@ -2514,8 +2549,8 @@ impl fmt::Display for Statement { temporary = if *temporary { "TEMPORARY " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } )?; - if !with_options.is_empty() { - write!(f, " WITH ({})", display_comma_separated(with_options))?; + if matches!(options, CreateTableOptions::With(_)) { + write!(f, " {options}")?; } if !columns.is_empty() { write!(f, " ({})", display_comma_separated(columns))?; @@ -2523,6 +2558,9 @@ impl fmt::Display for Statement { if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } + if matches!(options, CreateTableOptions::Options(_)) { + write!(f, " {options}")?; + } write!(f, " AS {query}")?; if *with_no_schema_binding { write!(f, " WITH NO SCHEMA BINDING")?; @@ -2557,6 +2595,7 @@ impl fmt::Display for Statement { on_commit, on_cluster, order_by, + big_query_config, strict, } => { // We want to allow the following options @@ -2713,6 +2752,25 @@ impl fmt::Display for Statement { if let Some(order_by) = order_by { write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; } + if let Some(bigquery_config) = big_query_config { + if let Some(partition_by) = bigquery_config.partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = bigquery_config.cluster_by.as_ref() { + write!( + f, + " CLUSTER BY {}", + display_comma_separated(cluster_by.as_slice()) + )?; + } + if let Some(options) = bigquery_config.options.as_ref() { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; + } + } if let Some(query) = query { write!(f, " AS {query}")?; } @@ -4220,12 +4278,31 @@ pub struct HiveFormat { pub location: Option, } +/// Represents BigQuery specific configuration like partitioning, clustering +/// information during table creation. +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct BigQueryCreateTableConfiguration { + /// A partition expression for the table. + /// + pub partition_by: Option, + /// Table clustering column list. + /// + pub cluster_by: Option>, + /// Table options list. + /// + pub options: Option>, +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct SqlOption { pub name: Ident, - pub value: Value, + pub value: Expr, } impl fmt::Display for SqlOption { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d9d4761c3..16bef84d2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3357,8 +3357,12 @@ impl<'a> Parser<'a> { // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let name = self.parse_object_name()?; - let columns = self.parse_parenthesized_column_list(Optional, false)?; + let columns = self.parse_view_columns()?; + let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; + if !with_options.is_empty() { + options = CreateTableOptions::With(with_options); + } let cluster_by = if self.parse_keyword(Keyword::CLUSTER) { self.expect_keyword(Keyword::BY)?; @@ -3367,6 +3371,17 @@ impl<'a> Parser<'a> { vec![] }; + if dialect_of!(self is BigQueryDialect | GenericDialect) { + if let Token::Word(word) = self.peek_token().token { + if word.keyword == Keyword::OPTIONS { + let opts = self.parse_options(Keyword::OPTIONS)?; + if !opts.is_empty() { + options = CreateTableOptions::Options(opts); + } + } + }; + } + self.expect_keyword(Keyword::AS)?; let query = Box::new(self.parse_query()?); // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. @@ -3385,7 +3400,7 @@ impl<'a> Parser<'a> { query, materialized, or_replace, - with_options, + options, cluster_by, with_no_schema_binding, if_not_exists, @@ -4049,6 +4064,12 @@ impl<'a> Parser<'a> { None }; + let big_query_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { + self.parse_optional_big_query_create_table_config()? + } else { + None + }; + // Parse optional `AS ( query )` let query = if self.parse_keyword(Keyword::AS) { Some(Box::new(self.parse_query()?)) @@ -4120,10 +4141,44 @@ impl<'a> Parser<'a> { .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) + .big_query_config(big_query_config) .strict(strict) .build()) } + /// Parse configuration like partitioning, clustering information during big-query table creation. + /// + fn parse_optional_big_query_create_table_config( + &mut self, + ) -> Result>, ParserError> { + let mut partition_by = None; + if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + partition_by = Some(self.parse_expr()?); + }; + + let mut cluster_by = None; + if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { + cluster_by = Some(self.parse_comma_separated(Parser::parse_identifier)?); + }; + + let mut options = None; + if let Token::Word(word) = self.peek_token().token { + if word.keyword == Keyword::OPTIONS { + options = Some(self.parse_options(Keyword::OPTIONS)?); + } + }; + + if partition_by.is_some() || cluster_by.is_some() || options.is_some() { + Ok(Some(Box::new(BigQueryCreateTableConfiguration { + partition_by, + cluster_by, + options, + }))) + } else { + Ok(None) + } + } + pub fn parse_optional_procedure_parameters( &mut self, ) -> Result>, ParserError> { @@ -4282,6 +4337,13 @@ impl<'a> Parser<'a> { Ok(Some(ColumnOption::OnUpdate(expr))) } else if self.parse_keyword(Keyword::GENERATED) { self.parse_optional_column_option_generated() + } else if dialect_of!(self is BigQueryDialect | GenericDialect) + && self.parse_keyword(Keyword::OPTIONS) + { + self.prev_token(); + Ok(Some(ColumnOption::SqlOptions( + self.parse_options(Keyword::OPTIONS)?, + ))) } else { Ok(None) } @@ -4527,7 +4589,7 @@ impl<'a> Parser<'a> { pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; - let value = self.parse_value()?; + let value = self.parse_expr()?; Ok(SqlOption { name, value }) } @@ -5649,6 +5711,36 @@ impl<'a> Parser<'a> { } } + /// Parses a parenthesized, comma-separated list of column definitions within a view. + fn parse_view_columns(&mut self) -> Result, ParserError> { + if self.consume_token(&Token::LParen) { + if self.peek_token().token == Token::RParen { + self.next_token(); + Ok(vec![]) + } else { + let cols = self.parse_comma_separated(Parser::parse_view_column)?; + self.expect_token(&Token::RParen)?; + Ok(cols) + } + } else { + Ok(vec![]) + } + } + + /// Parses a column definition within a view. + fn parse_view_column(&mut self) -> Result { + let name = self.parse_identifier()?; + let options = if dialect_of!(self is BigQueryDialect | GenericDialect) + && self.parse_keyword(Keyword::OPTIONS) + { + self.prev_token(); + Some(self.parse_options(Keyword::OPTIONS)?) + } else { + None + }; + Ok(ViewColumnDef { name, options }) + } + /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers pub fn parse_parenthesized_column_list( &mut self, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 04cca0b7c..99ce0bd3a 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -21,6 +21,18 @@ use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use sqlparser::parser::ParserError; use test_utils::*; +/// Strips out newlines and spaces from the `sql` so that it can +/// be comparable with the serialized result +fn trim_sql(sql: &str) -> String { + sql.split('\n') + .filter(|line| !line.trim().is_empty()) + .map(|line| line.trim_start()) + .collect::>() + .join(" ") + .trim() + .to_string() +} + #[test] fn parse_literal_string() { let sql = r#"SELECT 'single', "double""#; @@ -86,6 +98,177 @@ fn parse_raw_literal() { panic!("invalid query") } +#[test] +fn parse_create_view_with_options() { + let sql = trim_sql( + r#" + CREATE VIEW myproject.mydataset.newview + (name, age OPTIONS (description = "field age")) + OPTIONS + (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), + friendly_name = "newview", + description = "a view that expires in 2 days", + labels = [("org_unit", "development")]) + AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable"#, + ); + match bigquery().verified_stmt(sql.as_str()) { + Statement::CreateView { + name, + query, + options, + columns, + .. + } => { + assert_eq!( + name, + ObjectName(vec![ + "myproject".into(), + "mydataset".into(), + "newview".into() + ]) + ); + assert_eq!( + vec![ + ViewColumnDef { + name: Ident::new("name"), + options: None, + }, + ViewColumnDef { + name: Ident::new("age"), + options: Some(vec![SqlOption { + name: Ident::new("description"), + value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), + }]) + }, + ], + columns + ); + assert_eq!( + "SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable", + query.to_string() + ); + assert_eq!( + r#"OPTIONS (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), friendly_name = "newview", description = "a view that expires in 2 days", labels = [("org_unit", "development")])"#, + options.to_string() + ); + let CreateTableOptions::Options(options) = options else { + unreachable!() + }; + assert_eq!( + &SqlOption { + name: Ident::new("description"), + value: Expr::Value(Value::DoubleQuotedString( + "a view that expires in 2 days".to_string() + )), + }, + &options[2], + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_with_options() { + let sql = trim_sql( + r#" + CREATE TABLE mydataset.newtable + (x INT64 NOT NULL OPTIONS (description = "field x"), + y BOOL OPTIONS (description = "field y")) + + PARTITION BY _PARTITIONDATE + CLUSTER BY userid, age + OPTIONS(partition_expiration_days = 1, + description = "table option description") + "#, + ); + match bigquery().verified_stmt(sql.as_str()) { + Statement::CreateTable { + name, + columns, + big_query_config, + .. + } => { + assert_eq!( + name, + ObjectName(vec!["mydataset".into(), "newtable".into()]) + ); + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("x"), + data_type: DataType::Int64, + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + }, + ColumnOptionDef { + name: None, + option: ColumnOption::SqlOptions(vec![SqlOption { + name: Ident::new("description"), + value: Expr::Value(Value::DoubleQuotedString( + "field x".to_string() + )), + },]) + }, + ] + }, + ColumnDef { + name: Ident::new("y"), + data_type: DataType::Bool, + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::SqlOptions(vec![SqlOption { + name: Ident::new("description"), + value: Expr::Value(Value::DoubleQuotedString( + "field y".to_string() + )), + },]) + }] + }, + ], + columns + ); + assert_eq!( + BigQueryCreateTableConfiguration { + partition_by: Some(Expr::Identifier(Ident::new("_PARTITIONDATE"))), + cluster_by: Some(vec![Ident::new("userid"), Ident::new("age"),]), + options: Some(vec![ + SqlOption { + name: Ident::new("partition_expiration_days"), + value: Expr::Value(number("1")), + }, + SqlOption { + name: Ident::new("description"), + value: Expr::Value(Value::DoubleQuotedString( + "table option description".to_string() + )), + }, + ]) + }, + *big_query_config.unwrap(), + ) + } + _ => unreachable!(), + } + + let sql = trim_sql( + r#" + CREATE TABLE mydataset.newtable + (x INT64 NOT NULL OPTIONS (description = "field x"), + y BOOL OPTIONS (description = "field y")) + + CLUSTER BY userid + OPTIONS(partition_expiration_days = 1, + description = "table option description") + "#, + ); + bigquery().verified_stmt(sql.as_str()); +} + #[test] fn parse_nested_data_types() { let sql = "CREATE TABLE table (x STRUCT, b BYTES(42)>, y ARRAY>)"; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1d0923b4f..089e9db92 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2972,11 +2972,11 @@ fn parse_create_table_with_options() { vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()), + value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption { name: "a".into(), - value: number("123"), + value: Expr::Value(number("123")), }, ], with_options @@ -3235,11 +3235,11 @@ fn parse_alter_view_with_options() { vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()), + value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption { name: "a".into(), - value: number("123"), + value: Expr::Value(number("123")), }, ], with_options @@ -5564,18 +5564,18 @@ fn parse_create_view() { query, or_replace, materialized, - with_options, + options, cluster_by, with_no_schema_binding: late_binding, if_not_exists, temporary, } => { assert_eq!("myschema.myview", name.to_string()); - assert_eq!(Vec::::new(), columns); + assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(!materialized); assert!(!or_replace); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); assert!(!late_binding); assert!(!if_not_exists); @@ -5589,19 +5589,19 @@ fn parse_create_view() { fn parse_create_view_with_options() { let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { with_options, .. } => { + Statement::CreateView { options, .. } => { assert_eq!( - vec![ + CreateTableOptions::With(vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()), + value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption { name: "a".into(), - value: number("123"), + value: Expr::Value(number("123")), }, - ], - with_options + ]), + options ); } _ => unreachable!(), @@ -5616,7 +5616,7 @@ fn parse_create_view_with_columns() { name, columns, or_replace, - with_options, + options, query, materialized, cluster_by, @@ -5625,8 +5625,17 @@ fn parse_create_view_with_columns() { temporary, } => { assert_eq!("v", name.to_string()); - assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); - assert_eq!(with_options, vec![]); + assert_eq!( + columns, + vec![Ident::new("has"), Ident::new("cols"),] + .into_iter() + .map(|name| ViewColumnDef { + name, + options: None + }) + .collect::>() + ); + assert_eq!(options, CreateTableOptions::None); assert_eq!("SELECT 1, 2", query.to_string()); assert!(!materialized); assert!(!or_replace); @@ -5649,18 +5658,18 @@ fn parse_create_view_temporary() { query, or_replace, materialized, - with_options, + options, cluster_by, with_no_schema_binding: late_binding, if_not_exists, temporary, } => { assert_eq!("myschema.myview", name.to_string()); - assert_eq!(Vec::::new(), columns); + assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(!materialized); assert!(!or_replace); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); assert!(!late_binding); assert!(!if_not_exists); @@ -5678,7 +5687,7 @@ fn parse_create_or_replace_view() { name, columns, or_replace, - with_options, + options, query, materialized, cluster_by, @@ -5688,7 +5697,7 @@ fn parse_create_or_replace_view() { } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert_eq!("SELECT 1", query.to_string()); assert!(!materialized); assert!(or_replace); @@ -5713,7 +5722,7 @@ fn parse_create_or_replace_materialized_view() { name, columns, or_replace, - with_options, + options, query, materialized, cluster_by, @@ -5723,7 +5732,7 @@ fn parse_create_or_replace_materialized_view() { } => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert_eq!("SELECT 1", query.to_string()); assert!(materialized); assert!(or_replace); @@ -5746,17 +5755,17 @@ fn parse_create_materialized_view() { columns, query, materialized, - with_options, + options, cluster_by, with_no_schema_binding: late_binding, if_not_exists, temporary, } => { assert_eq!("myschema.myview", name.to_string()); - assert_eq!(Vec::::new(), columns); + assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(materialized); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert!(!or_replace); assert_eq!(cluster_by, vec![]); assert!(!late_binding); @@ -5777,17 +5786,17 @@ fn parse_create_materialized_view_with_cluster_by() { columns, query, materialized, - with_options, + options, cluster_by, with_no_schema_binding: late_binding, if_not_exists, temporary, } => { assert_eq!("myschema.myview", name.to_string()); - assert_eq!(Vec::::new(), columns); + assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(materialized); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert!(!or_replace); assert_eq!(cluster_by, vec![Ident::new("foo")]); assert!(!late_binding); @@ -7414,11 +7423,11 @@ fn parse_cache_table() { options: vec![ SqlOption { name: Ident::with_quote('\'', "K1"), - value: Value::SingleQuotedString("V1".into()), + value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption { name: Ident::with_quote('\'', "K2"), - value: number("0.88"), + value: Expr::Value(number("0.88")), }, ], query: None, @@ -7439,11 +7448,11 @@ fn parse_cache_table() { options: vec![ SqlOption { name: Ident::with_quote('\'', "K1"), - value: Value::SingleQuotedString("V1".into()), + value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption { name: Ident::with_quote('\'', "K2"), - value: number("0.88"), + value: Expr::Value(number("0.88")), }, ], query: Some(query.clone()), @@ -7464,11 +7473,11 @@ fn parse_cache_table() { options: vec![ SqlOption { name: Ident::with_quote('\'', "K1"), - value: Value::SingleQuotedString("V1".into()), + value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption { name: Ident::with_quote('\'', "K2"), - value: number("0.88"), + value: Expr::Value(number("0.88")), }, ], query: Some(query.clone()), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b075a9b4d..dde1b8d00 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -459,15 +459,15 @@ fn parse_create_table_with_defaults() { vec![ SqlOption { name: "fillfactor".into(), - value: number("20") + value: Expr::Value(number("20")) }, SqlOption { name: "user_catalog_table".into(), - value: Value::Boolean(true) + value: Expr::Value(Value::Boolean(true)) }, SqlOption { name: "autovacuum_vacuum_threshold".into(), - value: number("100") + value: Expr::Value(number("100")) }, ] ); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index cc0d53b14..5c9cf9068 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -117,18 +117,18 @@ fn parse_create_view_temporary_if_not_exists() { query, or_replace, materialized, - with_options, + options, cluster_by, with_no_schema_binding: late_binding, if_not_exists, temporary, } => { assert_eq!("myschema.myview", name.to_string()); - assert_eq!(Vec::::new(), columns); + assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(!materialized); assert!(!or_replace); - assert_eq!(with_options, vec![]); + assert_eq!(options, CreateTableOptions::None); assert_eq!(cluster_by, vec![]); assert!(!late_binding); assert!(if_not_exists); From 3b2dec1e7f0daee0fca12b2b8a29640cc9606a7a Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Mon, 4 Dec 2023 15:35:33 +0100 Subject: [PATCH 2/4] format option labels without leading space also replace custom trimming with concat --- src/ast/ddl.rs | 6 ++-- src/ast/mod.rs | 4 +-- tests/sqlparser_bigquery.rs | 71 +++++++++++++------------------------ 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 8a785b9da..3c0d2f71a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -543,7 +543,7 @@ impl fmt::Display for ViewColumnDef { if let Some(options) = self.options.as_ref() { write!( f, - " OPTIONS ({})", + " OPTIONS({})", display_comma_separated(options.as_slice()) )?; } @@ -628,7 +628,7 @@ pub enum ColumnOption { /// BigQuery specific: Explicit column options in a view [1] or table [2] /// Syntax /// ```sql - /// OPTIONS (description="field desc") + /// OPTIONS(description="field desc") /// ``` /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#view_column_option_list /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_option_list @@ -707,7 +707,7 @@ impl fmt::Display for ColumnOption { } } SqlOptions(options) => { - write!(f, "OPTIONS ({})", display_comma_separated(options)) + write!(f, "OPTIONS({})", display_comma_separated(options)) } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 976aef50c..c0963b798 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1376,7 +1376,7 @@ pub enum CreateTableOptions { /// With(Vec), /// Options specified using the `OPTIONS` keyword. - /// e.g. `OPTIONS (description = "123")` + /// e.g. `OPTIONS(description = "123")` /// /// Options(Vec), @@ -1389,7 +1389,7 @@ impl fmt::Display for CreateTableOptions { write!(f, "WITH ({})", display_comma_separated(with_options)) } CreateTableOptions::Options(options) => { - write!(f, "OPTIONS ({})", display_comma_separated(options)) + write!(f, "OPTIONS({})", display_comma_separated(options)) } CreateTableOptions::None => Ok(()), } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 99ce0bd3a..29a9d30cc 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -21,18 +21,6 @@ use sqlparser::dialect::{BigQueryDialect, GenericDialect}; use sqlparser::parser::ParserError; use test_utils::*; -/// Strips out newlines and spaces from the `sql` so that it can -/// be comparable with the serialized result -fn trim_sql(sql: &str) -> String { - sql.split('\n') - .filter(|line| !line.trim().is_empty()) - .map(|line| line.trim_start()) - .collect::>() - .join(" ") - .trim() - .to_string() -} - #[test] fn parse_literal_string() { let sql = r#"SELECT 'single', "double""#; @@ -100,18 +88,14 @@ fn parse_raw_literal() { #[test] fn parse_create_view_with_options() { - let sql = trim_sql( - r#" - CREATE VIEW myproject.mydataset.newview - (name, age OPTIONS (description = "field age")) - OPTIONS - (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), - friendly_name = "newview", - description = "a view that expires in 2 days", - labels = [("org_unit", "development")]) - AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable"#, + let sql = concat!( + "CREATE VIEW myproject.mydataset.newview ", + r#"(name, age OPTIONS(description = "field age")) "#, + r#"OPTIONS(expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), "#, + r#"friendly_name = "newview", description = "a view that expires in 2 days", labels = [("org_unit", "development")]) "#, + "AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable", ); - match bigquery().verified_stmt(sql.as_str()) { + match bigquery().verified_stmt(sql) { Statement::CreateView { name, query, @@ -148,7 +132,7 @@ fn parse_create_view_with_options() { query.to_string() ); assert_eq!( - r#"OPTIONS (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), friendly_name = "newview", description = "a view that expires in 2 days", labels = [("org_unit", "development")])"#, + r#"OPTIONS(expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR), friendly_name = "newview", description = "a view that expires in 2 days", labels = [("org_unit", "development")])"#, options.to_string() ); let CreateTableOptions::Options(options) = options else { @@ -170,19 +154,15 @@ fn parse_create_view_with_options() { #[test] fn parse_create_table_with_options() { - let sql = trim_sql( - r#" - CREATE TABLE mydataset.newtable - (x INT64 NOT NULL OPTIONS (description = "field x"), - y BOOL OPTIONS (description = "field y")) - - PARTITION BY _PARTITIONDATE - CLUSTER BY userid, age - OPTIONS(partition_expiration_days = 1, - description = "table option description") - "#, + let sql = concat!( + "CREATE TABLE mydataset.newtable ", + r#"(x INT64 NOT NULL OPTIONS(description = "field x"), "#, + r#"y BOOL OPTIONS(description = "field y")) "#, + "PARTITION BY _PARTITIONDATE ", + "CLUSTER BY userid, age ", + r#"OPTIONS(partition_expiration_days = 1, description = "table option description")"# ); - match bigquery().verified_stmt(sql.as_str()) { + match bigquery().verified_stmt(sql) { Statement::CreateTable { name, columns, @@ -255,18 +235,15 @@ fn parse_create_table_with_options() { _ => unreachable!(), } - let sql = trim_sql( - r#" - CREATE TABLE mydataset.newtable - (x INT64 NOT NULL OPTIONS (description = "field x"), - y BOOL OPTIONS (description = "field y")) - - CLUSTER BY userid - OPTIONS(partition_expiration_days = 1, - description = "table option description") - "#, + let sql = concat!( + "CREATE TABLE mydataset.newtable ", + r#"(x INT64 NOT NULL OPTIONS(description = "field x"), "#, + r#"y BOOL OPTIONS(description = "field y")) "#, + "CLUSTER BY userid ", + r#"OPTIONS(partition_expiration_days = 1, "#, + r#"description = "table option description")"# ); - bigquery().verified_stmt(sql.as_str()); + bigquery().verified_stmt(sql); } #[test] From f7a1c56f2ab4027edcd48fae5aca6029258131a6 Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Sat, 16 Dec 2023 09:20:10 +0100 Subject: [PATCH 3/4] use enum to represent table config --- src/ast/helpers/stmt_create_table.rs | 21 +++++++++------------ src/ast/mod.rs | 25 +++++++++++++++++-------- src/parser/mod.rs | 18 ++++++++++-------- tests/sqlparser_bigquery.rs | 8 ++++---- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index be31b10e1..fce01418f 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - BigQueryCreateTableConfiguration, ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, - Ident, ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, + ColumnDef, CreateTableConfiguration, FileFormat, HiveDistributionStyle, HiveFormat, Ident, + ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, }; use crate::parser::ParserError; @@ -72,7 +72,7 @@ pub struct CreateTableBuilder { pub on_commit: Option, pub on_cluster: Option, pub order_by: Option>, - pub big_query_config: Option>, + pub table_config: Option>, pub strict: bool, } @@ -106,7 +106,7 @@ impl CreateTableBuilder { on_commit: None, on_cluster: None, order_by: None, - big_query_config: None, + table_config: None, strict: false, } } @@ -238,11 +238,8 @@ impl CreateTableBuilder { self } - pub fn big_query_config( - mut self, - big_query_config: Option>, - ) -> Self { - self.big_query_config = big_query_config; + pub fn table_config(mut self, table_config: Option>) -> Self { + self.table_config = table_config; self } @@ -280,7 +277,7 @@ impl CreateTableBuilder { on_commit: self.on_commit, on_cluster: self.on_cluster, order_by: self.order_by, - big_query_config: self.big_query_config, + table_config: self.table_config, strict: self.strict, } } @@ -321,7 +318,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, - big_query_config, + table_config: config, strict, } => Ok(Self { or_replace, @@ -351,7 +348,7 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, - big_query_config, + table_config: config, strict, }), _ => Err(ParserError::ParserError(format!( diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c0963b798..472b5e54d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1592,9 +1592,8 @@ pub enum Statement { /// than empty (represented as ()), the latter meaning "no sorting". /// order_by: Option>, - /// BigQuery specific configuration during table creation. - /// - big_query_config: Option>, + /// Database specific configuration during table creation. + table_config: Option>, /// SQLite "STRICT" clause. /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. @@ -2595,7 +2594,7 @@ impl fmt::Display for Statement { on_commit, on_cluster, order_by, - big_query_config, + table_config, strict, } => { // We want to allow the following options @@ -2752,18 +2751,20 @@ impl fmt::Display for Statement { if let Some(order_by) = order_by { write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; } - if let Some(bigquery_config) = big_query_config { - if let Some(partition_by) = bigquery_config.partition_by.as_ref() { + if let Some(CreateTableConfiguration::BigQuery(big_query_config)) = + table_config.as_ref().map(|c| c.as_ref()) + { + if let Some(partition_by) = big_query_config.partition_by.as_ref() { write!(f, " PARTITION BY {partition_by}")?; } - if let Some(cluster_by) = bigquery_config.cluster_by.as_ref() { + if let Some(cluster_by) = big_query_config.cluster_by.as_ref() { write!( f, " CLUSTER BY {}", display_comma_separated(cluster_by.as_slice()) )?; } - if let Some(options) = bigquery_config.options.as_ref() { + if let Some(options) = big_query_config.options.as_ref() { write!( f, " OPTIONS({})", @@ -4297,6 +4298,14 @@ pub struct BigQueryCreateTableConfiguration { pub options: Option>, } +/// Represents database specific configuration during table creation. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTableConfiguration { + BigQuery(BigQueryCreateTableConfiguration), +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16bef84d2..b160caf79 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4064,7 +4064,7 @@ impl<'a> Parser<'a> { None }; - let big_query_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { + let table_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { self.parse_optional_big_query_create_table_config()? } else { None @@ -4141,7 +4141,7 @@ impl<'a> Parser<'a> { .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) - .big_query_config(big_query_config) + .table_config(table_config) .strict(strict) .build()) } @@ -4150,7 +4150,7 @@ impl<'a> Parser<'a> { /// fn parse_optional_big_query_create_table_config( &mut self, - ) -> Result>, ParserError> { + ) -> Result>, ParserError> { let mut partition_by = None; if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { partition_by = Some(self.parse_expr()?); @@ -4169,11 +4169,13 @@ impl<'a> Parser<'a> { }; if partition_by.is_some() || cluster_by.is_some() || options.is_some() { - Ok(Some(Box::new(BigQueryCreateTableConfiguration { - partition_by, - cluster_by, - options, - }))) + Ok(Some(Box::new(CreateTableConfiguration::BigQuery( + BigQueryCreateTableConfiguration { + partition_by, + cluster_by, + options, + }, + )))) } else { Ok(None) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 29a9d30cc..821f70564 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -166,7 +166,7 @@ fn parse_create_table_with_options() { Statement::CreateTable { name, columns, - big_query_config, + table_config, .. } => { assert_eq!( @@ -213,7 +213,7 @@ fn parse_create_table_with_options() { columns ); assert_eq!( - BigQueryCreateTableConfiguration { + CreateTableConfiguration::BigQuery(BigQueryCreateTableConfiguration { partition_by: Some(Expr::Identifier(Ident::new("_PARTITIONDATE"))), cluster_by: Some(vec![Ident::new("userid"), Ident::new("age"),]), options: Some(vec![ @@ -228,8 +228,8 @@ fn parse_create_table_with_options() { )), }, ]) - }, - *big_query_config.unwrap(), + }), + *table_config.unwrap(), ) } _ => unreachable!(), From 87642c8ab39c11c68c4360a8ce2f5963df341019 Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Fri, 19 Jan 2024 14:18:09 +0100 Subject: [PATCH 4/4] Inline table options, add coment to ViewColumnDef --- src/ast/ddl.rs | 19 +++++-- src/ast/helpers/stmt_create_table.rs | 46 ++++++++++++---- src/ast/mod.rs | 78 ++++++++++------------------ src/parser/mod.rs | 32 +++++------- tests/sqlparser_bigquery.rs | 20 +++---- 5 files changed, 106 insertions(+), 89 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index de9a37248..c720c7aad 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -635,7 +635,20 @@ impl fmt::Display for ColumnDef { } } -/// Column definition for a view. +/// Column definition specified in a `CREATE VIEW` statement. +/// +/// Syntax +/// ```markdown +/// [OPTIONS(option, ...)] +/// +/// option: = +/// ``` +/// +/// Examples: +/// ```sql +/// name +/// age OPTIONS(description = "age column", tag = "prod") +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -741,7 +754,7 @@ pub enum ColumnOption { /// ``` /// [1]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#view_column_option_list /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#column_option_list - SqlOptions(Vec), + Options(Vec), } impl fmt::Display for ColumnOption { @@ -820,7 +833,7 @@ impl fmt::Display for ColumnOption { Ok(()) } } - SqlOptions(options) => { + Options(options) => { write!(f, "OPTIONS({})", display_comma_separated(options)) } } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index fce01418f..126542379 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - ColumnDef, CreateTableConfiguration, FileFormat, HiveDistributionStyle, HiveFormat, Ident, - ObjectName, OnCommit, Query, SqlOption, Statement, TableConstraint, + ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, + Query, SqlOption, Statement, TableConstraint, }; use crate::parser::ParserError; @@ -72,7 +72,9 @@ pub struct CreateTableBuilder { pub on_commit: Option, pub on_cluster: Option, pub order_by: Option>, - pub table_config: Option>, + pub partition_by: Option>, + pub cluster_by: Option>, + pub options: Option>, pub strict: bool, } @@ -106,7 +108,9 @@ impl CreateTableBuilder { on_commit: None, on_cluster: None, order_by: None, - table_config: None, + partition_by: None, + cluster_by: None, + options: None, strict: false, } } @@ -238,8 +242,18 @@ impl CreateTableBuilder { self } - pub fn table_config(mut self, table_config: Option>) -> Self { - self.table_config = table_config; + pub fn partition_by(mut self, partition_by: Option>) -> Self { + self.partition_by = partition_by; + self + } + + pub fn cluster_by(mut self, cluster_by: Option>) -> Self { + self.cluster_by = cluster_by; + self + } + + pub fn options(mut self, options: Option>) -> Self { + self.options = options; self } @@ -277,7 +291,9 @@ impl CreateTableBuilder { on_commit: self.on_commit, on_cluster: self.on_cluster, order_by: self.order_by, - table_config: self.table_config, + partition_by: self.partition_by, + cluster_by: self.cluster_by, + options: self.options, strict: self.strict, } } @@ -318,7 +334,9 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, - table_config: config, + partition_by, + cluster_by, + options, strict, } => Ok(Self { or_replace, @@ -348,7 +366,9 @@ impl TryFrom for CreateTableBuilder { on_commit, on_cluster, order_by, - table_config: config, + partition_by, + cluster_by, + options, strict, }), _ => Err(ParserError::ParserError(format!( @@ -358,6 +378,14 @@ impl TryFrom for CreateTableBuilder { } } +/// Helper return type when parsing configuration for a BigQuery `CREATE TABLE` statement. +#[derive(Default)] +pub(crate) struct BigQueryTableConfiguration { + pub partition_by: Option>, + pub cluster_by: Option>, + pub options: Option>, +} + #[cfg(test)] mod tests { use crate::ast::helpers::stmt_create_table::CreateTableBuilder; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 257cf94bf..43a5de661 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1632,8 +1632,15 @@ pub enum Statement { /// than empty (represented as ()), the latter meaning "no sorting". /// order_by: Option>, - /// Database specific configuration during table creation. - table_config: Option>, + /// BigQuery: A partition expression for the table. + /// + partition_by: Option>, + /// BigQuery: Table clustering column list. + /// + cluster_by: Option>, + /// BigQuery: Table options list. + /// + options: Option>, /// SQLite "STRICT" clause. /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. @@ -2826,7 +2833,9 @@ impl fmt::Display for Statement { on_commit, on_cluster, order_by, - table_config, + partition_by, + cluster_by, + options, strict, } => { // We want to allow the following options @@ -2983,26 +2992,22 @@ impl fmt::Display for Statement { if let Some(order_by) = order_by { write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; } - if let Some(CreateTableConfiguration::BigQuery(big_query_config)) = - table_config.as_ref().map(|c| c.as_ref()) - { - if let Some(partition_by) = big_query_config.partition_by.as_ref() { - write!(f, " PARTITION BY {partition_by}")?; - } - if let Some(cluster_by) = big_query_config.cluster_by.as_ref() { - write!( - f, - " CLUSTER BY {}", - display_comma_separated(cluster_by.as_slice()) - )?; - } - if let Some(options) = big_query_config.options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; - } + if let Some(partition_by) = partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = cluster_by.as_ref() { + write!( + f, + " CLUSTER BY {}", + display_comma_separated(cluster_by.as_slice()) + )?; + } + if let Some(options) = options.as_ref() { + write!( + f, + " OPTIONS({})", + display_comma_separated(options.as_slice()) + )?; } if let Some(query) = query { write!(f, " AS {query}")?; @@ -4550,33 +4555,6 @@ pub struct HiveFormat { pub location: Option, } -/// Represents BigQuery specific configuration like partitioning, clustering -/// information during table creation. -/// -/// -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct BigQueryCreateTableConfiguration { - /// A partition expression for the table. - /// - pub partition_by: Option, - /// Table clustering column list. - /// - pub cluster_by: Option>, - /// Table options list. - /// - pub options: Option>, -} - -/// Represents database specific configuration during table creation. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub enum CreateTableConfiguration { - BigQuery(BigQueryCreateTableConfiguration), -} - #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f3c5809b8..c898824e2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -27,7 +27,7 @@ use log::debug; use IsLateral::*; use IsOptional::*; -use crate::ast::helpers::stmt_create_table::CreateTableBuilder; +use crate::ast::helpers::stmt_create_table::{BigQueryTableConfiguration, CreateTableBuilder}; use crate::ast::*; use crate::dialect::*; use crate::keywords::{self, Keyword, ALL_KEYWORDS}; @@ -4184,10 +4184,10 @@ impl<'a> Parser<'a> { None }; - let table_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { + let big_query_config = if dialect_of!(self is BigQueryDialect | GenericDialect) { self.parse_optional_big_query_create_table_config()? } else { - None + Default::default() }; // Parse optional `AS ( query )` @@ -4273,7 +4273,9 @@ impl<'a> Parser<'a> { .collation(collation) .on_commit(on_commit) .on_cluster(on_cluster) - .table_config(table_config) + .partition_by(big_query_config.partition_by) + .cluster_by(big_query_config.cluster_by) + .options(big_query_config.options) .strict(strict) .build()) } @@ -4282,10 +4284,10 @@ impl<'a> Parser<'a> { /// fn parse_optional_big_query_create_table_config( &mut self, - ) -> Result>, ParserError> { + ) -> Result { let mut partition_by = None; if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { - partition_by = Some(self.parse_expr()?); + partition_by = Some(Box::new(self.parse_expr()?)); }; let mut cluster_by = None; @@ -4300,17 +4302,11 @@ impl<'a> Parser<'a> { } }; - if partition_by.is_some() || cluster_by.is_some() || options.is_some() { - Ok(Some(Box::new(CreateTableConfiguration::BigQuery( - BigQueryCreateTableConfiguration { - partition_by, - cluster_by, - options, - }, - )))) - } else { - Ok(None) - } + Ok(BigQueryTableConfiguration { + partition_by, + cluster_by, + options, + }) } pub fn parse_optional_procedure_parameters( @@ -4506,7 +4502,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::OPTIONS) { self.prev_token(); - Ok(Some(ColumnOption::SqlOptions( + Ok(Some(ColumnOption::Options( self.parse_options(Keyword::OPTIONS)?, ))) } else if self.parse_keyword(Keyword::AS) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 821f70564..90e1293df 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -166,7 +166,9 @@ fn parse_create_table_with_options() { Statement::CreateTable { name, columns, - table_config, + partition_by, + cluster_by, + options, .. } => { assert_eq!( @@ -186,7 +188,7 @@ fn parse_create_table_with_options() { }, ColumnOptionDef { name: None, - option: ColumnOption::SqlOptions(vec![SqlOption { + option: ColumnOption::Options(vec![SqlOption { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field x".to_string() @@ -201,7 +203,7 @@ fn parse_create_table_with_options() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::SqlOptions(vec![SqlOption { + option: ColumnOption::Options(vec![SqlOption { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field y".to_string() @@ -213,10 +215,10 @@ fn parse_create_table_with_options() { columns ); assert_eq!( - CreateTableConfiguration::BigQuery(BigQueryCreateTableConfiguration { - partition_by: Some(Expr::Identifier(Ident::new("_PARTITIONDATE"))), - cluster_by: Some(vec![Ident::new("userid"), Ident::new("age"),]), - options: Some(vec![ + ( + Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))), + Some(vec![Ident::new("userid"), Ident::new("age"),]), + Some(vec![ SqlOption { name: Ident::new("partition_expiration_days"), value: Expr::Value(number("1")), @@ -228,8 +230,8 @@ fn parse_create_table_with_options() { )), }, ]) - }), - *table_config.unwrap(), + ), + (partition_by, cluster_by, options) ) } _ => unreachable!(),