Skip to content

Commit

Permalink
feat: Add support for T-SQL table options
Browse files Browse the repository at this point in the history
  • Loading branch information
bombsimon committed Sep 6, 2024
1 parent 1bed87a commit 978517f
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 20 deletions.
6 changes: 3 additions & 3 deletions src/ast/dml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use super::{
display_comma_separated, display_separated, ClusteredBy, 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,
OrderByExpr, Query, RowAccessPolicy, SelectItem, SqlOption, SqlWithOption, SqliteOnConflict,
TableEngine, TableWithJoins, Tag, WrappedCollection,
};

/// CREATE INDEX statement.
Expand Down Expand Up @@ -116,7 +116,7 @@ pub struct CreateTable {
pub hive_distribution: HiveDistributionStyle,
pub hive_formats: Option<HiveFormat>,
pub table_properties: Vec<SqlOption>,
pub with_options: Vec<SqlOption>,
pub with_options: Vec<SqlWithOption>,
pub file_format: Option<FileFormat>,
pub location: Option<String>,
pub query: Option<Box<Query>>,
Expand Down
8 changes: 4 additions & 4 deletions src/ast/helpers/stmt_create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use sqlparser_derive::{Visit, VisitMut};
use super::super::dml::CreateTable;
use crate::ast::{
ClusteredBy, ColumnDef, CommentDef, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident,
ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, Statement,
TableConstraint, TableEngine, Tag, WrappedCollection,
ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, SqlOption, SqlWithOption,
Statement, TableConstraint, TableEngine, Tag, WrappedCollection,
};
use crate::parser::ParserError;

Expand Down Expand Up @@ -60,7 +60,7 @@ pub struct CreateTableBuilder {
pub hive_distribution: HiveDistributionStyle,
pub hive_formats: Option<HiveFormat>,
pub table_properties: Vec<SqlOption>,
pub with_options: Vec<SqlOption>,
pub with_options: Vec<SqlWithOption>,
pub file_format: Option<FileFormat>,
pub location: Option<String>,
pub query: Option<Box<Query>>,
Expand Down Expand Up @@ -200,7 +200,7 @@ impl CreateTableBuilder {
self
}

pub fn with_options(mut self, with_options: Vec<SqlOption>) -> Self {
pub fn with_options(mut self, with_options: Vec<SqlWithOption>) -> Self {
self.with_options = with_options;
self
}
Expand Down
126 changes: 125 additions & 1 deletion src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1943,7 +1943,16 @@ pub enum CreateTableOptions {
/// e.g. `WITH (description = "123")`
///
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
With(Vec<SqlOption>),
///
/// T-sql supports more specific options that's not only key-value pairs.
///
/// WITH (
/// DISTRIBUTION = ROUND_ROBIN,
/// CLUSTERED INDEX (column_a DESC, column_b)
/// )
///
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#syntax>
With(Vec<SqlWithOption>),
/// Options specified using the `OPTIONS` keyword.
/// e.g. `OPTIONS(description = "123")`
///
Expand Down Expand Up @@ -5586,6 +5595,121 @@ pub struct HiveFormat {
pub location: Option<String>,
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ClusteredIndex {
pub name: Ident,
pub asc: Option<bool>,
}

impl fmt::Display for ClusteredIndex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)?;
match self.asc {
Some(true) => write!(f, " ASC"),
Some(false) => write!(f, " DESC"),
_ => Ok(()),
}
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum TableOptionsClustered {
ColumnstoreIndex,
ColumnstoreIndexOrder(Vec<Ident>),
Index(Vec<ClusteredIndex>),
}

impl fmt::Display for TableOptionsClustered {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TableOptionsClustered::ColumnstoreIndex => {
write!(f, "CLUSTERED COLUMNSTORE INDEX")
}
TableOptionsClustered::ColumnstoreIndexOrder(values) => {
write!(
f,
"CLUSTERED COLUMNSTORE INDEX ORDER ({})",
display_comma_separated(values)
)
}
TableOptionsClustered::Index(values) => {
write!(f, "CLUSTERED INDEX ({})", display_comma_separated(values))
}
}
}
}

/// Specifies which partition the boundary values on table partitioning belongs to.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum PartitionRangeDirection {
Left,
Right,
}

/// [`SqlWithOption`] are Sql options that can exist within a `WITH()` expression, e.g.
/// `WITH(description = 123)`. Some dialects support more other options such as how the table is
/// stored or how it's partitioned.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum SqlWithOption {
/// Clustered represents the clustered version of table storage for T-sql.
///
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TableOptions>
Clustered(TableOptionsClustered),
/// Single identifier options, e.g. `HEAP`.
Ident(Ident),
/// Any option that consists of a key value pair where the value is an expression.
KeyValue { name: Ident, value: Expr },
/// One or more table partitions and represents which partition the boundary values belong to.
///
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TablePartitionOptions>
Partition {
column_name: Ident,
range_direction: Option<PartitionRangeDirection>,
for_values: Vec<Expr>,
},
}

