From d0917c9ab80184054cd545a124928e91e3265950 Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Tue, 13 May 2025 16:48:01 +0300 Subject: [PATCH 1/3] Add support for INCLUDE/EXCLUDE NULLS for UNPIVOT --- src/ast/query.rs | 15 ++++-- src/ast/spans.rs | 1 + src/parser/mod.rs | 10 ++++ tests/sqlparser_common.rs | 109 ++++++++++++++++++++++++-------------- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 33168695f7..b752eb37a2 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1336,7 +1336,7 @@ pub enum TableFactor { /// /// Syntax: /// ```sql - /// table UNPIVOT(value FOR name IN (column1, [ column2, ... ])) [ alias ] + /// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ] /// ``` /// /// See . @@ -1345,6 +1345,7 @@ pub enum TableFactor { value: Ident, name: Ident, columns: Vec, + include_nulls: Option, alias: Option, }, /// A `MATCH_RECOGNIZE` operation on a table. @@ -2015,15 +2016,23 @@ impl fmt::Display for TableFactor { } TableFactor::Unpivot { table, + include_nulls, value, name, columns, alias, } => { + write!(f, "{table} UNPIVOT")?; + if let Some(include_nulls) = include_nulls { + if *include_nulls { + write!(f, " INCLUDE NULLS ")?; + } else { + write!(f, " EXCLUDE NULLS ")?; + } + } write!( f, - "{} UNPIVOT({} FOR {} IN ({}))", - table, + "({} FOR {} IN ({}))", value, name, display_comma_separated(columns) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cb1c48cab8..e01d5af2bd 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1944,6 +1944,7 @@ impl Spanned for TableFactor { TableFactor::Unpivot { table, value, + include_nulls: _, name, columns, alias, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4665bb7697..30607f794d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13368,6 +13368,15 @@ impl<'a> Parser<'a> { &mut self, table: TableFactor, ) -> Result { + let include_nulls = if self.parse_keyword(Keyword::INCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(true) + } else if self.parse_keyword(Keyword::EXCLUDE) { + self.expect_keyword_is(Keyword::NULLS)?; + Some(false) + } else { + None + }; self.expect_token(&Token::LParen)?; let value = self.parse_identifier()?; self.expect_keyword_is(Keyword::FOR)?; @@ -13379,6 +13388,7 @@ impl<'a> Parser<'a> { Ok(TableFactor::Unpivot { table: Box::new(table), value, + include_nulls, name, columns, alias, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a07bcc68ef..858ea748e0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10746,49 +10746,47 @@ fn parse_unpivot_table() { "SELECT * FROM sales AS s ", "UNPIVOT(quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" ); - - pretty_assertions::assert_eq!( - verified_only_select(sql).from[0].relation, - Unpivot { - table: Box::new(TableFactor::Table { - name: ObjectName::from(vec![Ident::new("sales")]), - alias: Some(TableAlias { - name: Ident::new("s"), - columns: vec![] - }), - args: None, - with_hints: vec![], - version: None, - partitions: vec![], - with_ordinality: false, - json_path: None, - sample: None, - index_hints: vec![], + let base_unpivot = Unpivot { + table: Box::new(TableFactor::Table { + name: ObjectName::from(vec![Ident::new("sales")]), + alias: Some(TableAlias { + name: Ident::new("s"), + columns: vec![], }), - value: Ident { - value: "quantity".to_string(), - quote_style: None, - span: Span::empty() - }, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }), + include_nulls: None, + value: Ident { + value: "quantity".to_string(), + quote_style: None, + span: Span::empty(), + }, - name: Ident { - value: "quarter".to_string(), - quote_style: None, - span: Span::empty() - }, - columns: ["Q1", "Q2", "Q3", "Q4"] + name: Ident { + value: "quarter".to_string(), + quote_style: None, + span: Span::empty(), + }, + columns: ["Q1", "Q2", "Q3", "Q4"] + .into_iter() + .map(Ident::new) + .collect(), + alias: Some(TableAlias { + name: Ident::new("u"), + columns: ["product", "quarter", "quantity"] .into_iter() - .map(Ident::new) + .map(TableAliasColumnDef::from_name) .collect(), - alias: Some(TableAlias { - name: Ident::new("u"), - columns: ["product", "quarter", "quantity"] - .into_iter() - .map(TableAliasColumnDef::from_name) - .collect(), - }), - } - ); + }), + }; + pretty_assertions::assert_eq!(verified_only_select(sql).from[0].relation, base_unpivot); assert_eq!(verified_stmt(sql).to_string(), sql); let sql_without_aliases = concat!( @@ -10808,6 +10806,38 @@ fn parse_unpivot_table() { verified_stmt(sql_without_aliases).to_string(), sql_without_aliases ); + + let sql_unpivot_exclude_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT EXCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { include_nulls, .. } = + &verified_only_select(sql_unpivot_exclude_nulls).from[0].relation + { + assert_eq!(*include_nulls, Some(false)); + } + + assert_eq!( + verified_stmt(sql_unpivot_exclude_nulls).to_string(), + sql_unpivot_exclude_nulls + ); + + let sql_unpivot_include_nulls = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { include_nulls, .. } = + &verified_only_select(sql_unpivot_include_nulls).from[0].relation + { + assert_eq!(*include_nulls, Some(true)); + } + + assert_eq!( + verified_stmt(sql_unpivot_include_nulls).to_string(), + sql_unpivot_include_nulls + ); } #[test] @@ -10904,6 +10934,7 @@ fn parse_pivot_unpivot_table() { sample: None, index_hints: vec![], }), + include_nulls: None, value: Ident { value: "population".to_string(), quote_style: None, From e029450af6ee9b13e4b2ae520f6b170c5ad7612f Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Wed, 14 May 2025 17:17:47 +0300 Subject: [PATCH 2/3] Add NullInclussion as type instead of bool --- src/ast/mod.rs | 20 ++++++++++++++++++++ src/ast/query.rs | 8 ++------ src/parser/mod.rs | 4 ++-- tests/sqlparser_common.rs | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d1d706e46f..1ad1b6da7c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9633,6 +9633,26 @@ impl fmt::Display for OpenStatement { } } +/// Specifies Include / Exclude NULL within UNPIVOT command. +/// For example +/// `UNPIVOT (column1 FOR new_column IN (col3, col4, col5, col6))` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum NullInclusion { + IncludeNulls, + ExcludeNulls, +} + +impl fmt::Display for NullInclusion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NullInclusion::IncludeNulls => write!(f, "INCLUDE NULLS"), + NullInclusion::ExcludeNulls => write!(f, "EXCLUDE NULLS"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/query.rs b/src/ast/query.rs index b752eb37a2..8baae457c6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1345,7 +1345,7 @@ pub enum TableFactor { value: Ident, name: Ident, columns: Vec, - include_nulls: Option, + include_nulls: Option, alias: Option, }, /// A `MATCH_RECOGNIZE` operation on a table. @@ -2024,11 +2024,7 @@ impl fmt::Display for TableFactor { } => { write!(f, "{table} UNPIVOT")?; if let Some(include_nulls) = include_nulls { - if *include_nulls { - write!(f, " INCLUDE NULLS ")?; - } else { - write!(f, " EXCLUDE NULLS ")?; - } + write!(f, " {include_nulls} ")?; } write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 30607f794d..abb0f2b168 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13370,10 +13370,10 @@ impl<'a> Parser<'a> { ) -> Result { let include_nulls = if self.parse_keyword(Keyword::INCLUDE) { self.expect_keyword_is(Keyword::NULLS)?; - Some(true) + Some(NullInclusion::IncludeNulls) } else if self.parse_keyword(Keyword::EXCLUDE) { self.expect_keyword_is(Keyword::NULLS)?; - Some(false) + Some(NullInclusion::ExcludeNulls) } else { None }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 858ea748e0..c175fb92a7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10815,7 +10815,7 @@ fn parse_unpivot_table() { if let Unpivot { include_nulls, .. } = &verified_only_select(sql_unpivot_exclude_nulls).from[0].relation { - assert_eq!(*include_nulls, Some(false)); + assert_eq!(*include_nulls, Some(NullInclusion::ExcludeNulls)); } assert_eq!( @@ -10831,7 +10831,7 @@ fn parse_unpivot_table() { if let Unpivot { include_nulls, .. } = &verified_only_select(sql_unpivot_include_nulls).from[0].relation { - assert_eq!(*include_nulls, Some(true)); + assert_eq!(*include_nulls, Some(NullInclusion::IncludeNulls)); } assert_eq!( From 9e99163af2c68c3d281d802feaa889c7b8b9284c Mon Sep 17 00:00:00 2001 From: Denys Tsomenko Date: Wed, 14 May 2025 17:31:42 +0300 Subject: [PATCH 3/3] Replace include_nulls to null_inclusion everywhere --- src/ast/query.rs | 8 ++++---- src/ast/spans.rs | 2 +- src/parser/mod.rs | 4 ++-- tests/sqlparser_common.rs | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 8baae457c6..adb8516f9d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1345,7 +1345,7 @@ pub enum TableFactor { value: Ident, name: Ident, columns: Vec, - include_nulls: Option, + null_inclusion: Option, alias: Option, }, /// A `MATCH_RECOGNIZE` operation on a table. @@ -2016,15 +2016,15 @@ impl fmt::Display for TableFactor { } TableFactor::Unpivot { table, - include_nulls, + null_inclusion, value, name, columns, alias, } => { write!(f, "{table} UNPIVOT")?; - if let Some(include_nulls) = include_nulls { - write!(f, " {include_nulls} ")?; + if let Some(null_inclusion) = null_inclusion { + write!(f, " {null_inclusion} ")?; } write!( f, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e01d5af2bd..bffd117223 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1944,7 +1944,7 @@ impl Spanned for TableFactor { TableFactor::Unpivot { table, value, - include_nulls: _, + null_inclusion: _, name, columns, alias, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index abb0f2b168..838996130b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13368,7 +13368,7 @@ impl<'a> Parser<'a> { &mut self, table: TableFactor, ) -> Result { - let include_nulls = if self.parse_keyword(Keyword::INCLUDE) { + let null_inclusion = if self.parse_keyword(Keyword::INCLUDE) { self.expect_keyword_is(Keyword::NULLS)?; Some(NullInclusion::IncludeNulls) } else if self.parse_keyword(Keyword::EXCLUDE) { @@ -13388,7 +13388,7 @@ impl<'a> Parser<'a> { Ok(TableFactor::Unpivot { table: Box::new(table), value, - include_nulls, + null_inclusion, name, columns, alias, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c175fb92a7..8e3bc00217 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10762,7 +10762,7 @@ fn parse_unpivot_table() { sample: None, index_hints: vec![], }), - include_nulls: None, + null_inclusion: None, value: Ident { value: "quantity".to_string(), quote_style: None, @@ -10812,10 +10812,10 @@ fn parse_unpivot_table() { "UNPIVOT EXCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" ); - if let Unpivot { include_nulls, .. } = + if let Unpivot { null_inclusion, .. } = &verified_only_select(sql_unpivot_exclude_nulls).from[0].relation { - assert_eq!(*include_nulls, Some(NullInclusion::ExcludeNulls)); + assert_eq!(*null_inclusion, Some(NullInclusion::ExcludeNulls)); } assert_eq!( @@ -10828,10 +10828,10 @@ fn parse_unpivot_table() { "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1, Q2, Q3, Q4)) AS u (product, quarter, quantity)" ); - if let Unpivot { include_nulls, .. } = + if let Unpivot { null_inclusion, .. } = &verified_only_select(sql_unpivot_include_nulls).from[0].relation { - assert_eq!(*include_nulls, Some(NullInclusion::IncludeNulls)); + assert_eq!(*null_inclusion, Some(NullInclusion::IncludeNulls)); } assert_eq!( @@ -10934,7 +10934,7 @@ fn parse_pivot_unpivot_table() { sample: None, index_hints: vec![], }), - include_nulls: None, + null_inclusion: None, value: Ident { value: "population".to_string(), quote_style: None,