Skip to content

Commit b9e7754

Browse files
authored
feat: Add support for MSSQL table options (#1414)
1 parent cb0c511 commit b9e7754

File tree

7 files changed

+523
-65
lines changed

7 files changed

+523
-65
lines changed

src/ast/mod.rs

+117-3
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,15 @@ pub enum CreateTableOptions {
20822082
/// e.g. `WITH (description = "123")`
20832083
///
20842084
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
2085+
///
2086+
/// MSSQL supports more specific options that's not only key-value pairs.
2087+
///
2088+
/// WITH (
2089+
/// DISTRIBUTION = ROUND_ROBIN,
2090+
/// CLUSTERED INDEX (column_a DESC, column_b)
2091+
/// )
2092+
///
2093+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#syntax>
20852094
With(Vec<SqlOption>),
20862095
/// Options specified using the `OPTIONS` keyword.
20872096
/// e.g. `OPTIONS(description = "123")`
@@ -5728,14 +5737,119 @@ pub struct HiveFormat {
57285737
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
57295738
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
57305739
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5731-
pub struct SqlOption {
5740+
pub struct ClusteredIndex {
57325741
pub name: Ident,
5733-
pub value: Expr,
5742+
pub asc: Option<bool>,
5743+
}
5744+
5745+
impl fmt::Display for ClusteredIndex {
5746+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5747+
write!(f, "{}", self.name)?;
5748+
match self.asc {
5749+
Some(true) => write!(f, " ASC"),
5750+
Some(false) => write!(f, " DESC"),
5751+
_ => Ok(()),
5752+
}
5753+
}
5754+
}
5755+
5756+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5757+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5758+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5759+
pub enum TableOptionsClustered {
5760+
ColumnstoreIndex,
5761+
ColumnstoreIndexOrder(Vec<Ident>),
5762+
Index(Vec<ClusteredIndex>),
5763+
}
5764+
5765+
impl fmt::Display for TableOptionsClustered {
5766+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5767+
match self {
5768+
TableOptionsClustered::ColumnstoreIndex => {
5769+
write!(f, "CLUSTERED COLUMNSTORE INDEX")
5770+
}
5771+
TableOptionsClustered::ColumnstoreIndexOrder(values) => {
5772+
write!(
5773+
f,
5774+
"CLUSTERED COLUMNSTORE INDEX ORDER ({})",
5775+
display_comma_separated(values)
5776+
)
5777+
}
5778+
TableOptionsClustered::Index(values) => {
5779+
write!(f, "CLUSTERED INDEX ({})", display_comma_separated(values))
5780+
}
5781+
}
5782+
}
5783+
}
5784+
5785+
/// Specifies which partition the boundary values on table partitioning belongs to.
5786+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5787+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5788+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5789+
pub enum PartitionRangeDirection {
5790+
Left,
5791+
Right,
5792+
}
5793+
5794+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5795+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5796+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5797+
pub enum SqlOption {
5798+
/// Clustered represents the clustered version of table storage for MSSQL.
5799+
///
5800+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TableOptions>
5801+
Clustered(TableOptionsClustered),
5802+
/// Single identifier options, e.g. `HEAP` for MSSQL.
5803+
///
5804+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TableOptions>
5805+
Ident(Ident),
5806+
/// Any option that consists of a key value pair where the value is an expression. e.g.
5807+
///
5808+
/// WITH(DISTRIBUTION = ROUND_ROBIN)
5809+
KeyValue { key: Ident, value: Expr },
5810+
/// One or more table partitions and represents which partition the boundary values belong to,
5811+
/// e.g.
5812+
///
5813+
/// PARTITION (id RANGE LEFT FOR VALUES (10, 20, 30, 40))
5814+
///
5815+
/// <https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-azure-sql-data-warehouse?view=aps-pdw-2016-au7#TablePartitionOptions>
5816+
Partition {
5817+
column_name: Ident,
5818+
range_direction: Option<PartitionRangeDirection>,
5819+
for_values: Vec<Expr>,
5820+
},
57345821
}
57355822

57365823
impl fmt::Display for SqlOption {
57375824
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5738-
write!(f, "{} = {}", self.name, self.value)
5825+
match self {
5826+
SqlOption::Clustered(c) => write!(f, "{}", c),
5827+
SqlOption::Ident(ident) => {
5828+
write!(f, "{}", ident)
5829+
}
5830+
SqlOption::KeyValue { key: name, value } => {
5831+
write!(f, "{} = {}", name, value)
5832+
}
5833+
SqlOption::Partition {
5834+
column_name,
5835+
range_direction,
5836+
for_values,
5837+
} => {
5838+
let direction = match range_direction {
5839+
Some(PartitionRangeDirection::Left) => " LEFT",
5840+
Some(PartitionRangeDirection::Right) => " RIGHT",
5841+
None => "",
5842+
};
5843+
5844+
write!(
5845+
f,
5846+
"PARTITION ({} RANGE{} FOR VALUES ({}))",
5847+
column_name,
5848+
direction,
5849+
display_comma_separated(for_values)
5850+
)
5851+
}
5852+
}
57395853
}
57405854
}
57415855

src/keywords.rs

+2
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ define_keywords!(
166166
COLLECTION,
167167
COLUMN,
168168
COLUMNS,
169+
COLUMNSTORE,
169170
COMMENT,
170171
COMMIT,
171172
COMMITTED,
@@ -355,6 +356,7 @@ define_keywords!(
355356
HASH,
356357
HAVING,
357358
HEADER,
359+
HEAP,
358360
HIGH_PRIORITY,
359361
HISTORY,
360362
HIVEVAR,

src/parser/mod.rs

+97-10
Original file line numberDiff line numberDiff line change
@@ -6480,10 +6480,91 @@ impl<'a> Parser<'a> {
64806480
}
64816481

64826482
pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
6483-
let name = self.parse_identifier(false)?;
6484-
self.expect_token(&Token::Eq)?;
6485-
let value = self.parse_expr()?;
6486-
Ok(SqlOption { name, value })
6483+
let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect);
6484+
6485+
match self.peek_token().token {
6486+
Token::Word(w) if w.keyword == Keyword::HEAP && is_mssql => {
6487+
Ok(SqlOption::Ident(self.parse_identifier(false)?))
6488+
}
6489+
Token::Word(w) if w.keyword == Keyword::PARTITION && is_mssql => {
6490+
self.parse_option_partition()
6491+
}
6492+
Token::Word(w) if w.keyword == Keyword::CLUSTERED && is_mssql => {
6493+
self.parse_option_clustered()
6494+
}
6495+
_ => {
6496+
let name = self.parse_identifier(false)?;
6497+
self.expect_token(&Token::Eq)?;
6498+
let value = self.parse_expr()?;
6499+
6500+
Ok(SqlOption::KeyValue { key: name, value })
6501+
}
6502+
}
6503+
}
6504+
6505+
pub fn parse_option_clustered(&mut self) -> Result<SqlOption, ParserError> {
6506+
if self.parse_keywords(&[
6507+
Keyword::CLUSTERED,
6508+
Keyword::COLUMNSTORE,
6509+
Keyword::INDEX,
6510+
Keyword::ORDER,
6511+
]) {
6512+
Ok(SqlOption::Clustered(
6513+
TableOptionsClustered::ColumnstoreIndexOrder(
6514+
self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?,
6515+
),
6516+
))
6517+
} else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::COLUMNSTORE, Keyword::INDEX]) {
6518+
Ok(SqlOption::Clustered(
6519+
TableOptionsClustered::ColumnstoreIndex,
6520+
))
6521+
} else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::INDEX]) {
6522+
self.expect_token(&Token::LParen)?;
6523+
6524+
let columns = self.parse_comma_separated(|p| {
6525+
let name = p.parse_identifier(false)?;
6526+
let asc = p.parse_asc_desc();
6527+
6528+
Ok(ClusteredIndex { name, asc })
6529+
})?;
6530+
6531+
self.expect_token(&Token::RParen)?;
6532+
6533+
Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns)))
6534+
} else {
6535+
Err(ParserError::ParserError(
6536+
"invalid CLUSTERED sequence".to_string(),
6537+
))
6538+
}
6539+
}
6540+
6541+
pub fn parse_option_partition(&mut self) -> Result<SqlOption, ParserError> {
6542+
self.expect_keyword(Keyword::PARTITION)?;
6543+
self.expect_token(&Token::LParen)?;
6544+
let column_name = self.parse_identifier(false)?;
6545+
6546+
self.expect_keyword(Keyword::RANGE)?;
6547+
let range_direction = if self.parse_keyword(Keyword::LEFT) {
6548+
Some(PartitionRangeDirection::Left)
6549+
} else if self.parse_keyword(Keyword::RIGHT) {
6550+
Some(PartitionRangeDirection::Right)
6551+
} else {
6552+
None
6553+
};
6554+
6555+
self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?;
6556+
self.expect_token(&Token::LParen)?;
6557+
6558+
let for_values = self.parse_comma_separated(Parser::parse_expr)?;
6559+
6560+
self.expect_token(&Token::RParen)?;
6561+
self.expect_token(&Token::RParen)?;
6562+
6563+
Ok(SqlOption::Partition {
6564+
column_name,
6565+
range_direction,
6566+
for_values,
6567+
})
64876568
}
64886569

