Add support for BigQuery table and view options (#1061)

This commit is contained in:
Ifeanyi Ubah 2024-01-23 23:21:53 +01:00 committed by GitHub
parent c7d2903c6d
commit 3a6d3ecba2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 465 additions and 54 deletions

View file

@ -26,6 +26,7 @@ use sqlparser_derive::{Visit, VisitMut};
use crate::ast::value::escape_single_quote_string; use crate::ast::value::escape_single_quote_string;
use crate::ast::{ use crate::ast::{
display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions,
SqlOption,
}; };
use crate::tokenizer::Token; use crate::tokenizer::Token;
@ -634,6 +635,42 @@ impl fmt::Display for ColumnDef {
} }
} }
/// Column definition specified in a `CREATE VIEW` statement.
///
/// Syntax
/// ```markdown
/// <name> [OPTIONS(option, ...)]
///
/// option: <name> = <value>
/// ```
///
/// 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))]
pub struct ViewColumnDef {
pub name: Ident,
pub options: Option<Vec<SqlOption>>,
}
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 <name> ] <column-option>`. /// An optionally-named `ColumnOption`: `[ CONSTRAINT <name> ] <column-option>`.
/// ///
/// Note that implementations are substantially more permissive than the ANSI /// Note that implementations are substantially more permissive than the ANSI
@ -710,6 +747,14 @@ pub enum ColumnOption {
/// false if 'GENERATED ALWAYS' is skipped (option starts with AS) /// false if 'GENERATED ALWAYS' is skipped (option starts with AS)
generated_keyword: bool, generated_keyword: bool,
}, },
/// 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
Options(Vec<SqlOption>),
} }
impl fmt::Display for ColumnOption { impl fmt::Display for ColumnOption {
@ -788,6 +833,9 @@ impl fmt::Display for ColumnOption {
Ok(()) Ok(())
} }
} }
Options(options) => {
write!(f, "OPTIONS({})", display_comma_separated(options))
}
} }
} }
} }

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use sqlparser_derive::{Visit, VisitMut}; use sqlparser_derive::{Visit, VisitMut};
use crate::ast::{ use crate::ast::{
ColumnDef, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, Query, ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit,
SqlOption, Statement, TableConstraint, Query, SqlOption, Statement, TableConstraint,
}; };
use crate::parser::ParserError; use crate::parser::ParserError;
@ -72,6 +72,9 @@ pub struct CreateTableBuilder {
pub on_commit: Option<OnCommit>, pub on_commit: Option<OnCommit>,
pub on_cluster: Option<String>, pub on_cluster: Option<String>,
pub order_by: Option<Vec<Ident>>, pub order_by: Option<Vec<Ident>>,
pub partition_by: Option<Box<Expr>>,
pub cluster_by: Option<Vec<Ident>>,
pub options: Option<Vec<SqlOption>>,
pub strict: bool, pub strict: bool,
} }
@ -105,6 +108,9 @@ impl CreateTableBuilder {
on_commit: None, on_commit: None,
on_cluster: None, on_cluster: None,
order_by: None, order_by: None,
partition_by: None,
cluster_by: None,
options: None,
strict: false, strict: false,
} }
} }
@ -236,6 +242,21 @@ impl CreateTableBuilder {
self self
} }
pub fn partition_by(mut self, partition_by: Option<Box<Expr>>) -> Self {
self.partition_by = partition_by;
self
}
pub fn cluster_by(mut self, cluster_by: Option<Vec<Ident>>) -> Self {
self.cluster_by = cluster_by;
self
}
pub fn options(mut self, options: Option<Vec<SqlOption>>) -> Self {
self.options = options;
self
}
pub fn strict(mut self, strict: bool) -> Self { pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict; self.strict = strict;
self self
@ -270,6 +291,9 @@ impl CreateTableBuilder {
on_commit: self.on_commit, on_commit: self.on_commit,
on_cluster: self.on_cluster, on_cluster: self.on_cluster,
order_by: self.order_by, order_by: self.order_by,
partition_by: self.partition_by,
cluster_by: self.cluster_by,
options: self.options,
strict: self.strict, strict: self.strict,
} }
} }
@ -310,6 +334,9 @@ impl TryFrom<Statement> for CreateTableBuilder {
on_commit, on_commit,
on_cluster, on_cluster,
order_by, order_by,
partition_by,
cluster_by,
options,
strict, strict,
} => Ok(Self { } => Ok(Self {
or_replace, or_replace,
@ -339,6 +366,9 @@ impl TryFrom<Statement> for CreateTableBuilder {
on_commit, on_commit,
on_cluster, on_cluster,
order_by, order_by,
partition_by,
cluster_by,
options,
strict, strict,
}), }),
_ => Err(ParserError::ParserError(format!( _ => Err(ParserError::ParserError(format!(
@ -348,6 +378,14 @@ impl TryFrom<Statement> for CreateTableBuilder {
} }
} }
/// Helper return type when parsing configuration for a BigQuery `CREATE TABLE` statement.
#[derive(Default)]
pub(crate) struct BigQueryTableConfiguration {
pub partition_by: Option<Box<Expr>>,
pub cluster_by: Option<Vec<Ident>>,
pub options: Option<Vec<SqlOption>>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_create_table::CreateTableBuilder;

View file

@ -34,7 +34,7 @@ pub use self::ddl::{
AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption,
ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition,
ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef,
UserDefinedTypeRepresentation, UserDefinedTypeRepresentation, ViewColumnDef,
}; };
pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::operator::{BinaryOperator, UnaryOperator};
pub use self::query::{ pub use self::query::{
@ -1367,6 +1367,38 @@ pub enum Password {
NullPassword, 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")`
///
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
With(Vec<SqlOption>),
/// Options specified using the `OPTIONS` keyword.
/// e.g. `OPTIONS(description = "123")`
///
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
Options(Vec<SqlOption>),
}
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.) /// A top-level statement (SELECT, INSERT, CREATE, etc.)
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@ -1550,9 +1582,9 @@ pub enum Statement {
materialized: bool, materialized: bool,
/// View name /// View name
name: ObjectName, name: ObjectName,
columns: Vec<Ident>, columns: Vec<ViewColumnDef>,
query: Box<Query>, query: Box<Query>,
with_options: Vec<SqlOption>, options: CreateTableOptions,
cluster_by: Vec<Ident>, cluster_by: Vec<Ident>,
/// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause <https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_VIEW.html> /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause <https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_VIEW.html>
with_no_schema_binding: bool, with_no_schema_binding: bool,
@ -1600,6 +1632,15 @@ pub enum Statement {
/// than empty (represented as ()), the latter meaning "no sorting". /// than empty (represented as ()), the latter meaning "no sorting".
/// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/> /// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
order_by: Option<Vec<Ident>>, order_by: Option<Vec<Ident>>,
/// BigQuery: A partition expression for the table.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#partition_expression>
partition_by: Option<Box<Expr>>,
/// BigQuery: Table clustering column list.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
cluster_by: Option<Vec<Ident>>,
/// BigQuery: Table options list.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
options: Option<Vec<SqlOption>>,
/// SQLite "STRICT" clause. /// SQLite "STRICT" clause.
/// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// if the "STRICT" table-option keyword is added to the end, after the closing ")",
/// then strict typing rules apply to that table. /// then strict typing rules apply to that table.
@ -2731,7 +2772,7 @@ impl fmt::Display for Statement {
columns, columns,
query, query,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding, with_no_schema_binding,
if_not_exists, if_not_exists,
@ -2746,8 +2787,8 @@ impl fmt::Display for Statement {
temporary = if *temporary { "TEMPORARY " } else { "" }, temporary = if *temporary { "TEMPORARY " } else { "" },
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }
)?; )?;
if !with_options.is_empty() { if matches!(options, CreateTableOptions::With(_)) {
write!(f, " WITH ({})", display_comma_separated(with_options))?; write!(f, " {options}")?;
} }
if !columns.is_empty() { if !columns.is_empty() {
write!(f, " ({})", display_comma_separated(columns))?; write!(f, " ({})", display_comma_separated(columns))?;
@ -2755,6 +2796,9 @@ impl fmt::Display for Statement {
if !cluster_by.is_empty() { if !cluster_by.is_empty() {
write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?;
} }
if matches!(options, CreateTableOptions::Options(_)) {
write!(f, " {options}")?;
}
write!(f, " AS {query}")?; write!(f, " AS {query}")?;
if *with_no_schema_binding { if *with_no_schema_binding {
write!(f, " WITH NO SCHEMA BINDING")?; write!(f, " WITH NO SCHEMA BINDING")?;
@ -2789,6 +2833,9 @@ impl fmt::Display for Statement {
on_commit, on_commit,
on_cluster, on_cluster,
order_by, order_by,
partition_by,
cluster_by,
options,
strict, strict,
} => { } => {
// We want to allow the following options // We want to allow the following options
@ -2945,6 +2992,23 @@ impl fmt::Display for Statement {
if let Some(order_by) = order_by { if let Some(order_by) = order_by {
write!(f, " ORDER BY ({})", display_comma_separated(order_by))?; write!(f, " ORDER BY ({})", display_comma_separated(order_by))?;
} }
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 { if let Some(query) = query {
write!(f, " AS {query}")?; write!(f, " AS {query}")?;
} }
@ -4496,7 +4560,7 @@ pub struct HiveFormat {
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct SqlOption { pub struct SqlOption {
pub name: Ident, pub name: Ident,
pub value: Value, pub value: Expr,
} }
impl fmt::Display for SqlOption { impl fmt::Display for SqlOption {

View file

@ -27,7 +27,7 @@ use log::debug;
use IsLateral::*; use IsLateral::*;
use IsOptional::*; use IsOptional::*;
use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_create_table::{BigQueryTableConfiguration, CreateTableBuilder};
use crate::ast::*; use crate::ast::*;
use crate::dialect::*; use crate::dialect::*;
use crate::keywords::{self, Keyword, ALL_KEYWORDS}; use crate::keywords::{self, Keyword, ALL_KEYWORDS};
@ -3463,8 +3463,12 @@ impl<'a> Parser<'a> {
// Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // 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. // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either.
let name = self.parse_object_name()?; 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)?; 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) { let cluster_by = if self.parse_keyword(Keyword::CLUSTER) {
self.expect_keyword(Keyword::BY)?; self.expect_keyword(Keyword::BY)?;
@ -3473,6 +3477,17 @@ impl<'a> Parser<'a> {
vec![] 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)?; self.expect_keyword(Keyword::AS)?;
let query = Box::new(self.parse_query()?); let query = Box::new(self.parse_query()?);
// Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here.
@ -3491,7 +3506,7 @@ impl<'a> Parser<'a> {
query, query,
materialized, materialized,
or_replace, or_replace,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding, with_no_schema_binding,
if_not_exists, if_not_exists,
@ -4177,6 +4192,12 @@ impl<'a> Parser<'a> {
None None
}; };
let big_query_config = if dialect_of!(self is BigQueryDialect | GenericDialect) {
self.parse_optional_big_query_create_table_config()?
} else {
Default::default()
};
// Parse optional `AS ( query )` // Parse optional `AS ( query )`
let query = if self.parse_keyword(Keyword::AS) { let query = if self.parse_keyword(Keyword::AS) {
Some(Box::new(self.parse_query()?)) Some(Box::new(self.parse_query()?))
@ -4260,10 +4281,42 @@ impl<'a> Parser<'a> {
.collation(collation) .collation(collation)
.on_commit(on_commit) .on_commit(on_commit)
.on_cluster(on_cluster) .on_cluster(on_cluster)
.partition_by(big_query_config.partition_by)
.cluster_by(big_query_config.cluster_by)
.options(big_query_config.options)
.strict(strict) .strict(strict)
.build()) .build())
} }
/// Parse configuration like partitioning, clustering information during big-query table creation.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2>
fn parse_optional_big_query_create_table_config(
&mut self,
) -> Result<BigQueryTableConfiguration, ParserError> {
let mut partition_by = None;
if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) {
partition_by = Some(Box::new(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)?);
}
};
Ok(BigQueryTableConfiguration {
partition_by,
cluster_by,
options,
})
}
pub fn parse_optional_procedure_parameters( pub fn parse_optional_procedure_parameters(
&mut self, &mut self,
) -> Result<Option<Vec<ProcedureParam>>, ParserError> { ) -> Result<Option<Vec<ProcedureParam>>, ParserError> {
@ -4453,6 +4506,13 @@ impl<'a> Parser<'a> {
Ok(Some(ColumnOption::OnUpdate(expr))) Ok(Some(ColumnOption::OnUpdate(expr)))
} else if self.parse_keyword(Keyword::GENERATED) { } else if self.parse_keyword(Keyword::GENERATED) {
self.parse_optional_column_option_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::Options(
self.parse_options(Keyword::OPTIONS)?,
)))
} else if self.parse_keyword(Keyword::AS) } else if self.parse_keyword(Keyword::AS)
&& dialect_of!(self is MySqlDialect | SQLiteDialect | DuckDbDialect | GenericDialect) && dialect_of!(self is MySqlDialect | SQLiteDialect | DuckDbDialect | GenericDialect)
{ {
@ -4731,7 +4791,7 @@ impl<'a> Parser<'a> {
pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> { pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
let name = self.parse_identifier()?; let name = self.parse_identifier()?;
self.expect_token(&Token::Eq)?; self.expect_token(&Token::Eq)?;
let value = self.parse_value()?; let value = self.parse_expr()?;
Ok(SqlOption { name, value }) Ok(SqlOption { name, value })
} }
@ -5952,6 +6012,36 @@ impl<'a> Parser<'a> {
} }
} }
/// Parses a parenthesized, comma-separated list of column definitions within a view.
fn parse_view_columns(&mut self) -> Result<Vec<ViewColumnDef>, 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<ViewColumnDef, ParserError> {
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 /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers
pub fn parse_parenthesized_column_list( pub fn parse_parenthesized_column_list(
&mut self, &mut self,

View file

@ -86,6 +86,168 @@ fn parse_raw_literal() {
panic!("invalid query") panic!("invalid query")
} }
#[test]
fn parse_create_view_with_options() {
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) {
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 = 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) {
Statement::CreateTable {
name,
columns,
partition_by,
cluster_by,
options,
..
} => {
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::Options(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::Options(vec![SqlOption {
name: Ident::new("description"),
value: Expr::Value(Value::DoubleQuotedString(
"field y".to_string()
)),
},])
}]
},
],
columns
);
assert_eq!(
(
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")),
},
SqlOption {
name: Ident::new("description"),
value: Expr::Value(Value::DoubleQuotedString(
"table option description".to_string()
)),
},
])
),
(partition_by, cluster_by, options)
)
}
_ => unreachable!(),
}
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);
}
#[test] #[test]
fn parse_nested_data_types() { fn parse_nested_data_types() {
let sql = "CREATE TABLE table (x STRUCT<a ARRAY<INT64>, b BYTES(42)>, y ARRAY<STRUCT<INT64>>)"; let sql = "CREATE TABLE table (x STRUCT<a ARRAY<INT64>, b BYTES(42)>, y ARRAY<STRUCT<INT64>>)";

View file

@ -2997,11 +2997,11 @@ fn parse_create_table_with_options() {
vec![ vec![
SqlOption { SqlOption {
name: "foo".into(), name: "foo".into(),
value: Value::SingleQuotedString("bar".into()), value: Expr::Value(Value::SingleQuotedString("bar".into())),
}, },
SqlOption { SqlOption {
name: "a".into(), name: "a".into(),
value: number("123"), value: Expr::Value(number("123")),
}, },
], ],
with_options with_options
@ -3260,11 +3260,11 @@ fn parse_alter_view_with_options() {
vec![ vec![
SqlOption { SqlOption {
name: "foo".into(), name: "foo".into(),
value: Value::SingleQuotedString("bar".into()), value: Expr::Value(Value::SingleQuotedString("bar".into())),
}, },
SqlOption { SqlOption {
name: "a".into(), name: "a".into(),
value: number("123"), value: Expr::Value(number("123")),
}, },
], ],
with_options with_options
@ -5589,18 +5589,18 @@ fn parse_create_view() {
query, query,
or_replace, or_replace,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding: late_binding, with_no_schema_binding: late_binding,
if_not_exists, if_not_exists,
temporary, temporary,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!(Vec::<Ident>::new(), columns); assert_eq!(Vec::<ViewColumnDef>::new(), columns);
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(!materialized); assert!(!materialized);
assert!(!or_replace); assert!(!or_replace);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert_eq!(cluster_by, vec![]); assert_eq!(cluster_by, vec![]);
assert!(!late_binding); assert!(!late_binding);
assert!(!if_not_exists); assert!(!if_not_exists);
@ -5614,19 +5614,19 @@ fn parse_create_view() {
fn parse_create_view_with_options() { fn parse_create_view_with_options() {
let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1";
match verified_stmt(sql) { match verified_stmt(sql) {
Statement::CreateView { with_options, .. } => { Statement::CreateView { options, .. } => {
assert_eq!( assert_eq!(
vec![ CreateTableOptions::With(vec![
SqlOption { SqlOption {
name: "foo".into(), name: "foo".into(),
value: Value::SingleQuotedString("bar".into()), value: Expr::Value(Value::SingleQuotedString("bar".into())),
}, },
SqlOption { SqlOption {
name: "a".into(), name: "a".into(),
value: number("123"), value: Expr::Value(number("123")),
}, },
], ]),
with_options options
); );
} }
_ => unreachable!(), _ => unreachable!(),
@ -5641,7 +5641,7 @@ fn parse_create_view_with_columns() {
name, name,
columns, columns,
or_replace, or_replace,
with_options, options,
query, query,
materialized, materialized,
cluster_by, cluster_by,
@ -5650,8 +5650,17 @@ fn parse_create_view_with_columns() {
temporary, temporary,
} => { } => {
assert_eq!("v", name.to_string()); assert_eq!("v", name.to_string());
assert_eq!(columns, vec![Ident::new("has"), Ident::new("cols")]); assert_eq!(
assert_eq!(with_options, vec![]); columns,
vec![Ident::new("has"), Ident::new("cols"),]
.into_iter()
.map(|name| ViewColumnDef {
name,
options: None
})
.collect::<Vec<_>>()
);
assert_eq!(options, CreateTableOptions::None);
assert_eq!("SELECT 1, 2", query.to_string()); assert_eq!("SELECT 1, 2", query.to_string());
assert!(!materialized); assert!(!materialized);
assert!(!or_replace); assert!(!or_replace);
@ -5674,18 +5683,18 @@ fn parse_create_view_temporary() {
query, query,
or_replace, or_replace,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding: late_binding, with_no_schema_binding: late_binding,
if_not_exists, if_not_exists,
temporary, temporary,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!(Vec::<Ident>::new(), columns); assert_eq!(Vec::<ViewColumnDef>::new(), columns);
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(!materialized); assert!(!materialized);
assert!(!or_replace); assert!(!or_replace);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert_eq!(cluster_by, vec![]); assert_eq!(cluster_by, vec![]);
assert!(!late_binding); assert!(!late_binding);
assert!(!if_not_exists); assert!(!if_not_exists);
@ -5703,7 +5712,7 @@ fn parse_create_or_replace_view() {
name, name,
columns, columns,
or_replace, or_replace,
with_options, options,
query, query,
materialized, materialized,
cluster_by, cluster_by,
@ -5713,7 +5722,7 @@ fn parse_create_or_replace_view() {
} => { } => {
assert_eq!("v", name.to_string()); assert_eq!("v", name.to_string());
assert_eq!(columns, vec![]); assert_eq!(columns, vec![]);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert_eq!("SELECT 1", query.to_string()); assert_eq!("SELECT 1", query.to_string());
assert!(!materialized); assert!(!materialized);
assert!(or_replace); assert!(or_replace);
@ -5738,7 +5747,7 @@ fn parse_create_or_replace_materialized_view() {
name, name,
columns, columns,
or_replace, or_replace,
with_options, options,
query, query,
materialized, materialized,
cluster_by, cluster_by,
@ -5748,7 +5757,7 @@ fn parse_create_or_replace_materialized_view() {
} => { } => {
assert_eq!("v", name.to_string()); assert_eq!("v", name.to_string());
assert_eq!(columns, vec![]); assert_eq!(columns, vec![]);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert_eq!("SELECT 1", query.to_string()); assert_eq!("SELECT 1", query.to_string());
assert!(materialized); assert!(materialized);
assert!(or_replace); assert!(or_replace);
@ -5771,17 +5780,17 @@ fn parse_create_materialized_view() {
columns, columns,
query, query,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding: late_binding, with_no_schema_binding: late_binding,
if_not_exists, if_not_exists,
temporary, temporary,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!(Vec::<Ident>::new(), columns); assert_eq!(Vec::<ViewColumnDef>::new(), columns);
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(materialized); assert!(materialized);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert!(!or_replace); assert!(!or_replace);
assert_eq!(cluster_by, vec![]); assert_eq!(cluster_by, vec![]);
assert!(!late_binding); assert!(!late_binding);
@ -5802,17 +5811,17 @@ fn parse_create_materialized_view_with_cluster_by() {
columns, columns,
query, query,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding: late_binding, with_no_schema_binding: late_binding,
if_not_exists, if_not_exists,
temporary, temporary,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!(Vec::<Ident>::new(), columns); assert_eq!(Vec::<ViewColumnDef>::new(), columns);
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(materialized); assert!(materialized);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert!(!or_replace); assert!(!or_replace);
assert_eq!(cluster_by, vec![Ident::new("foo")]); assert_eq!(cluster_by, vec![Ident::new("foo")]);
assert!(!late_binding); assert!(!late_binding);
@ -7439,11 +7448,11 @@ fn parse_cache_table() {
options: vec![ options: vec![
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K1"), name: Ident::with_quote('\'', "K1"),
value: Value::SingleQuotedString("V1".into()), value: Expr::Value(Value::SingleQuotedString("V1".into())),
}, },
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K2"), name: Ident::with_quote('\'', "K2"),
value: number("0.88"), value: Expr::Value(number("0.88")),
}, },
], ],
query: None, query: None,
@ -7464,11 +7473,11 @@ fn parse_cache_table() {
options: vec![ options: vec![
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K1"), name: Ident::with_quote('\'', "K1"),
value: Value::SingleQuotedString("V1".into()), value: Expr::Value(Value::SingleQuotedString("V1".into())),
}, },
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K2"), name: Ident::with_quote('\'', "K2"),
value: number("0.88"), value: Expr::Value(number("0.88")),
}, },
], ],
query: Some(query.clone()), query: Some(query.clone()),
@ -7489,11 +7498,11 @@ fn parse_cache_table() {
options: vec![ options: vec![
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K1"), name: Ident::with_quote('\'', "K1"),
value: Value::SingleQuotedString("V1".into()), value: Expr::Value(Value::SingleQuotedString("V1".into())),
}, },
SqlOption { SqlOption {
name: Ident::with_quote('\'', "K2"), name: Ident::with_quote('\'', "K2"),
value: number("0.88"), value: Expr::Value(number("0.88")),
}, },
], ],
query: Some(query.clone()), query: Some(query.clone()),

View file

@ -459,15 +459,15 @@ fn parse_create_table_with_defaults() {
vec![ vec![
SqlOption { SqlOption {
name: "fillfactor".into(), name: "fillfactor".into(),
value: number("20") value: Expr::Value(number("20"))
}, },
SqlOption { SqlOption {
name: "user_catalog_table".into(), name: "user_catalog_table".into(),
value: Value::Boolean(true) value: Expr::Value(Value::Boolean(true))
}, },
SqlOption { SqlOption {
name: "autovacuum_vacuum_threshold".into(), name: "autovacuum_vacuum_threshold".into(),
value: number("100") value: Expr::Value(number("100"))
}, },
] ]
); );

View file

@ -165,18 +165,18 @@ fn parse_create_view_temporary_if_not_exists() {
query, query,
or_replace, or_replace,
materialized, materialized,
with_options, options,
cluster_by, cluster_by,
with_no_schema_binding: late_binding, with_no_schema_binding: late_binding,
if_not_exists, if_not_exists,
temporary, temporary,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!(Vec::<Ident>::new(), columns); assert_eq!(Vec::<ViewColumnDef>::new(), columns);
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(!materialized); assert!(!materialized);
assert!(!or_replace); assert!(!or_replace);
assert_eq!(with_options, vec![]); assert_eq!(options, CreateTableOptions::None);
assert_eq!(cluster_by, vec![]); assert_eq!(cluster_by, vec![]);
assert!(!late_binding); assert!(!late_binding);
assert!(if_not_exists); assert!(if_not_exists);