diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7b460c62a8..ca96702c80 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -17,7 +17,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::fmt; +use core::fmt::{self, Display}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -35,10 +35,10 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ Cte, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, IdentWithAlias, Join, - JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NonBlock, Offset, OffsetRows, - OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, - TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NamedWindowDefinition, + NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, + TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, @@ -917,6 +917,23 @@ impl fmt::Display for Expr { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum WindowType { + WindowSpec(WindowSpec), + NamedWindow(Ident), +} + +impl Display for WindowType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WindowType::WindowSpec(spec) => write!(f, "({})", spec), + WindowType::NamedWindow(name) => write!(f, "{}", name), + } + } +} + /// A window specification (i.e. `OVER (PARTITION BY .. ORDER BY .. etc.)`) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3343,7 +3360,7 @@ impl fmt::Display for CloseCursor { pub struct Function { pub name: ObjectName, pub args: Vec, - pub over: Option, + pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, // Some functions must be called without trailing parentheses, for example Postgres @@ -3384,7 +3401,7 @@ impl fmt::Display for Function { )?; if let Some(o) = &self.over { - write!(f, " OVER ({o})")?; + write!(f, " OVER {o}")?; } } diff --git a/src/ast/query.rs b/src/ast/query.rs index a85c62a25c..a709e101fd 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -216,6 +216,8 @@ pub struct Select { pub sort_by: Vec, /// HAVING pub having: Option, + /// WINDOW AS + pub named_window: Vec, /// QUALIFY (Snowflake) pub qualify: Option, } @@ -269,6 +271,9 @@ impl fmt::Display for Select { if let Some(ref having) = self.having { write!(f, " HAVING {having}")?; } + if !self.named_window.is_empty() { + write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?; + } if let Some(ref qualify) = self.qualify { write!(f, " QUALIFY {qualify}")?; } @@ -311,6 +316,17 @@ impl fmt::Display for LateralView { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct NamedWindowDefinition(pub Ident, pub WindowSpec); + +impl fmt::Display for NamedWindowDefinition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} AS ({})", self.0, self.1) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/keywords.rs b/src/keywords.rs index a0c5b68cba..c3418bb277 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -687,6 +687,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::OUTER, Keyword::SET, Keyword::QUALIFY, + Keyword::WINDOW, ]; /// Can't be used as a column alias, so that `SELECT alias` diff --git a/src/parser.rs b/src/parser.rs index 82cbe9d12c..148a262680 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -882,32 +882,12 @@ impl<'a> Parser<'a> { let distinct = self.parse_all_or_distinct()?.is_some(); let args = self.parse_optional_args()?; let over = if self.parse_keyword(Keyword::OVER) { - // TBD: support window names (`OVER mywin`) in place of inline specification - self.expect_token(&Token::LParen)?; - let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { - // a list of possibly-qualified column names - self.parse_comma_separated(Parser::parse_expr)? - } else { - vec![] - }; - let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { - self.parse_comma_separated(Parser::parse_order_by_expr)? - } else { - vec![] - }; - let window_frame = if !self.consume_token(&Token::RParen) { - let window_frame = self.parse_window_frame()?; - self.expect_token(&Token::RParen)?; - Some(window_frame) + if self.consume_token(&Token::LParen) { + let window_spec = self.parse_window_spec()?; + Some(WindowType::WindowSpec(window_spec)) } else { - None - }; - - Some(WindowSpec { - partition_by, - order_by, - window_frame, - }) + Some(WindowType::NamedWindow(self.parse_identifier()?)) + } } else { None }; @@ -5267,6 +5247,12 @@ impl<'a> Parser<'a> { None }; + let named_windows = if self.parse_keyword(Keyword::WINDOW) { + self.parse_comma_separated(Parser::parse_named_window)? + } else { + vec![] + }; + let qualify = if self.parse_keyword(Keyword::QUALIFY) { Some(self.parse_expr()?) } else { @@ -5286,6 +5272,7 @@ impl<'a> Parser<'a> { distribute_by, sort_by, having, + named_window: named_windows, qualify, }) } @@ -6915,6 +6902,39 @@ impl<'a> Parser<'a> { pub fn index(&self) -> usize { self.index } + + pub fn parse_named_window(&mut self) -> Result { + let ident = self.parse_identifier()?; + self.expect_keyword(Keyword::AS)?; + self.expect_token(&Token::LParen)?; + let window_spec = self.parse_window_spec()?; + Ok(NamedWindowDefinition(ident, window_spec)) + } + + pub fn parse_window_spec(&mut self) -> Result { + let partition_by = if self.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_expr)? + } else { + vec![] + }; + let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + self.parse_comma_separated(Parser::parse_order_by_expr)? + } else { + vec![] + }; + let window_frame = if !self.consume_token(&Token::RParen) { + let window_frame = self.parse_window_frame()?; + self.expect_token(&Token::RParen)?; + Some(window_frame) + } else { + None + }; + Ok(WindowSpec { + partition_by, + order_by, + window_frame, + }) + } } impl Word { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index bb0ec48fa6..fd80f6d939 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -99,6 +99,7 @@ fn parse_map_access_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }, select diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 16fd623dd5..269e37bda0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19,7 +19,6 @@ //! dialect-specific parsing rules). use matches::assert_matches; - use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::TableFactor::Pivot; use sqlparser::ast::*; @@ -249,6 +248,7 @@ fn parse_update_set_from() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -1715,7 +1715,7 @@ fn parse_select_qualify() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), args: vec![], - over: Some(WindowSpec { + over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![Expr::Identifier(Ident::new("p"))], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), @@ -1723,7 +1723,7 @@ fn parse_select_qualify() { nulls_first: None, }], window_frame: None, - }), + })), distinct: false, special: false, })), @@ -3259,7 +3259,7 @@ fn parse_window_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), args: vec![], - over: Some(WindowSpec { + over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), @@ -3267,7 +3267,7 @@ fn parse_window_functions() { nulls_first: None, }], window_frame: None, - }), + })), distinct: false, special: false, }), @@ -3275,6 +3275,128 @@ fn parse_window_functions() { ); } +#[test] +fn test_parse_named_window() { + let sql = "SELECT \ + MIN(c12) OVER window1 AS min1, \ + MAX(c12) OVER window2 AS max1 \ + FROM aggregate_test_100 \ + WINDOW window1 AS (ORDER BY C12), \ + window2 AS (PARTITION BY C11) \ + ORDER BY C3"; + let actual_select_only = verified_only_select(sql); + let expected = Select { + distinct: None, + top: None, + projection: vec![ + SelectItem::ExprWithAlias { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "MIN".to_string(), + quote_style: None, + }]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident { + value: "c12".to_string(), + quote_style: None, + }), + ))], + over: Some(WindowType::NamedWindow(Ident { + value: "window1".to_string(), + quote_style: None, + })), + distinct: false, + special: false, + }), + alias: Ident { + value: "min1".to_string(), + quote_style: None, + }, + }, + SelectItem::ExprWithAlias { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "MAX".to_string(), + quote_style: None, + }]), + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident { + value: "c12".to_string(), + quote_style: None, + }), + ))], + over: Some(WindowType::NamedWindow(Ident { + value: "window2".to_string(), + quote_style: None, + })), + distinct: false, + special: false, + }), + alias: Ident { + value: "max1".to_string(), + quote_style: None, + }, + }, + ], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "aggregate_test_100".to_string(), + quote_style: None, + }]), + alias: None, + args: None, + with_hints: vec![], + }, + joins: vec![], + }], + lateral_views: vec![], + selection: None, + group_by: vec![], + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![ + NamedWindowDefinition( + Ident { + value: "window1".to_string(), + quote_style: None, + }, + WindowSpec { + partition_by: vec![], + order_by: vec![OrderByExpr { + expr: Expr::Identifier(Ident { + value: "C12".to_string(), + quote_style: None, + }), + asc: None, + nulls_first: None, + }], + window_frame: None, + }, + ), + NamedWindowDefinition( + Ident { + value: "window2".to_string(), + quote_style: None, + }, + WindowSpec { + partition_by: vec![Expr::Identifier(Ident { + value: "C11".to_string(), + quote_style: None, + })], + order_by: vec![], + window_frame: None, + }, + ), + ], + qualify: None, + }; + assert_eq!(actual_select_only, expected); +} + #[test] fn parse_aggregate_with_group_by() { let sql = "SELECT a, COUNT(1), MIN(b), MAX(b) FROM foo GROUP BY a"; @@ -3622,6 +3744,7 @@ fn parse_interval_and_or_xor() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), order_by: vec![], @@ -5888,6 +6011,7 @@ fn parse_merge() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), order_by: vec![], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 1c479bb188..d9f6834e40 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -460,6 +460,7 @@ fn parse_quote_identifiers_2() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -494,6 +495,7 @@ fn parse_quote_identifiers_3() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -879,6 +881,7 @@ fn parse_select_with_numeric_prefix_column_name() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))) ); @@ -922,6 +925,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))) ); @@ -1111,6 +1115,7 @@ fn parse_substring_in_select() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None }))), order_by: vec![], @@ -1388,6 +1393,7 @@ fn parse_hex_string_introducer() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, into: None }))), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9de8e0eb4d..8761b97ae1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -954,6 +954,7 @@ fn parse_copy_to() { selection: None, group_by: vec![], having: None, + named_window: vec![], cluster_by: vec![], distribute_by: vec![], sort_by: vec![], @@ -1799,6 +1800,7 @@ fn parse_array_subquery_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), right: Box::new(SetExpr::Select(Box::new(Select { @@ -1820,6 +1822,7 @@ fn parse_array_subquery_expr() { distribute_by: vec![], sort_by: vec![], having: None, + named_window: vec![], qualify: None, }))), }),