64896570
pub fn parse_partition(&mut self) -> Result<Partition, ParserError> {
@@ -11014,17 +11095,23 @@ impl<'a> Parser<'a> {
1101411095
})
1101511096
}
1101611097

11017-
/// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY)
11018-
pub fn parse_order_by_expr(&mut self) -> Result<OrderByExpr, ParserError> {
11019-
let expr = self.parse_expr()?;
11020-
11021-
let asc = if self.parse_keyword(Keyword::ASC) {
11098+
/// Parse ASC or DESC, returns an Option with true if ASC, false of DESC or `None` if none of
11099+
/// them.
11100+
pub fn parse_asc_desc(&mut self) -> Option<bool> {
11101+
if self.parse_keyword(Keyword::ASC) {
1102211102
Some(true)
1102311103
} else if self.parse_keyword(Keyword::DESC) {
1102411104
Some(false)
1102511105
} else {
1102611106
None
11027-
};
11107+
}
11108+
}
11109+
11110+
/// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY)
11111+
pub fn parse_order_by_expr(&mut self) -> Result<OrderByExpr, ParserError> {
11112+
let expr = self.parse_expr()?;
11113+
11114+
let asc = self.parse_asc_desc();
1102811115

1102911116
let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) {
1103011117
Some(true)

tests/sqlparser_bigquery.rs

+14-14
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ fn parse_create_view_with_options() {
267267
ViewColumnDef {
268268
name: Ident::new("age"),
269269
data_type: None,
270-
options: Some(vec![SqlOption {
271-
name: Ident::new("description"),
270+
options: Some(vec![SqlOption::KeyValue {
271+
key: Ident::new("description"),
272272
value: Expr::Value(Value::DoubleQuotedString("field age".to_string())),
273273
}])
274274
},
@@ -287,8 +287,8 @@ fn parse_create_view_with_options() {
287287
unreachable!()
288288
};
289289
assert_eq!(
290-
&SqlOption {
291-
name: Ident::new("description"),
290+
&SqlOption::KeyValue {
291+
key: Ident::new("description"),
292292
value: Expr::Value(Value::DoubleQuotedString(
293293
"a view that expires in 2 days".to_string()
294294
)),
@@ -414,8 +414,8 @@ fn parse_create_table_with_options() {
414414
},
415415
ColumnOptionDef {
416416
name: None,
417-
option: ColumnOption::Options(vec![SqlOption {
418-
name: Ident::new("description"),
417+
option: ColumnOption::Options(vec![SqlOption::KeyValue {
418+
key: Ident::new("description"),
419419
value: Expr::Value(Value::DoubleQuotedString(
420420
"field x".to_string()
421421
)),
@@ -429,8 +429,8 @@ fn parse_create_table_with_options() {
429429
collation: None,
430430
options: vec![ColumnOptionDef {
431431
name: None,
432-
option: ColumnOption::Options(vec![SqlOption {
433-
name: Ident::new("description"),
432+
option: ColumnOption::Options(vec![SqlOption::KeyValue {
433+
key: Ident::new("description"),
434434
value: Expr::Value(Value::DoubleQuotedString(
435435
"field y".to_string()
436436
)),
@@ -448,12 +448,12 @@ fn parse_create_table_with_options() {
448448
Ident::new("age"),
449449
])),
450450
Some(vec![
451-
SqlOption {
452-
name: Ident::new("partition_expiration_days"),
451+
SqlOption::KeyValue {
452+
key: Ident::new("partition_expiration_days"),
453453
value: Expr::Value(number("1")),
454454
},
455-
SqlOption {
456-
name: Ident::new("description"),
455+
SqlOption::KeyValue {
456+
key: Ident::new("description"),
457457
value: Expr::Value(Value::DoubleQuotedString(
458458
"table option description".to_string()
459459
)),
@@ -2005,8 +2005,8 @@ fn test_bigquery_create_function() {
20052005
function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number(
20062006
"42"
20072007
)))),
2008-
options: Some(vec![SqlOption {
2009-
name: Ident::new("x"),
2008+
options: Some(vec![SqlOption::KeyValue {
2009+
key: Ident::new("x"),
20102010
value: Expr::Value(Value::SingleQuotedString("y".into())),
20112011
}]),
20122012
behavior: None,

0 commit comments

Comments
 (0)