diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b3bf3f62..dfdc86e06 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1246,11 +1246,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 ...)` @@ -1262,7 +1270,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 fcb3e3391..568d89e36 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9528,6 +9528,13 @@ 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::NoKeyword => { + self.maybe_parse(|parser| parser.parse_identifier(false)) + } + _ => None, + }; + let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { self.parse_comma_separated(Parser::parse_expr)? } else { @@ -9538,6 +9545,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)?; @@ -9546,6 +9554,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 f78eda0cc..6e33dce9b 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")), @@ -4122,7 +4123,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")]), @@ -4130,6 +4134,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")), @@ -4144,6 +4149,66 @@ fn parse_window_functions() { }), expr_from_projection(&select.projection[0]) ); + + for i in 0..EXPECTED_PROJ_QTY { + 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 supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + ], + 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); + + 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 { + over: Some(WindowType::WindowSpec(WindowSpec { + window_name: Some(Ident { value, .. }), + .. + })), + .. + }) 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] @@ -4244,6 +4309,7 @@ fn test_parse_named_window() { quote_style: None, }, WindowSpec { + window_name: None, partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident { @@ -4262,6 +4328,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 0352b4ec6..c9d5d98cd 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -446,6 +446,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,