Skip to content

Commit d0917c9

Browse files
committed
Add support for INCLUDE/EXCLUDE NULLS for UNPIVOT
1 parent c6e897d commit d0917c9

File tree

4 files changed

+93
-42
lines changed

4 files changed

+93
-42
lines changed

src/ast/query.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,7 @@ pub enum TableFactor {
13361336
///
13371337
/// Syntax:
13381338
/// ```sql
1339-
/// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ]
1339+
/// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ]
13401340
/// ```
13411341
///
13421342
/// See <https://docs.snowflake.com/en/sql-reference/constructs/unpivot>.
@@ -1345,6 +1345,7 @@ pub enum TableFactor {
13451345
value: Ident,
13461346
name: Ident,
13471347
columns: Vec<Ident>,
1348+
include_nulls: Option<bool>,
13481349
alias: Option<TableAlias>,
13491350
},
13501351
/// A `MATCH_RECOGNIZE` operation on a table.
@@ -2015,15 +2016,23 @@ impl fmt::Display for TableFactor {
20152016
}
20162017
TableFactor::Unpivot {
20172018
table,
2019+
include_nulls,
20182020
value,
20192021
name,
20202022
columns,
20212023
alias,
20222024
} => {
2025+
write!(f, "{table} UNPIVOT")?;
2026+
if let Some(include_nulls) = include_nulls {
2027+
if *include_nulls {
2028+
write!(f, " INCLUDE NULLS ")?;
2029+
} else {
2030+
write!(f, " EXCLUDE NULLS ")?;
2031+
}
2032+
}
20232033
write!(
20242034
f,
2025-
"{} UNPIVOT({} FOR {} IN ({}))",
2026-
table,
2035+
"({} FOR {} IN ({}))",
20272036
value,
20282037
name,
20292038
display_comma_separated(columns)

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,7 @@ impl Spanned for TableFactor {
19441944
TableFactor::Unpivot {
19451945
table,
19461946
value,
1947+
include_nulls: _,
19471948
name,
19481949
columns,
19491950
alias,

src/parser/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13368,6 +13368,15 @@ impl<'a> Parser<'a> {
1336813368
&mut self,
1336913369
table: TableFactor,
1337013370
) -> Result<TableFactor, ParserError> {
13371+
let include_nulls = if self.parse_keyword(Keyword::INCLUDE) {
13372+
self.expect_keyword_is(Keyword::NULLS)?;
13373+
Some(true)
13374+
} else if self.parse_keyword(Keyword::EXCLUDE) {
13375+
self.expect_keyword_is(Keyword::NULLS)?;
13376+
Some(false)
13377+
} else {
13378+
None
13379+
};
1337113380
self.expect_token(&Token::LParen)?;
1337213381
let value = self.parse_identifier()?;
1337313382
self.expect_keyword_is(Keyword::FOR)?;
@@ -13379,6 +13388,7 @@ impl<'a> Parser<'a> {
1337913388
Ok(TableFactor::Unpivot {
1338013389
table: Box::new(table),
1338113390
value,
13391+
include_nulls,
1338213392
name,
1338313393
columns,
1338413394
alias,

tests/sqlparser_common.rs

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10746,49 +10746,47 @@ fn parse_unpivot_table() {
1074610746
"SELECT * FROM sales AS s ",
1074710747
"UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)"
1074810748
);
10749-
10750-
pretty_assertions::assert_eq!(
10751-
verified_only_select(sql).from[0].relation,
10752-
Unpivot {
10753-
table: Box::new(TableFactor::Table {
10754-
name: ObjectName::from(vec![Ident::new("sales")]),
10755-
alias: Some(TableAlias {
10756-
name: Ident::new("s"),
10757-
columns: vec![]
10758-
}),
10759-
args: None,
10760-
with_hints: vec![],
10761-
version: None,
10762-
partitions: vec![],
10763-
with_ordinality: false,
10764-
json_path: None,
10765-
sample: None,
10766-
index_hints: vec![],
10749+
let base_unpivot = Unpivot {
10750+
table: Box::new(TableFactor::Table {
10751+
name: ObjectName::from(vec![Ident::new("sales")]),
10752+
alias: Some(TableAlias {
10753+
name: Ident::new("s"),
10754+
columns: vec![],
1076710755
}),
10768-
value: Ident {
10769-
value: "quantity".to_string(),
10770-
quote_style: None,
10771-
span: Span::empty()
10772-
},
10756+
args: None,
10757+
with_hints: vec![],
10758+
version: None,
10759+
partitions: vec![],
10760+
with_ordinality: false,
10761+
json_path: None,
10762+
sample: None,
10763+
index_hints: vec![],
10764+
}),
10765+
include_nulls: None,
10766+
value: Ident {
10767+
value: "quantity".to_string(),
10768+
quote_style: None,
10769+
span: Span::empty(),
10770+
},
1077310771

10774-
name: Ident {
10775-
value: "quarter".to_string(),
10776-
quote_style: None,
10777-
span: Span::empty()
10778-
},
10779-
columns: ["Q1", "Q2", "Q3", "Q4"]
10772+
name: Ident {
10773+
value: "quarter".to_string(),
10774+
quote_style: None,
10775+
span: Span::empty(),
10776+
},
10777+
columns: ["Q1", "Q2", "Q3", "Q4"]
10778+
.into_iter()
10779+
.map(Ident::new)
10780+
.collect(),
10781+
alias: Some(TableAlias {
10782+
name: Ident::new("u"),
10783+
columns: ["product", "quarter", "quantity"]
1078010784
.into_iter()
10781-
.map(Ident::new)
10785+
.map(TableAliasColumnDef::from_name)
1078210786
.collect(),
10783-
alias: Some(TableAlias {
10784-
name: Ident::new("u"),
10785-
columns: ["product", "quarter", "quantity"]
10786-
.into_iter()
10787-
.map(TableAliasColumnDef::from_name)
10788-
.collect(),
10789-
}),
10790-
}
10791-
);
10787+
}),
10788+
};
10789+
pretty_assertions::assert_eq!(verified_only_select(sql).from[0].relation, base_unpivot);
1079210790
assert_eq!(verified_stmt(sql).to_string(), sql);
1079310791

1079410792
let sql_without_aliases = concat!(
@@ -10808,6 +10806,38 @@ fn parse_unpivot_table() {
1080810806
verified_stmt(sql_without_aliases).to_string(),
1080910807
sql_without_aliases
1081010808
);
10809+
10810+
let sql_unpivot_exclude_nulls = concat!(
10811+
"SELECT * FROM sales AS s ",
10812+
"UNPIVOT EXCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)"
10813+
);
10814+
10815+
if let Unpivot { include_nulls, .. } =
10816+
&verified_only_select(sql_unpivot_exclude_nulls).from[0].relation
10817+
{
10818+
assert_eq!(*include_nulls, Some(false));
10819+
}
10820+
10821+
assert_eq!(
10822+
verified_stmt(sql_unpivot_exclude_nulls).to_string(),
10823+
sql_unpivot_exclude_nulls
10824+
);
10825+
10826+
let sql_unpivot_include_nulls = concat!(
10827+
"SELECT * FROM sales AS s ",
10828+
"UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)"
10829+
);
10830+
10831+
if let Unpivot { include_nulls, .. } =
10832+
&verified_only_select(sql_unpivot_include_nulls).from[0].relation
10833+
{
10834+
assert_eq!(*include_nulls, Some(true));
10835+
}
10836+
10837+
assert_eq!(
10838+
verified_stmt(sql_unpivot_include_nulls).to_string(),
10839+
sql_unpivot_include_nulls
10840+
);
1081110841
}
1081210842

1081310843
#[test]
@@ -10904,6 +10934,7 @@ fn parse_pivot_unpivot_table() {
1090410934
sample: None,
1090510935
index_hints: vec![],
1090610936
}),
10937+
include_nulls: None,
1090710938
value: Ident {
1090810939
value: "population".to_string(),
1090910940
quote_style: None,

0 commit comments

Comments
 (0)