impl fmt::Display for SqlWithOption {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SqlWithOption::Clustered(c) => write!(f, "{}", c),
SqlWithOption::Ident(ident) => {
write!(f, "{}", ident)
}
SqlWithOption::KeyValue { name, value } => {
write!(f, "{} = {}", name, value)
}
SqlWithOption::Partition {
column_name,
range_direction,
for_values,
} => {
let direction = match range_direction {
Some(PartitionRangeDirection::Left) => " LEFT",
Some(PartitionRangeDirection::Right) => " RIGHT",
None => "",
};

write!(
f,
"PARTITION ({} RANGE{} FOR VALUES ({}))",
column_name,
direction,
display_comma_separated(for_values)
)
}
}
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ define_keywords!(
COLLECTION,
COLUMN,
COLUMNS,
COLUMNSTORE,
COMMENT,
COMMIT,
COMMITTED,
Expand Down Expand Up @@ -354,6 +355,7 @@ define_keywords!(
HASH,
HAVING,
HEADER,
HEAP,
HIGH_PRIORITY,
HISTORY,
HIVEVAR,
Expand Down
118 changes: 115 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4458,7 +4458,7 @@ impl<'a> Parser<'a> {
let name = self.parse_object_name(allow_unquoted_hyphen)?;
let columns = self.parse_view_columns()?;
let mut options = CreateTableOptions::None;
let with_options = self.parse_options(Keyword::WITH)?;
let with_options = self.parse_table_options(Keyword::WITH)?;
if !with_options.is_empty() {
options = CreateTableOptions::With(with_options);
}
Expand Down Expand Up @@ -5621,7 +5621,8 @@ impl<'a> Parser<'a> {
let clustered_by = self.parse_optional_clustered_by()?;
let hive_formats = self.parse_hive_formats()?;
// PostgreSQL supports `WITH ( options )`, before `AS`
let with_options = self.parse_options(Keyword::WITH)?;
// T-sql supports `WITH` options for clustering and distribution
let with_options = self.parse_table_options(Keyword::WITH)?;
let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?;

let engine = if self.parse_keyword(Keyword::ENGINE) {
Expand Down Expand Up @@ -6399,6 +6400,20 @@ impl<'a> Parser<'a> {
Ok(None)
}

pub fn parse_table_options(
&mut self,
keyword: Keyword,
) -> Result<Vec<SqlWithOption>, ParserError> {
if self.parse_keyword(keyword) {
self.expect_token(&Token::LParen)?;
let options = self.parse_comma_separated(Parser::parse_sql_with_option)?;
self.expect_token(&Token::RParen)?;
Ok(options)
} else {
Ok(vec![])
}
}

pub fn parse_options(&mut self, keyword: Keyword) -> Result<Vec<SqlOption>, ParserError> {
if self.parse_keyword(keyword) {
self.expect_token(&Token::LParen)?;
Expand Down Expand Up @@ -6484,13 +6499,110 @@ impl<'a> Parser<'a> {
}
}

pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
pub fn parse_key_value(&mut self) -> Result<(Ident, Expr), ParserError> {
let name = self.parse_identifier(false)?;
self.expect_token(&Token::Eq)?;
let value = self.parse_expr()?;

Ok((name, value))
}

pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
let (name, value) = self.parse_key_value()?;
Ok(SqlOption { name, value })
}

pub fn parse_table_option_clustered(&mut self) -> Result<SqlWithOption, ParserError> {
self.expect_keyword(Keyword::CLUSTERED)?;

if self.parse_keywords(&[Keyword::COLUMNSTORE, Keyword::INDEX]) {
if self.parse_keyword(Keyword::ORDER) {
Ok(SqlWithOption::Clustered(
TableOptionsClustered::ColumnstoreIndexOrder(
self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?,
),
))
} else {
Ok(SqlWithOption::Clustered(
TableOptionsClustered::ColumnstoreIndex,
))
}
} else {
self.expect_keyword(Keyword::INDEX)?;
self.expect_token(&Token::LParen)?;

let columns = self.parse_comma_separated(|p| {
let name = p.parse_identifier(false)?;
let asc = if p.parse_keyword(Keyword::ASC) {
Some(true)
} else if p.parse_keyword(Keyword::DESC) {
Some(false)
} else {
None
};

Ok(ClusteredIndex { name, asc })
})?;

self.expect_token(&Token::RParen)?;

Ok(SqlWithOption::Clustered(TableOptionsClustered::Index(
columns,
)))
}
}

pub fn parse_table_option_partition(&mut self) -> Result<SqlWithOption, ParserError> {
self.expect_keyword(Keyword::PARTITION)?;
self.expect_token(&Token::LParen)?;
let column_name = self.parse_identifier(false)?;

self.expect_keyword(Keyword::RANGE)?;
let range_direction = if self.parse_keyword(Keyword::LEFT) {
Some(PartitionRangeDirection::Left)
} else if self.parse_keyword(Keyword::RIGHT) {
Some(PartitionRangeDirection::Right)
} else {
None
};

self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?;
self.expect_token(&Token::LParen)?;

let for_values = self.parse_comma_separated(Parser::parse_expr)?;

self.expect_token(&Token::RParen)?;
self.expect_token(&Token::RParen)?;

Ok(SqlWithOption::Partition {
column_name,
range_direction,
for_values,
})
}

pub fn parse_sql_with_option(&mut self) -> Result<SqlWithOption, ParserError> {
let next_token = self.peek_token();
let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect);

let Token::Word(w) = next_token.token else {
return self.expected(
"expected sql with option to be a keyword",
self.peek_token(),
);
};

match w.keyword {
Keyword::HEAP if is_mssql => Ok(SqlWithOption::Ident(self.parse_identifier(false)?)),
Keyword::PARTITION if is_mssql => self.parse_table_option_partition(),
Keyword::CLUSTERED if is_mssql => self.parse_table_option_clustered(),
_ => {
let (name, value) = self.parse_key_value()?;
Ok(SqlWithOption::KeyValue { name, value })
}
}
}

pub fn parse_partition(&mut self) -> Result<Partition, ParserError> {
self.expect_token(&Token::LParen)?;
let partitions = self.parse_comma_separated(Parser::parse_expr)?;
Expand Down
8 changes: 4 additions & 4 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3637,11 +3637,11 @@ fn parse_create_table_with_options() {
Statement::CreateTable(CreateTable { with_options, .. }) => {
assert_eq!(
vec![
SqlOption {
SqlWithOption::KeyValue {
name: "foo".into(),
value: Expr::Value(Value::SingleQuotedString("bar".into())),
},
SqlOption {
SqlWithOption::KeyValue {
name: "a".into(),
value: Expr::Value(number("123")),
},
Expand Down Expand Up @@ -6530,11 +6530,11 @@ fn parse_create_view_with_options() {
Statement::CreateView { options, .. } => {
assert_eq!(
CreateTableOptions::With(vec![
SqlOption {
SqlWithOption::KeyValue {
name: "foo".into(),
value: Expr::Value(Value::SingleQuotedString("bar".into())),
},
SqlOption {
SqlWithOption::KeyValue {
name: "a".into(),
value: Expr::Value(number("123")),
},
Expand Down
Loading

0 comments on commit 978517f

Please sign in to comment.