Named window frames (#881)
* after over clause, named window can be parsed with window ... as after having clause
* Lint errors are fixed
* Support for multiple windows
* fix lint errors
* simplifications
* rename function
* Rewrite named window search in functional style
* Test added and some minor changes
* Minor changes on tests and namings, and semantic check is removed
---------
Co-authored-by: Mustafa Akur <mustafa.akur@synnada.ai>
Co-authored-by: Mehmet Ozan Kabak <ozankabak@gmail.com>
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 14cf04f..ee3366a 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -17,7 +17,7 @@
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::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,
@@ -930,6 +930,23 @@
}
}
+#[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))]
@@ -3360,7 +3377,7 @@
pub struct Function {
pub name: ObjectName,
pub args: Vec<FunctionArg>,
- pub over: Option<WindowSpec>,
+ pub over: Option<WindowType>,
// aggregate functions may specify eg `COUNT(DISTINCT x)`
pub distinct: bool,
// Some functions must be called without trailing parentheses, for example Postgres
@@ -3409,7 +3426,7 @@
)?;
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 a85c62a..a709e10 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -216,6 +216,8 @@
pub sort_by: Vec<Expr>,
/// HAVING
pub having: Option<Expr>,
+ /// WINDOW AS
+ pub named_window: Vec<NamedWindowDefinition>,
/// QUALIFY (Snowflake)
pub qualify: Option<Expr>,
}
@@ -269,6 +271,9 @@
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}")?;
}
@@ -314,6 +319,17 @@
#[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))]
pub struct With {
pub recursive: bool,
pub cte_tables: Vec<Cte>,
diff --git a/src/keywords.rs b/src/keywords.rs
index 6132b31..e73b89a 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -688,6 +688,7 @@
Keyword::OUTER,
Keyword::SET,
Keyword::QUALIFY,
+ Keyword::WINDOW,
];
/// Can't be used as a column alias, so that `SELECT <expr> alias`
diff --git a/src/parser.rs b/src/parser.rs
index 9e593eb..bab5814 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -884,32 +884,12 @@
let distinct = self.parse_all_or_distinct()?.is_some();
let (args, order_by) = self.parse_optional_args_with_orderby()?;
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)?
+ if self.consume_token(&Token::LParen) {
+ let window_spec = self.parse_window_spec()?;
+ Some(WindowType::WindowSpec(window_spec))
} 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
- };
-
- Some(WindowSpec {
- partition_by,
- order_by,
- window_frame,
- })
+ Some(WindowType::NamedWindow(self.parse_identifier()?))
+ }
} else {
None
};
@@ -5367,6 +5347,12 @@
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 {
@@ -5386,6 +5372,7 @@
distribute_by,
sort_by,
having,
+ named_window: named_windows,
qualify,
})
}
@@ -7033,6 +7020,39 @@
pub fn index(&self) -> usize {
self.index
}
+
+ pub fn parse_named_window(&mut self) -> Result<NamedWindowDefinition, ParserError> {
+ 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<WindowSpec, ParserError> {
+ 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 23a3996..24c6415 100644
--- a/tests/sqlparser_clickhouse.rs
+++ b/tests/sqlparser_clickhouse.rs
@@ -101,6 +101,7 @@
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 fadefcf..a5458ca 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::*;
@@ -251,6 +250,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None
}))),
order_by: vec![],
@@ -1721,7 +1721,7 @@
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")),
@@ -1729,7 +1729,7 @@
nulls_first: None,
}],
window_frame: None,
- }),
+ })),
distinct: false,
special: false,
order_by: vec![],
@@ -3295,7 +3295,7 @@
&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")),
@@ -3303,7 +3303,7 @@
nulls_first: None,
}],
window_frame: None,
- }),
+ })),
distinct: false,
special: false,
order_by: vec![],
@@ -3313,6 +3313,128 @@
}
#[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";
let _ast = verified_only_select(sql);
@@ -3659,6 +3781,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None,
}))),
order_by: vec![],
@@ -5929,6 +6052,7 @@
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 ba6f2b5..3e5c810 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -460,6 +460,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None
}))),
order_by: vec![],
@@ -494,6 +495,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None
}))),
order_by: vec![],
@@ -884,6 +886,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None,
})))
);
@@ -927,6 +930,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None,
})))
);
@@ -1116,6 +1120,7 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None
}))),
order_by: vec![],
@@ -1394,6 +1399,7 @@
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 94e5d23..80a4261 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -954,6 +954,7 @@
selection: None,
group_by: vec![],
having: None,
+ named_window: vec![],
cluster_by: vec![],
distribute_by: vec![],
sort_by: vec![],
@@ -1799,6 +1800,7 @@
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 @@
distribute_by: vec![],
sort_by: vec![],
having: None,
+ named_window: vec![],
qualify: None,
}))),
}),