Skip to content

Commit 7612a1e

Browse files
committed
feat: Add support for T-SQL table options
1 parent 1bed87a commit 7612a1e

File tree

7 files changed

+425
-33
lines changed

7 files changed

+425
-33
lines changed

src/ast/mod.rs

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,15 @@ pub enum CreateTableOptions {
19431943
/// e.g. `WITH (description = "123")`
19441944
///
19451945
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
1946+
///
1947+
/// T-sql supports more specific options that's not only key-value pairs.
1948+
///
1949+
/// WITH (
1950+
/// DISTRIBUTION = ROUND_ROBIN,
1951+
/// CLUSTERED INDEX (column_a DESC, column_b)
1952+
/// )
1953+
///
1954+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#syntax>
19461955
With(Vec<SqlOption>),
19471956
/// Options specified using the `OPTIONS` keyword.
19481957
/// e.g. `OPTIONS(description = "123")`
@@ -5589,14 +5598,112 @@ pub struct HiveFormat {
55895598
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
55905599
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55915600
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5592-
pub struct SqlOption {
5601+
pub struct ClusteredIndex {
55935602
pub name: Ident,
5594-
pub value: Expr,
5603+
pub asc: Option<bool>,
5604+
}
5605+
5606+
impl fmt::Display for ClusteredIndex {
5607+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5608+
write!(f, "{}", self.name)?;
5609+
match self.asc {
5610+
Some(true) => write!(f, " ASC"),
5611+
Some(false) => write!(f, " DESC"),
5612+
_ => Ok(()),
5613+
}
5614+
}
5615+
}
5616+
5617+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5618+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5619+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5620+
pub enum TableOptionsClustered {
5621+
ColumnstoreIndex,
5622+
ColumnstoreIndexOrder(Vec<Ident>),
5623+
Index(Vec<ClusteredIndex>),
5624+
}
5625+
5626+
impl fmt::Display for TableOptionsClustered {
5627+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5628+
match self {
5629+
TableOptionsClustered::ColumnstoreIndex => {
5630+
write!(f, "CLUSTERED COLUMNSTORE INDEX")
5631+
}
5632+
TableOptionsClustered::ColumnstoreIndexOrder(values) => {
5633+
write!(
5634+
f,
5635+
"CLUSTERED COLUMNSTORE INDEX ORDER ({})",
5636+
display_comma_separated(values)
5637+
)
5638+
}
5639+
TableOptionsClustered::Index(values) => {
5640+
write!(f, "CLUSTERED INDEX ({})", display_comma_separated(values))
5641+
}
5642+
}
5643+
}
5644+
}
5645+
5646+
/// Specifies which partition the boundary values on table partitioning belongs to.
5647+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5648+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5649+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5650+
pub enum PartitionRangeDirection {
5651+
Left,
5652+
Right,
5653+
}
5654+
5655+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5656+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5657+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5658+
pub enum SqlOption {
5659+
/// Clustered represents the clustered version of table storage for T-sql.
5660+
///
5661+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TableOptions>
5662+
Clustered(TableOptionsClustered),
5663+
/// Single identifier options, e.g. `HEAP`.
5664+
Ident(Ident),
5665+
/// Any option that consists of a key value pair where the value is an expression.
5666+
KeyValue { name: Ident, value: Expr },
5667+
/// One or more table partitions and represents which partition the boundary values belong to.
5668+
///
5669+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TablePartitionOptions>
5670+
Partition {
5671+
column_name: Ident,
5672+
range_direction: Option<PartitionRangeDirection>,
5673+
for_values: Vec<Expr>,
5674+
},
55955675
}
55965676

55975677
impl fmt::Display for SqlOption {
55985678
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5599-
write!(f, "{} = {}", self.name, self.value)
5679+
match self {
5680+
SqlOption::Clustered(c) => write!(f, "{}", c),
5681+
SqlOption::Ident(ident) => {
5682+
write!(f, "{}", ident)
5683+
}
5684+
SqlOption::KeyValue { name, value } => {
5685+
write!(f, "{} = {}", name, value)
5686+
}
5687+
SqlOption::Partition {
5688+
column_name,
5689+
range_direction,
5690+
for_values,
5691+
} => {
5692+
let direction = match range_direction {
5693+
Some(PartitionRangeDirection::Left) => " LEFT",
5694+
Some(PartitionRangeDirection::Right) => " RIGHT",
5695+
None => "",
5696+
};
5697+
5698+
write!(
5699+
f,
5700+
"PARTITION ({} RANGE{} FOR VALUES ({}))",
5701+
column_name,
5702+
direction,
5703+
display_comma_separated(for_values)
5704+
)
5705+
}
5706+
}
56005707
}
56015708
}
56025709

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ define_keywords!(
165165
COLLECTION,
166166
COLUMN,
167167
COLUMNS,
168+
COLUMNSTORE,
168169
COMMENT,
169170
COMMIT,
170171
COMMITTED,
@@ -354,6 +355,7 @@ define_keywords!(
354355
HASH,
355356
HAVING,
356357
HEADER,
358+
HEAP,
357359
HIGH_PRIORITY,
358360
HISTORY,
359361
HIVEVAR,

src/parser/mod.rs

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4458,7 +4458,7 @@ impl<'a> Parser<'a> {
44584458
let name = self.parse_object_name(allow_unquoted_hyphen)?;
44594459
let columns = self.parse_view_columns()?;
44604460
let mut options = CreateTableOptions::None;
4461-
let with_options = self.parse_options(Keyword::WITH)?;
4461+
let with_options = self.parse_table_options(Keyword::WITH)?;
44624462
if !with_options.is_empty() {
44634463
options = CreateTableOptions::With(with_options);
44644464
}
@@ -5621,7 +5621,8 @@ impl<'a> Parser<'a> {
56215621
let clustered_by = self.parse_optional_clustered_by()?;
56225622
let hive_formats = self.parse_hive_formats()?;
56235623
// PostgreSQL supports `WITH ( options )`, before `AS`
5624-
let with_options = self.parse_options(Keyword::WITH)?;
5624+
// T-sql supports `WITH` options for clustering and distribution
5625+
let with_options = self.parse_table_options(Keyword::WITH)?;
56255626
let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?;
56265627

56275628
let engine = if self.parse_keyword(Keyword::ENGINE) {
@@ -6399,6 +6400,17 @@ impl<'a> Parser<'a> {
63996400
Ok(None)
64006401
}
64016402

6403+
pub fn parse_table_options(&mut self, keyword: Keyword) -> Result<Vec<SqlOption>, ParserError> {
6404+
if self.parse_keyword(keyword) {
6405+
self.expect_token(&Token::LParen)?;
6406+
let options = self.parse_comma_separated(Parser::parse_sql_option)?;
6407+
self.expect_token(&Token::RParen)?;
6408+
Ok(options)
6409+
} else {
6410+
Ok(vec![])
6411+
}
6412+
}
6413+
64026414
pub fn parse_options(&mut self, keyword: Keyword) -> Result<Vec<SqlOption>, ParserError> {
64036415
if self.parse_keyword(keyword) {
64046416
self.expect_token(&Token::LParen)?;
@@ -6484,11 +6496,99 @@ impl<'a> Parser<'a> {
64846496
}
64856497
}
64866498

6487-
pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
6499+
pub fn parse_key_value(&mut self) -> Result<(Ident, Expr), ParserError> {
64886500
let name = self.parse_identifier(false)?;
64896501
self.expect_token(&Token::Eq)?;
64906502
let value = self.parse_expr()?;
6491-
Ok(SqlOption { name, value })
6503+
6504+
Ok((name, value))
6505+
}
6506+
6507+
pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
6508+
let next_token = self.peek_token();
6509+
let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect);
6510+
6511+
let Token::Word(w) = next_token.token else {
6512+
let (name, value) = self.parse_key_value()?;
6513+
return Ok(SqlOption::KeyValue { name, value });
6514+
};
6515+
6516+
match w.keyword {
6517+
Keyword::HEAP if is_mssql => Ok(SqlOption::Ident(self.parse_identifier(false)?)),
6518+
Keyword::PARTITION if is_mssql => self.parse_table_option_partition(),
6519+
Keyword::CLUSTERED if is_mssql => self.parse_table_option_clustered(),
6520+
_ => {
6521+
let (name, value) = self.parse_key_value()?;
6522+
Ok(SqlOption::KeyValue { name, value })
6523+
}
6524+
}
6525+
}
6526+
6527+
pub fn parse_table_option_clustered(&mut self) -> Result<SqlOption, ParserError> {
6528+
self.expect_keyword(Keyword::CLUSTERED)?;
6529+
6530+
if self.parse_keywords(&[Keyword::COLUMNSTORE, Keyword::INDEX]) {
6531+
if self.parse_keyword(Keyword::ORDER) {
6532+
Ok(SqlOption::Clustered(
6533+
TableOptionsClustered::ColumnstoreIndexOrder(
6534+
self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?,
6535+
),
6536+
))
6537+
} else {
6538+
Ok(SqlOption::Clustered(
6539+
TableOptionsClustered::ColumnstoreIndex,
6540+
))
6541+
}
6542+
} else {
6543+
self.expect_keyword(Keyword::INDEX)?;
6544+
self.expect_token(&Token::LParen)?;
6545+
6546+
let columns = self.parse_comma_separated(|p| {
6547+
let name = p.parse_identifier(false)?;
6548+
let asc = if p.parse_keyword(Keyword::ASC) {
6549+
Some(true)
6550+
} else if p.parse_keyword(Keyword::DESC) {
6551+
Some(false)
6552+
} else {
6553+
None
6554+
};
6555+
6556+
Ok(ClusteredIndex { name, asc })
6557+
})?;
6558+
6559+
self.expect_token(&Token::RParen)?;
6560+
6561+
Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns)))
6562+
}
6563+
}
6564+
6565+
pub fn parse_table_option_partition(&mut self) -> Result<SqlOption, ParserError> {
6566+
self.expect_keyword(Keyword::PARTITION)?;
6567+
self.expect_token(&Token::LParen)?;
6568+
let column_name = self.parse_identifier(false)?;
6569+
6570+
self.expect_keyword(Keyword::RANGE)?;
6571+
let range_direction = if self.parse_keyword(Keyword::LEFT) {
6572+
Some(PartitionRangeDirection::Left)
6573+
} else if self.parse_keyword(Keyword::RIGHT) {
6574+
Some(PartitionRangeDirection::Right)
6575+
} else {
6576+
None
6577+
};
6578+
6579+
self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?;
6580+
self.expect_token(&Token::LParen)?;
6581+
6582+
let for_values = self.parse_comma_separated(Parser::parse_expr)?;
6583+
6584+
self.expect_token(&Token::RParen)?;
6585+
self.expect_token(&Token::RParen)?;
6586+
6587+
Ok(SqlOption::Partition {
6588+
column_name,
6589+
range_direction,
6590+
for_values,
6591+
})
64926592
}
64936593

64946594
pub fn parse_partition(&mut self) -> Result<Partition, ParserError> {

tests/sqlparser_bigquery.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ fn parse_create_view_with_options() {
268268
ViewColumnDef {
269269
name: Ident::new("age"),
270270
data_type: None,
271-
options: Some(vec![SqlOption {
271+
options: Some(vec![SqlOption::KeyValue {
272272
name: Ident::new("description"),
273273
value: Expr::Value(Value::DoubleQuotedString("field age".to_string())),
274274
}])
@@ -288,7 +288,7 @@ fn parse_create_view_with_options() {
288288
unreachable!()
289289
};
290290
assert_eq!(
291-
&SqlOption {
291+
&SqlOption::KeyValue {
292292
name: Ident::new("description"),
293293
value: Expr::Value(Value::DoubleQuotedString(
294294
"a view that expires in 2 days".to_string()
@@ -415,7 +415,7 @@ fn parse_create_table_with_options() {
415415
},
416416
ColumnOptionDef {
417417
name: None,
418-
option: ColumnOption::Options(vec![SqlOption {
418+
option: ColumnOption::Options(vec![SqlOption::KeyValue {
419419
name: Ident::new("description"),
420420
value: Expr::Value(Value::DoubleQuotedString(
421421
"field x".to_string()
@@ -430,7 +430,7 @@ fn parse_create_table_with_options() {
430430
collation: None,
431431
options: vec![ColumnOptionDef {
432432
name: None,
433-
option: ColumnOption::Options(vec![SqlOption {
433+
option: ColumnOption::Options(vec![SqlOption::KeyValue {
434434
name: Ident::new("description"),
435435
value: Expr::Value(Value::DoubleQuotedString(
436436
"field y".to_string()
@@ -449,11 +449,11 @@ fn parse_create_table_with_options() {
449449
Ident::new("age"),
450450
])),
451451
Some(vec![
452-
SqlOption {
452+
SqlOption::KeyValue {
453453
name: Ident::new("partition_expiration_days"),
454454
value: Expr::Value(number("1")),
455455
},
456-
SqlOption {
456+
SqlOption::KeyValue {
457457
name: Ident::new("description"),
458458
value: Expr::Value(Value::DoubleQuotedString(
459459
"table option description".to_string()
@@ -2010,7 +2010,7 @@ fn test_bigquery_create_function() {
20102010
function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number(
20112011
"42"
20122012
)))),
2013-
options: Some(vec![SqlOption {
2013+
options: Some(vec![SqlOption::KeyValue {
20142014
name: Ident::new("x"),
20152015
value: Expr::Value(Value::SingleQuotedString("y".into())),
20162016
}]),

0 commit comments

Comments
 (0)