From 6efcb55df226d4812e60a43386c0c7db46feeae6 Mon Sep 17 00:00:00 2001 From: Nikita-str Date: Thu, 7 Mar 2024 19:48:25 +0300 Subject: [PATCH 1/4] allow named windows in `OVER (window_definition)` clause --- src/ast/mod.rs | 15 ++++++++++- src/parser/mod.rs | 11 +++++++++ tests/sqlparser_common.rs | 52 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 1 + 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d8688c1ab..45ce7e040 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1213,11 +1213,19 @@ impl Display for WindowType { } } -/// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) +/// A window specification (i.e. `OVER ([window_name] PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WindowSpec { + /// Optional window name. + /// + /// You can find it at least in [MySQL][1], [BigQuery][2], [PostgreSQL][3] + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/window-functions-named-windows.html + /// [2]: https://cloud.google.com/bigquery/docs/reference/standard-sql/window-function-calls + /// [3]: https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS + pub window_name: Option, /// `OVER (PARTITION BY ...)` pub partition_by: Vec, /// `OVER (ORDER BY ...)` @@ -1229,7 +1237,12 @@ pub struct WindowSpec { impl fmt::Display for WindowSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut delim = ""; + if let Some(window_name) = &self.window_name { + delim = " "; + write!(f, "{window_name}")?; + } if !self.partition_by.is_empty() { + f.write_str(delim)?; delim = " "; write!( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6d7ac3604..beb9cbe28 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9404,6 +9404,15 @@ impl<'a> Parser<'a> { } pub fn parse_window_spec(&mut self) -> Result { + let window_name = match self.peek_token().token { + Token::Word(word) + if word.keyword == Keyword::PARTITION || word.keyword == Keyword::ORDER => + { + None + } + _ => self.maybe_parse(|parser| parser.parse_identifier(false)), + }; + let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { self.parse_comma_separated(Parser::parse_expr)? } else { @@ -9414,6 +9423,7 @@ impl<'a> Parser<'a> { } else { vec![] }; + let window_frame = if !self.consume_token(&Token::RParen) { let window_frame = self.parse_window_frame()?; self.expect_token(&Token::RParen)?; @@ -9422,6 +9432,7 @@ impl<'a> Parser<'a> { None }; Ok(WindowSpec { + window_name, partition_by, order_by, window_frame, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f81456849..5794ef284 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2062,6 +2062,7 @@ fn parse_select_qualify() { null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), @@ -4101,6 +4102,7 @@ fn parse_window_functions() { null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), @@ -4115,6 +4117,54 @@ fn parse_window_functions() { }), expr_from_projection(&select.projection[0]) ); + + for i in 0..7 { + assert!(matches!( + expr_from_projection(&select.projection[i]), + Expr::Function(Function { + over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, + .. + })), + .. + }) + )); + } +} + +#[test] +fn parse_named_window_functions() { + let sql = "SELECT row_number() OVER (w ORDER BY dt DESC), \ + sum(foo) OVER (win PARTITION BY a, b ORDER BY c, d \ + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) \ + FROM foo \ + WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; + + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + ], + options: None, + }; + supported_dialects.verified_stmt(sql); + + let select = verified_only_select(sql); + assert_eq!(2, select.projection.len()); + for (i, win_name) in ["w", "win"].iter().enumerate() { + assert!(matches!( + expr_from_projection(&select.projection[i]), + Expr::Function(Function { + over: Some(WindowType::WindowSpec(WindowSpec { + window_name: Some(Ident { value, .. }), + .. + })), + .. + }) if value == win_name + )); + } } #[test] @@ -4215,6 +4265,7 @@ fn test_parse_named_window() { quote_style: None, }, WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident { @@ -4233,6 +4284,7 @@ fn test_parse_named_window() { quote_style: None, }, WindowSpec { + window_name: None, partition_by: vec![Expr::Identifier(Ident { value: "C11".to_string(), quote_style: None, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 6c8b507de..4bf020d6b 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -434,6 +434,7 @@ fn parse_window_function_with_filter() { ))], null_treatment: None, over: Some(WindowType::WindowSpec(WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![], window_frame: None, From 144f56081839c1df47bbe4fd811fe0b5ab9fea7f Mon Sep 17 00:00:00 2001 From: Nikita-str Date: Thu, 7 Mar 2024 20:14:52 +0300 Subject: [PATCH 2/4] allow `RANGE | ROWS | GROUPS` after named `window_definition` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Actually disallow any keyword as `window_name` — seems like databases do the same --- src/parser/mod.rs | 8 +++----- tests/sqlparser_common.rs | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index beb9cbe28..547acb5bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9405,12 +9405,10 @@ impl<'a> Parser<'a> { pub fn parse_window_spec(&mut self) -> Result { let window_name = match self.peek_token().token { - Token::Word(word) - if word.keyword == Keyword::PARTITION || word.keyword == Keyword::ORDER => - { - None + Token::Word(word) if word.keyword == Keyword::NoKeyword => { + self.maybe_parse(|parser| parser.parse_identifier(false)) } - _ => self.maybe_parse(|parser| parser.parse_identifier(false)), + _ => None, }; let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5794ef284..49301b3c2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4134,12 +4134,6 @@ fn parse_window_functions() { #[test] fn parse_named_window_functions() { - let sql = "SELECT row_number() OVER (w ORDER BY dt DESC), \ - sum(foo) OVER (win PARTITION BY a, b ORDER BY c, d \ - ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) \ - FROM foo \ - WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; - let supported_dialects = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), @@ -4149,6 +4143,12 @@ fn parse_named_window_functions() { ], options: None, }; + + let sql = "SELECT row_number() OVER (w ORDER BY dt DESC), \ + sum(foo) OVER (win PARTITION BY a, b ORDER BY c, d \ + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) \ + FROM foo \ + WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)"; supported_dialects.verified_stmt(sql); let select = verified_only_select(sql); @@ -4165,6 +4165,14 @@ fn parse_named_window_functions() { }) if value == win_name )); } + + let sql = "SELECT \ + FIRST_VALUE(x) OVER (w ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS first, \ + FIRST_VALUE(x) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS last, \ + SUM(y) OVER (win PARTITION BY x) AS last \ + FROM EMPLOYEE \ + WINDOW w AS (PARTITION BY x), win AS (w ORDER BY y)"; + supported_dialects.verified_stmt(sql); } #[test] From 03f7e6ac1df9d8668ba64bacbacbe279db6ffc23 Mon Sep 17 00:00:00 2001 From: Nikita-str Date: Tue, 12 Mar 2024 05:37:29 +0300 Subject: [PATCH 3/4] removed magic consts in tests --- tests/sqlparser_common.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 49301b3c2..d2f7912be 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4094,7 +4094,10 @@ fn parse_window_functions() { GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \ FROM foo"; let select = verified_only_select(sql); - assert_eq!(7, select.projection.len()); + + const EXPECTED_PROJ_QTY: usize = 7; + assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); + assert_eq!( &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), @@ -4118,7 +4121,7 @@ fn parse_window_functions() { expr_from_projection(&select.projection[0]) ); - for i in 0..7 { + for i in 0..EXPECTED_PROJ_QTY { assert!(matches!( expr_from_projection(&select.projection[i]), Expr::Function(Function { @@ -4152,8 +4155,12 @@ fn parse_named_window_functions() { supported_dialects.verified_stmt(sql); let select = verified_only_select(sql); - assert_eq!(2, select.projection.len()); - for (i, win_name) in ["w", "win"].iter().enumerate() { + + const EXPECTED_PROJ_QTY: usize = 2; + assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); + + const EXPECTED_WIN_NAMES: [&str; 2] = ["w", "win"]; + for (i, win_name) in EXPECTED_WIN_NAMES.iter().enumerate() { assert!(matches!( expr_from_projection(&select.projection[i]), Expr::Function(Function { From b30b08ed18c5c75c71ad781a9d06424fa434ea03 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 7 Apr 2024 08:15:34 -0400 Subject: [PATCH 4/4] fmt --- tests/sqlparser_common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cead74d45..6e33dce9b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4187,7 +4187,7 @@ fn parse_named_window_functions() { const EXPECTED_PROJ_QTY: usize = 2; assert_eq!(EXPECTED_PROJ_QTY, select.projection.len()); - + const EXPECTED_WIN_NAMES: [&str; 2] = ["w", "win"]; for (i, win_name) in EXPECTED_WIN_NAMES.iter().enumerate() { assert!(matches!(