Add support for snowflake exclusive create table options (#1233)

Co-authored-by: Ilson Roberto Balliego Junior <ilson@validio.io>
This commit is contained in:
Ilson Balliego 2024-06-09 23:47:21 +02:00 committed by GitHub
parent 3c33ac15bd
commit be77ce50ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1029 additions and 31 deletions

View file

@ -22,10 +22,11 @@ use sqlparser_derive::{Visit, VisitMut};
pub use super::ddl::{ColumnDef, TableConstraint};
use super::{
display_comma_separated, display_separated, Expr, FileFormat, FromTable, HiveDistributionStyle,
HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases, MysqlInsertPriority, ObjectName,
OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, SelectItem, SqlOption,
SqliteOnConflict, TableEngine, TableWithJoins,
display_comma_separated, display_separated, CommentDef, Expr, FileFormat, FromTable,
HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InsertAliases,
MysqlInsertPriority, ObjectName, OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query,
RowAccessPolicy, SelectItem, SqlOption, SqliteOnConflict, TableEngine, TableWithJoins, Tag,
WrappedCollection,
};
/// CREATE INDEX statement.
@ -57,6 +58,7 @@ pub struct CreateTable {
pub global: Option<bool>,
pub if_not_exists: bool,
pub transient: bool,
pub volatile: bool,
/// Table name
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
pub name: ObjectName,
@ -74,7 +76,7 @@ pub struct CreateTable {
pub like: Option<ObjectName>,
pub clone: Option<ObjectName>,
pub engine: Option<TableEngine>,
pub comment: Option<String>,
pub comment: Option<CommentDef>,
pub auto_increment_offset: Option<u32>,
pub default_charset: Option<String>,
pub collation: Option<String>,
@ -94,7 +96,7 @@ pub struct CreateTable {
pub 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>
pub cluster_by: Option<Vec<Ident>>,
pub cluster_by: Option<WrappedCollection<Vec<Ident>>>,
/// BigQuery: Table options list.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
pub options: Option<Vec<SqlOption>>,
@ -102,6 +104,33 @@ pub struct CreateTable {
/// if the "STRICT" table-option keyword is added to the end, after the closing ")",
/// then strict typing rules apply to that table.
pub strict: bool,
/// Snowflake "COPY GRANTS" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub copy_grants: bool,
/// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub enable_schema_evolution: Option<bool>,
/// Snowflake "CHANGE_TRACKING" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub change_tracking: Option<bool>,
/// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub data_retention_time_in_days: Option<u64>,
/// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub max_data_extension_time_in_days: Option<u64>,
/// Snowflake "DEFAULT_DDL_COLLATION" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub default_ddl_collation: Option<String>,
/// Snowflake "WITH AGGREGATION POLICY" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub with_aggregation_policy: Option<ObjectName>,
/// Snowflake "WITH ROW ACCESS POLICY" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub with_row_access_policy: Option<RowAccessPolicy>,
/// Snowflake "WITH TAG" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub with_tags: Option<Vec<Tag>>,
}
impl Display for CreateTable {
@ -115,7 +144,7 @@ impl Display for CreateTable {
// `CREATE TABLE t (a INT) AS SELECT a from t2`
write!(
f,
"CREATE {or_replace}{external}{global}{temporary}{transient}TABLE {if_not_exists}{name}",
"CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}TABLE {if_not_exists}{name}",
or_replace = if self.or_replace { "OR REPLACE " } else { "" },
external = if self.external { "EXTERNAL " } else { "" },
global = self.global
@ -130,6 +159,7 @@ impl Display for CreateTable {
if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" },
temporary = if self.temporary { "TEMPORARY " } else { "" },
transient = if self.transient { "TRANSIENT " } else { "" },
volatile = if self.volatile { "VOLATILE " } else { "" },
name = self.name,
)?;
if let Some(on_cluster) = &self.on_cluster {
@ -260,9 +290,17 @@ impl Display for CreateTable {
if let Some(engine) = &self.engine {
write!(f, " ENGINE={engine}")?;
}
if let Some(comment) = &self.comment {
write!(f, " COMMENT '{comment}'")?;
if let Some(comment_def) = &self.comment {
match comment_def {
CommentDef::WithEq(comment) => {
write!(f, " COMMENT = '{comment}'")?;
}
CommentDef::WithoutEq(comment) => {
write!(f, " COMMENT '{comment}'")?;
}
}
}
if let Some(auto_increment_offset) = self.auto_increment_offset {
write!(f, " AUTO_INCREMENT {auto_increment_offset}")?;
}
@ -276,12 +314,9 @@ impl Display for CreateTable {
write!(f, " PARTITION BY {partition_by}")?;
}
if let Some(cluster_by) = self.cluster_by.as_ref() {
write!(
f,
" CLUSTER BY {}",
display_comma_separated(cluster_by.as_slice())
)?;
write!(f, " CLUSTER BY {cluster_by}")?;
}
if let Some(options) = self.options.as_ref() {
write!(
f,
@ -289,6 +324,57 @@ impl Display for CreateTable {
display_comma_separated(options.as_slice())
)?;
}
if self.copy_grants {
write!(f, " COPY GRANTS")?;
}
if let Some(is_enabled) = self.enable_schema_evolution {
write!(
f,
" ENABLE_SCHEMA_EVOLUTION={}",
if is_enabled { "TRUE" } else { "FALSE" }
)?;
}
if let Some(is_enabled) = self.change_tracking {
write!(
f,
" CHANGE_TRACKING={}",
if is_enabled { "TRUE" } else { "FALSE" }
)?;
}
if let Some(data_retention_time_in_days) = self.data_retention_time_in_days {
write!(
f,
" DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}",
)?;
}
if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days {
write!(
f,
" MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}",
)?;
}
if let Some(default_ddl_collation) = &self.default_ddl_collation {
write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?;
}
if let Some(with_aggregation_policy) = &self.with_aggregation_policy {
write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?;
}
if let Some(row_access_policy) = &self.with_row_access_policy {
write!(f, " {row_access_policy}",)?;
}
if let Some(tag) = &self.with_tags {
write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?;
}
if let Some(query) = &self.query {
write!(f, " AS {query}")?;
}

View file

@ -9,8 +9,9 @@ use sqlparser_derive::{Visit, VisitMut};
use super::super::dml::CreateTable;
use crate::ast::{
ColumnDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit,
OneOrManyWithParens, Query, SqlOption, Statement, TableConstraint, TableEngine,
ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName,
OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement, TableConstraint,
TableEngine, Tag, WrappedCollection,
};
use crate::parser::ParserError;
@ -52,6 +53,7 @@ pub struct CreateTableBuilder {
pub global: Option<bool>,
pub if_not_exists: bool,
pub transient: bool,
pub volatile: bool,
pub name: ObjectName,
pub columns: Vec<ColumnDef>,
pub constraints: Vec<TableConstraint>,
@ -66,7 +68,7 @@ pub struct CreateTableBuilder {
pub like: Option<ObjectName>,
pub clone: Option<ObjectName>,
pub engine: Option<TableEngine>,
pub comment: Option<String>,
pub comment: Option<CommentDef>,
pub auto_increment_offset: Option<u32>,
pub default_charset: Option<String>,
pub collation: Option<String>,
@ -75,9 +77,18 @@ pub struct CreateTableBuilder {
pub primary_key: Option<Box<Expr>>,
pub order_by: Option<OneOrManyWithParens<Expr>>,
pub partition_by: Option<Box<Expr>>,
pub cluster_by: Option<Vec<Ident>>,
pub cluster_by: Option<WrappedCollection<Vec<Ident>>>,
pub options: Option<Vec<SqlOption>>,
pub strict: bool,
pub copy_grants: bool,
pub enable_schema_evolution: Option<bool>,
pub change_tracking: Option<bool>,
pub data_retention_time_in_days: Option<u64>,
pub max_data_extension_time_in_days: Option<u64>,
pub default_ddl_collation: Option<String>,
pub with_aggregation_policy: Option<ObjectName>,
pub with_row_access_policy: Option<RowAccessPolicy>,
pub with_tags: Option<Vec<Tag>>,
}
impl CreateTableBuilder {
@ -89,6 +100,7 @@ impl CreateTableBuilder {
global: None,
if_not_exists: false,
transient: false,
volatile: false,
name,
columns: vec![],
constraints: vec![],
@ -115,6 +127,15 @@ impl CreateTableBuilder {
cluster_by: None,
options: None,
strict: false,
copy_grants: false,
enable_schema_evolution: None,
change_tracking: None,
data_retention_time_in_days: None,
max_data_extension_time_in_days: None,
default_ddl_collation: None,
with_aggregation_policy: None,
with_row_access_policy: None,
with_tags: None,
}
}
pub fn or_replace(mut self, or_replace: bool) -> Self {
@ -147,6 +168,11 @@ impl CreateTableBuilder {
self
}
pub fn volatile(mut self, volatile: bool) -> Self {
self.volatile = volatile;
self
}
pub fn columns(mut self, columns: Vec<ColumnDef>) -> Self {
self.columns = columns;
self
@ -210,7 +236,7 @@ impl CreateTableBuilder {
self
}
pub fn comment(mut self, comment: Option<String>) -> Self {
pub fn comment(mut self, comment: Option<CommentDef>) -> Self {
self.comment = comment;
self
}
@ -255,7 +281,7 @@ impl CreateTableBuilder {
self
}
pub fn cluster_by(mut self, cluster_by: Option<Vec<Ident>>) -> Self {
pub fn cluster_by(mut self, cluster_by: Option<WrappedCollection<Vec<Ident>>>) -> Self {
self.cluster_by = cluster_by;
self
}
@ -270,6 +296,57 @@ impl CreateTableBuilder {
self
}
pub fn copy_grants(mut self, copy_grants: bool) -> Self {
self.copy_grants = copy_grants;
self
}
pub fn enable_schema_evolution(mut self, enable_schema_evolution: Option<bool>) -> Self {
self.enable_schema_evolution = enable_schema_evolution;
self
}
pub fn change_tracking(mut self, change_tracking: Option<bool>) -> Self {
self.change_tracking = change_tracking;
self
}
pub fn data_retention_time_in_days(mut self, data_retention_time_in_days: Option<u64>) -> Self {
self.data_retention_time_in_days = data_retention_time_in_days;
self
}
pub fn max_data_extension_time_in_days(
mut self,
max_data_extension_time_in_days: Option<u64>,
) -> Self {
self.max_data_extension_time_in_days = max_data_extension_time_in_days;
self
}
pub fn default_ddl_collation(mut self, default_ddl_collation: Option<String>) -> Self {
self.default_ddl_collation = default_ddl_collation;
self
}
pub fn with_aggregation_policy(mut self, with_aggregation_policy: Option<ObjectName>) -> Self {
self.with_aggregation_policy = with_aggregation_policy;
self
}
pub fn with_row_access_policy(
mut self,
with_row_access_policy: Option<RowAccessPolicy>,
) -> Self {
self.with_row_access_policy = with_row_access_policy;
self
}
pub fn with_tags(mut self, with_tags: Option<Vec<Tag>>) -> Self {
self.with_tags = with_tags;
self
}
pub fn build(self) -> Statement {
Statement::CreateTable(CreateTable {
or_replace: self.or_replace,
@ -278,6 +355,7 @@ impl CreateTableBuilder {
global: self.global,
if_not_exists: self.if_not_exists,
transient: self.transient,
volatile: self.volatile,
name: self.name,
columns: self.columns,
constraints: self.constraints,
@ -304,6 +382,15 @@ impl CreateTableBuilder {
cluster_by: self.cluster_by,
options: self.options,
strict: self.strict,
copy_grants: self.copy_grants,
enable_schema_evolution: self.enable_schema_evolution,
change_tracking: self.change_tracking,
data_retention_time_in_days: self.data_retention_time_in_days,
max_data_extension_time_in_days: self.max_data_extension_time_in_days,
default_ddl_collation: self.default_ddl_collation,
with_aggregation_policy: self.with_aggregation_policy,
with_row_access_policy: self.with_row_access_policy,
with_tags: self.with_tags,
})
}
}
@ -322,6 +409,7 @@ impl TryFrom<Statement> for CreateTableBuilder {
global,
if_not_exists,
transient,
volatile,
name,
columns,
constraints,
@ -348,6 +436,15 @@ impl TryFrom<Statement> for CreateTableBuilder {
cluster_by,
options,
strict,
copy_grants,
enable_schema_evolution,
change_tracking,
data_retention_time_in_days,
max_data_extension_time_in_days,
default_ddl_collation,
with_aggregation_policy,
with_row_access_policy,
with_tags,
}) => Ok(Self {
or_replace,
temporary,
@ -381,6 +478,16 @@ impl TryFrom<Statement> for CreateTableBuilder {
cluster_by,
options,
strict,
copy_grants,
enable_schema_evolution,
change_tracking,
data_retention_time_in_days,
max_data_extension_time_in_days,
default_ddl_collation,
with_aggregation_policy,
with_row_access_policy,
with_tags,
volatile,
}),
_ => Err(ParserError::ParserError(format!(
"Expected create table statement, but received: {stmt}"
@ -393,7 +500,7 @@ impl TryFrom<Statement> for CreateTableBuilder {
#[derive(Default)]
pub(crate) struct BigQueryTableConfiguration {
pub partition_by: Option<Box<Expr>>,
pub cluster_by: Option<Vec<Ident>>,
pub cluster_by: Option<WrappedCollection<Vec<Ident>>>,
pub options: Option<Vec<SqlOption>>,
}

View file

@ -6338,6 +6338,117 @@ impl Display for TableEngine {
}
}
/// Snowflake `WITH ROW ACCESS POLICY policy_name ON (identifier, ...)`
///
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
/// <https://docs.snowflake.com/en/user-guide/security-row-intro>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct RowAccessPolicy {
pub policy: ObjectName,
pub on: Vec<Ident>,
}
impl RowAccessPolicy {
pub fn new(policy: ObjectName, on: Vec<Ident>) -> Self {
Self { policy, on }
}
}
impl Display for RowAccessPolicy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"WITH ROW ACCESS POLICY {} ON ({})",
self.policy,
display_comma_separated(self.on.as_slice())
)
}
}
/// Snowflake `WITH TAG ( tag_name = '<tag_value>', ...)`
///
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct Tag {
pub key: Ident,
pub value: String,
}
impl Tag {
pub fn new(key: Ident, value: String) -> Self {
Self { key, value }
}
}
impl Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}='{}'", self.key, self.value)
}
}
/// Helper to indicate if a comment includes the `=` in the display form
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum CommentDef {
/// Includes `=` when printing the comment, as `COMMENT = 'comment'`
/// Does not include `=` when printing the comment, as `COMMENT 'comment'`
WithEq(String),
WithoutEq(String),
}
impl Display for CommentDef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"),
}
}
}
/// Helper to indicate if a collection should be wrapped by a symbol in the display form
///
/// [`Display`] is implemented for every [`Vec<T>`] where `T: Display`.
/// The string output is a comma separated list for the vec items
///
/// # Examples
/// ```
/// # use sqlparser::ast::WrappedCollection;
/// let items = WrappedCollection::Parentheses(vec!["one", "two", "three"]);
/// assert_eq!("(one, two, three)", items.to_string());
///
/// let items = WrappedCollection::NoWrapping(vec!["one", "two", "three"]);
/// assert_eq!("one, two, three", items.to_string());
/// ```
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum WrappedCollection<T> {
/// Print the collection without wrapping symbols, as `item, item, item`
NoWrapping(T),
/// Wraps the collection in Parentheses, as `(item, item, item)`
Parentheses(T),
}
impl<T> Display for WrappedCollection<Vec<T>>
where
T: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WrappedCollection::NoWrapping(inner) => {
write!(f, "{}", display_comma_separated(inner.as_slice()))
}
WrappedCollection::Parentheses(inner) => {
write!(f, "({})", display_comma_separated(inner.as_slice()))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -12,11 +12,14 @@
#[cfg(not(feature = "std"))]
use crate::alloc::string::ToString;
use crate::ast::helpers::stmt_create_table::CreateTableBuilder;
use crate::ast::helpers::stmt_data_loading::{
DataLoadingOption, DataLoadingOptionType, DataLoadingOptions, StageLoadSelectItem,
StageParamsObject,
};
use crate::ast::{Ident, ObjectName, Statement};
use crate::ast::{
CommentDef, Ident, ObjectName, RowAccessPolicy, Statement, Tag, WrappedCollection,
};
use crate::dialect::Dialect;
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
@ -91,12 +94,36 @@ impl Dialect for SnowflakeDialect {
// possibly CREATE STAGE
//[ OR REPLACE ]
let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
//[ TEMPORARY ]
let temporary = parser.parse_keyword(Keyword::TEMPORARY);
// LOCAL | GLOBAL
let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) {
Some(Keyword::LOCAL) => Some(false),
Some(Keyword::GLOBAL) => Some(true),
_ => None,
};
let mut temporary = false;
let mut volatile = false;
let mut transient = false;
match parser.parse_one_of_keywords(&[
Keyword::TEMP,
Keyword::TEMPORARY,
Keyword::VOLATILE,
Keyword::TRANSIENT,
]) {
Some(Keyword::TEMP | Keyword::TEMPORARY) => temporary = true,
Some(Keyword::VOLATILE) => volatile = true,
Some(Keyword::TRANSIENT) => transient = true,
_ => {}
}
if parser.parse_keyword(Keyword::STAGE) {
// OK - this is CREATE STAGE statement
return Some(parse_create_stage(or_replace, temporary, parser));
} else if parser.parse_keyword(Keyword::TABLE) {
return Some(parse_create_table(
or_replace, global, temporary, volatile, transient, parser,
));
} else {
// need to go back with the cursor
let mut back = 1;
@ -120,6 +147,196 @@ impl Dialect for SnowflakeDialect {
}
}
/// Parse snowflake create table statement.
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub fn parse_create_table(
or_replace: bool,
global: Option<bool>,
temporary: bool,
volatile: bool,
transient: bool,
parser: &mut Parser,
) -> Result<Statement, ParserError> {
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let table_name = parser.parse_object_name(false)?;
let mut builder = CreateTableBuilder::new(table_name)
.or_replace(or_replace)
.if_not_exists(if_not_exists)
.temporary(temporary)
.transient(transient)
.volatile(volatile)
.global(global)
.hive_formats(Some(Default::default()));
// Snowflake does not enforce order of the parameters in the statement. The parser needs to
// parse the statement in a loop.
//
// "CREATE TABLE x COPY GRANTS (c INT)" and "CREATE TABLE x (c INT) COPY GRANTS" are both
// accepted by Snowflake
loop {
let next_token = parser.next_token();
match &next_token.token {
Token::Word(word) => match word.keyword {
Keyword::COPY => {
parser.expect_keyword(Keyword::GRANTS)?;
builder = builder.copy_grants(true);
}
Keyword::COMMENT => {
parser.expect_token(&Token::Eq)?;
let next_token = parser.next_token();
let comment = match next_token.token {
Token::SingleQuotedString(str) => Some(CommentDef::WithEq(str)),
_ => parser.expected("comment", next_token)?,
};
builder = builder.comment(comment);
}
Keyword::AS => {
let query = parser.parse_boxed_query()?;
builder = builder.query(Some(query));
break;
}
Keyword::CLONE => {
let clone = parser.parse_object_name(false).ok();
builder = builder.clone_clause(clone);
break;
}
Keyword::LIKE => {
let like = parser.parse_object_name(false).ok();
builder = builder.like(like);
break;
}
Keyword::CLUSTER => {
parser.expect_keyword(Keyword::BY)?;
parser.expect_token(&Token::LParen)?;
let cluster_by = Some(WrappedCollection::Parentheses(
parser.parse_comma_separated(|p| p.parse_identifier(false))?,
));
parser.expect_token(&Token::RParen)?;
builder = builder.cluster_by(cluster_by)
}
Keyword::ENABLE_SCHEMA_EVOLUTION => {
parser.expect_token(&Token::Eq)?;
let enable_schema_evolution =
match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) {
Some(Keyword::TRUE) => true,
Some(Keyword::FALSE) => false,
_ => {
return parser.expected("TRUE or FALSE", next_token);
}
};
builder = builder.enable_schema_evolution(Some(enable_schema_evolution));
}
Keyword::CHANGE_TRACKING => {
parser.expect_token(&Token::Eq)?;
let change_tracking =
match parser.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]) {
Some(Keyword::TRUE) => true,
Some(Keyword::FALSE) => false,
_ => {
return parser.expected("TRUE or FALSE", next_token);
}
};
builder = builder.change_tracking(Some(change_tracking));
}
Keyword::DATA_RETENTION_TIME_IN_DAYS => {
parser.expect_token(&Token::Eq)?;
let data_retention_time_in_days = parser.parse_literal_uint()?;
builder =
builder.data_retention_time_in_days(Some(data_retention_time_in_days));
}
Keyword::MAX_DATA_EXTENSION_TIME_IN_DAYS => {
parser.expect_token(&Token::Eq)?;
let max_data_extension_time_in_days = parser.parse_literal_uint()?;
builder = builder
.max_data_extension_time_in_days(Some(max_data_extension_time_in_days));
}
Keyword::DEFAULT_DDL_COLLATION => {
parser.expect_token(&Token::Eq)?;
let default_ddl_collation = parser.parse_literal_string()?;
builder = builder.default_ddl_collation(Some(default_ddl_collation));
}
// WITH is optional, we just verify that next token is one of the expected ones and
// fallback to the default match statement
Keyword::WITH => {
parser.expect_one_of_keywords(&[
Keyword::AGGREGATION,
Keyword::TAG,
Keyword::ROW,
])?;
parser.prev_token();
}
Keyword::AGGREGATION => {
parser.expect_keyword(Keyword::POLICY)?;
let aggregation_policy = parser.parse_object_name(false)?;
builder = builder.with_aggregation_policy(Some(aggregation_policy));
}
Keyword::ROW => {
parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?;
let policy = parser.parse_object_name(false)?;
parser.expect_keyword(Keyword::ON)?;
parser.expect_token(&Token::LParen)?;
let columns = parser.parse_comma_separated(|p| p.parse_identifier(false))?;
parser.expect_token(&Token::RParen)?;
builder =
builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, columns)))
}
Keyword::TAG => {
fn parse_tag(parser: &mut Parser) -> Result<Tag, ParserError> {
let name = parser.parse_identifier(false)?;
parser.expect_token(&Token::Eq)?;
let value = parser.parse_literal_string()?;
Ok(Tag::new(name, value))
}
parser.expect_token(&Token::LParen)?;
let tags = parser.parse_comma_separated(parse_tag)?;
parser.expect_token(&Token::RParen)?;
builder = builder.with_tags(Some(tags));
}
_ => {
return parser.expected("end of statement", next_token);
}
},
Token::LParen => {
parser.prev_token();
let (columns, constraints) = parser.parse_columns()?;
builder = builder.columns(columns).constraints(constraints);
}
Token::EOF => {
if builder.columns.is_empty() {
return Err(ParserError::ParserError(
"unexpected end of input".to_string(),
));
}
break;
}
Token::SemiColon => {
if builder.columns.is_empty() {
return Err(ParserError::ParserError(
"unexpected end of input".to_string(),
));
}
parser.prev_token();
break;
}
_ => {
return parser.expected("end of statement", next_token);
}
}
}
Ok(builder.build())
}
pub fn parse_create_stage(
or_replace: bool,
temporary: bool,

View file

@ -70,11 +70,13 @@ define_keywords!(
ABORT,
ABS,
ABSOLUTE,
ACCESS,
ACTION,
ADD,
ADMIN,
AFTER,
AGAINST,
AGGREGATION,
ALL,
ALLOCATE,
ALTER,
@ -138,6 +140,7 @@ define_keywords!(
CENTURY,
CHAIN,
CHANGE,
CHANGE_TRACKING,
CHANNEL,
CHAR,
CHARACTER,
@ -201,6 +204,7 @@ define_keywords!(
CYCLE,
DATA,
DATABASE,
DATA_RETENTION_TIME_IN_DAYS,
DATE,
DATE32,
DATETIME,
@ -214,6 +218,7 @@ define_keywords!(
DECIMAL,
DECLARE,
DEFAULT,
DEFAULT_DDL_COLLATION,
DEFERRABLE,
DEFERRED,
DEFINE,
@ -251,6 +256,7 @@ define_keywords!(
ELSE,
EMPTY,
ENABLE,
ENABLE_SCHEMA_EVOLUTION,
ENCODING,
ENCRYPTION,
END,
@ -330,6 +336,7 @@ define_keywords!(
GLOBAL,
GRANT,
GRANTED,
GRANTS,
GRAPHVIZ,
GROUP,
GROUPING,
@ -433,6 +440,7 @@ define_keywords!(
MATERIALIZED,
MAX,
MAXVALUE,
MAX_DATA_EXTENSION_TIME_IN_DAYS,
MEASURES,
MEDIUMINT,
MEMBER,
@ -539,6 +547,7 @@ define_keywords!(
PIVOT,
PLACING,
PLANS,
POLICY,
PORTION,
POSITION,
POSITION_REGEX,
@ -690,6 +699,7 @@ define_keywords!(
TABLE,
TABLES,
TABLESAMPLE,
TAG,
TARGET,
TBLPROPERTIES,
TEMP,

View file

@ -5372,7 +5372,7 @@ impl<'a> Parser<'a> {
let _ = self.consume_token(&Token::Eq);
let next_token = self.next_token();
match next_token.token {
Token::SingleQuotedString(str) => Some(str),
Token::SingleQuotedString(str) => Some(CommentDef::WithoutEq(str)),
_ => self.expected("comment", next_token)?,
}
} else {
@ -5423,7 +5423,9 @@ impl<'a> Parser<'a> {
let mut cluster_by = None;
if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) {
cluster_by = Some(self.parse_comma_separated(|p| p.parse_identifier(false))?);
cluster_by = Some(WrappedCollection::NoWrapping(
self.parse_comma_separated(|p| p.parse_identifier(false))?,
));
};
let mut options = None;
@ -7783,7 +7785,7 @@ impl<'a> Parser<'a> {
/// This function can be used to reduce the stack size required in debug
/// builds. Instead of `sizeof(Query)` only a pointer (`Box<Query>`)
/// is used.
fn parse_boxed_query(&mut self) -> Result<Box<Query>, ParserError> {
pub fn parse_boxed_query(&mut self) -> Result<Box<Query>, ParserError> {
self.parse_query().map(Box::new)
}

View file

@ -442,7 +442,10 @@ fn parse_create_table_with_options() {
assert_eq!(
(
Some(Box::new(Expr::Identifier(Ident::new("_PARTITIONDATE")))),
Some(vec![Ident::new("userid"), Ident::new("age"),]),
Some(WrappedCollection::NoWrapping(vec![
Ident::new("userid"),
Ident::new("age"),
])),
Some(vec![
SqlOption {
name: Ident::new("partition_expiration_days"),

View file

@ -3453,9 +3453,14 @@ fn parse_create_table_as_table() {
#[test]
fn parse_create_table_on_cluster() {
let generic = TestedDialects {
dialects: vec![Box::new(GenericDialect {})],
options: None,
};
// Using single-quote literal to define current cluster
let sql = "CREATE TABLE t ON CLUSTER '{cluster}' (a INT, b INT)";
match verified_stmt(sql) {
match generic.verified_stmt(sql) {
Statement::CreateTable(CreateTable { on_cluster, .. }) => {
assert_eq!(on_cluster.unwrap(), "{cluster}".to_string());
}
@ -3464,7 +3469,7 @@ fn parse_create_table_on_cluster() {
// Using explicitly declared cluster name
let sql = "CREATE TABLE t ON CLUSTER my_cluster (a INT, b INT)";
match verified_stmt(sql) {
match generic.verified_stmt(sql) {
Statement::CreateTable(CreateTable { on_cluster, .. }) => {
assert_eq!(on_cluster.unwrap(), "my_cluster".to_string());
}
@ -3517,8 +3522,13 @@ fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), Par
#[test]
fn parse_create_table_with_options() {
let generic = TestedDialects {
dialects: vec![Box::new(GenericDialect {})],
options: None,
};
let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)";
match verified_stmt(sql) {
match generic.verified_stmt(sql) {
Statement::CreateTable(CreateTable { with_options, .. }) => {
assert_eq!(
vec![

View file

@ -4136,3 +4136,26 @@ fn parse_at_time_zone() {
expr
);
}
#[test]
fn parse_create_table_with_options() {
let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)";
match pg().verified_stmt(sql) {
Statement::CreateTable(CreateTable { with_options, .. }) => {
assert_eq!(
vec![
SqlOption {
name: "foo".into(),
value: Expr::Value(Value::SingleQuotedString("bar".into())),
},
SqlOption {
name: "a".into(),
value: Expr::Value(number("123")),
},
],
with_options
);
}
_ => unreachable!(),
}
}

View file

@ -40,6 +40,279 @@ fn test_snowflake_create_table() {
}
}
#[test]
fn test_snowflake_create_or_replace_table() {
let sql = "CREATE OR REPLACE TABLE my_table (a number)";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name, or_replace, ..
}) => {
assert_eq!("my_table", name.to_string());
assert!(or_replace);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_or_replace_table_copy_grants() {
let sql = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
or_replace,
copy_grants,
..
}) => {
assert_eq!("my_table", name.to_string());
assert!(or_replace);
assert!(copy_grants);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_or_replace_table_copy_grants_at_end() {
let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS (a number) ";
let parsed = "CREATE OR REPLACE TABLE my_table (a number) COPY GRANTS";
match snowflake().one_statement_parses_to(sql, parsed) {
Statement::CreateTable(CreateTable {
name,
or_replace,
copy_grants,
..
}) => {
assert_eq!("my_table", name.to_string());
assert!(or_replace);
assert!(copy_grants);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_or_replace_table_copy_grants_cta() {
let sql = "CREATE OR REPLACE TABLE my_table COPY GRANTS AS SELECT 1 AS a";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
or_replace,
copy_grants,
..
}) => {
assert_eq!("my_table", name.to_string());
assert!(or_replace);
assert!(copy_grants);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_enable_schema_evolution() {
let sql = "CREATE TABLE my_table (a number) ENABLE_SCHEMA_EVOLUTION=TRUE";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
enable_schema_evolution,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(true), enable_schema_evolution);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_change_tracking() {
let sql = "CREATE TABLE my_table (a number) CHANGE_TRACKING=TRUE";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
change_tracking,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(true), change_tracking);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_data_retention_time_in_days() {
let sql = "CREATE TABLE my_table (a number) DATA_RETENTION_TIME_IN_DAYS=5";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
data_retention_time_in_days,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(5), data_retention_time_in_days);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_max_data_extension_time_in_days() {
let sql = "CREATE TABLE my_table (a number) MAX_DATA_EXTENSION_TIME_IN_DAYS=5";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
max_data_extension_time_in_days,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(5), max_data_extension_time_in_days);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_with_aggregation_policy() {
match snowflake()
.verified_stmt("CREATE TABLE my_table (a number) WITH AGGREGATION POLICY policy_name")
{
Statement::CreateTable(CreateTable {
name,
with_aggregation_policy,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some("policy_name".to_string()),
with_aggregation_policy.map(|name| name.to_string())
);
}
_ => unreachable!(),
}
match snowflake()
.parse_sql_statements("CREATE TABLE my_table (a number) AGGREGATION POLICY policy_name")
.unwrap()
.pop()
.unwrap()
{
Statement::CreateTable(CreateTable {
name,
with_aggregation_policy,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some("policy_name".to_string()),
with_aggregation_policy.map(|name| name.to_string())
);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_with_row_access_policy() {
match snowflake().verified_stmt(
"CREATE TABLE my_table (a number, b number) WITH ROW ACCESS POLICY policy_name ON (a)",
) {
Statement::CreateTable(CreateTable {
name,
with_row_access_policy,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()),
with_row_access_policy.map(|policy| policy.to_string())
);
}
_ => unreachable!(),
}
match snowflake()
.parse_sql_statements(
"CREATE TABLE my_table (a number, b number) ROW ACCESS POLICY policy_name ON (a)",
)
.unwrap()
.pop()
.unwrap()
{
Statement::CreateTable(CreateTable {
name,
with_row_access_policy,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some("WITH ROW ACCESS POLICY policy_name ON (a)".to_string()),
with_row_access_policy.map(|policy| policy.to_string())
);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_with_tag() {
match snowflake()
.verified_stmt("CREATE TABLE my_table (a number) WITH TAG (A='TAG A', B='TAG B')")
{
Statement::CreateTable(CreateTable {
name, with_tags, ..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some(vec![
Tag::new("A".into(), "TAG A".to_string()),
Tag::new("B".into(), "TAG B".to_string())
]),
with_tags
);
}
_ => unreachable!(),
}
match snowflake()
.parse_sql_statements("CREATE TABLE my_table (a number) TAG (A='TAG A', B='TAG B')")
.unwrap()
.pop()
.unwrap()
{
Statement::CreateTable(CreateTable {
name, with_tags, ..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some(vec![
Tag::new("A".into(), "TAG A".to_string()),
Tag::new("B".into(), "TAG B".to_string())
]),
with_tags
);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_default_ddl_collation() {
let sql = "CREATE TABLE my_table (a number) DEFAULT_DDL_COLLATION='de'";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
default_ddl_collation,
..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some("de".to_string()), default_ddl_collation);
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_transient_table() {
let sql = "CREATE TRANSIENT TABLE CUSTOMER (id INT, name VARCHAR(255))";
@ -54,6 +327,162 @@ fn test_snowflake_create_transient_table() {
}
}
#[test]
fn test_snowflake_create_table_column_comment() {
let sql = "CREATE TABLE my_table (a STRING COMMENT 'some comment')";
match snowflake().verified_stmt(sql) {
Statement::CreateTable(CreateTable { name, columns, .. }) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
vec![ColumnDef {
name: "a".into(),
data_type: DataType::String(None),
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Comment("some comment".to_string())
}],
collation: None
}],
columns
)
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_local_table() {
match snowflake().verified_stmt("CREATE TABLE my_table (a INT)") {
Statement::CreateTable(CreateTable { name, global, .. }) => {
assert_eq!("my_table", name.to_string());
assert!(global.is_none())
}
_ => unreachable!(),
}
match snowflake().verified_stmt("CREATE LOCAL TABLE my_table (a INT)") {
Statement::CreateTable(CreateTable { name, global, .. }) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(false), global)
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_global_table() {
match snowflake().verified_stmt("CREATE GLOBAL TABLE my_table (a INT)") {
Statement::CreateTable(CreateTable { name, global, .. }) => {
assert_eq!("my_table", name.to_string());
assert_eq!(Some(true), global)
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_invalid_local_global_table() {
assert_eq!(
snowflake().parse_sql_statements("CREATE LOCAL GLOBAL TABLE my_table (a INT)"),
Err(ParserError::ParserError(
"Expected an SQL statement, found: LOCAL".to_string()
))
);
assert_eq!(
snowflake().parse_sql_statements("CREATE GLOBAL LOCAL TABLE my_table (a INT)"),
Err(ParserError::ParserError(
"Expected an SQL statement, found: GLOBAL".to_string()
))
);
}
#[test]
fn test_snowflake_create_invalid_temporal_table() {
assert_eq!(
snowflake().parse_sql_statements("CREATE TEMP TEMPORARY TABLE my_table (a INT)"),
Err(ParserError::ParserError(
"Expected an object type after CREATE, found: TEMPORARY".to_string()
))
);
assert_eq!(
snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"),
Err(ParserError::ParserError(
"Expected an object type after CREATE, found: VOLATILE".to_string()
))
);
assert_eq!(
snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"),
Err(ParserError::ParserError(
"Expected an object type after CREATE, found: TRANSIENT".to_string()
))
);
}
#[test]
fn test_snowflake_create_table_if_not_exists() {
match snowflake().verified_stmt("CREATE TABLE IF NOT EXISTS my_table (a INT)") {
Statement::CreateTable(CreateTable {
name,
if_not_exists,
..
}) => {
assert_eq!("my_table", name.to_string());
assert!(if_not_exists)
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_cluster_by() {
match snowflake().verified_stmt("CREATE TABLE my_table (a INT) CLUSTER BY (a, b)") {
Statement::CreateTable(CreateTable {
name, cluster_by, ..
}) => {
assert_eq!("my_table", name.to_string());
assert_eq!(
Some(WrappedCollection::Parentheses(vec![
Ident::new("a"),
Ident::new("b"),
])),
cluster_by
)
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_comment() {
match snowflake().verified_stmt("CREATE TABLE my_table (a INT) COMMENT = 'some comment'") {
Statement::CreateTable(CreateTable { name, comment, .. }) => {
assert_eq!("my_table", name.to_string());
assert_eq!("some comment", comment.unwrap().to_string());
}
_ => unreachable!(),
}
}
#[test]
fn test_snowflake_create_table_incomplete_statement() {
assert_eq!(
snowflake().parse_sql_statements("CREATE TABLE my_table"),
Err(ParserError::ParserError(
"unexpected end of input".to_string()
))
);
assert_eq!(
snowflake().parse_sql_statements("CREATE TABLE my_table; (c int)"),
Err(ParserError::ParserError(
"unexpected end of input".to_string()
))
);
}
#[test]
fn test_snowflake_single_line_tokenize() {
let sql = "CREATE TABLE# this is a comment \ntable_1";