Support named windows in OVER (window_definition) clause (#1166)

Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
This commit is contained in:
Nikita-str 2024-04-07 15:20:21 +03:00 committed by GitHub
parent 83c5d8191b
commit 23103302e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 2 deletions

View file

@ -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<Ident>,
/// `OVER (PARTITION BY ...)`
pub partition_by: Vec<Expr>,
/// `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,

View file

@ -9528,6 +9528,13 @@ impl<'a> Parser<'a> {
}
pub fn parse_window_spec(&mut self) -> Result<WindowSpec, ParserError> {
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,

View file

@ -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,

View file

@ -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,