diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4c69d3ed0..b52bdf846 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1161,6 +1161,26 @@ impl fmt::Display for WindowFrameUnits { } } +/// Specifies Ignore / Respect NULL within window functions. +/// For example +/// `FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1)` +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum NullTreatment { + IgnoreNulls, + RespectNulls, +} + +impl fmt::Display for NullTreatment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + NullTreatment::IgnoreNulls => "IGNORE NULLS", + NullTreatment::RespectNulls => "RESPECT NULLS", + }) + } +} + /// Specifies [WindowFrame]'s `start_bound` and `end_bound` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3757,6 +3777,8 @@ pub struct Function { pub args: Vec, /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` pub filter: Option>, + // Snowflake/MSSQL supports diffrent options for null treatment in rank functions + pub null_treatment: Option, pub over: Option, // aggregate functions may specify eg `COUNT(DISTINCT x)` pub distinct: bool, @@ -3809,6 +3831,10 @@ impl fmt::Display for Function { write!(f, " FILTER (WHERE {filter_cond})")?; } + if let Some(o) = &self.null_treatment { + write!(f, " {o}")?; + } + if let Some(o) = &self.over { write!(f, " OVER {o}")?; } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 4e025f962..99db16107 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -506,6 +506,7 @@ where /// *expr = Expr::Function(Function { /// name: ObjectName(vec![Ident::new("f")]), /// args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(old_expr))], +/// null_treatment: None, /// filter: None, over: None, distinct: false, special: false, order_by: vec![], /// }); /// } diff --git a/src/keywords.rs b/src/keywords.rs index 405203601..dec324cfb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -518,6 +518,7 @@ define_keywords!( REPLACE, REPLICATION, RESET, + RESPECT, RESTRICT, RESULT, RETAIN, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a1323f7a8..4a465ec99 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -785,6 +785,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name: ObjectName(vec![w.to_ident()]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -987,6 +988,19 @@ impl<'a> Parser<'a> { } else { None }; + let null_treatment = match self.parse_one_of_keywords(&[Keyword::RESPECT, Keyword::IGNORE]) + { + Some(keyword) => { + self.expect_keyword(Keyword::NULLS)?; + + match keyword { + Keyword::RESPECT => Some(NullTreatment::RespectNulls), + Keyword::IGNORE => Some(NullTreatment::IgnoreNulls), + _ => None, + } + } + None => None, + }; let over = if self.parse_keyword(Keyword::OVER) { if self.consume_token(&Token::LParen) { let window_spec = self.parse_window_spec()?; @@ -1000,6 +1014,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + null_treatment, filter, over, distinct, @@ -1018,6 +1033,7 @@ impl<'a> Parser<'a> { Ok(Expr::Function(Function { name, args, + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index fe95b1873..e72a99b49 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -564,6 +564,7 @@ fn parse_map_access_offset() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( number("0") ))),], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 7d9cb0309..e7c85c2a3 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -50,6 +50,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("endpoint".to_string()) ))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -90,6 +91,7 @@ fn parse_map_access_expr() { Value::SingleQuotedString("app".to_string()) ))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -140,6 +142,7 @@ fn parse_array_fn() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x1")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(Ident::new("x2")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -199,6 +202,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9eb52f6ec..5eb70b09b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -875,6 +875,7 @@ fn parse_select_count_wildcard() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + null_treatment: None, filter: None, over: None, distinct: false, @@ -896,6 +897,7 @@ fn parse_select_count_distinct() { op: UnaryOperator::Plus, expr: Box::new(Expr::Identifier(Ident::new("x"))), }))], + null_treatment: None, filter: None, over: None, distinct: true, @@ -1864,6 +1866,7 @@ fn parse_select_having() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("COUNT")]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Wildcard)], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1890,6 +1893,7 @@ fn parse_select_qualify() { left: Box::new(Expr::Function(Function { name: ObjectName(vec![Ident::new("ROW_NUMBER")]), args: vec![], + null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![Expr::Identifier(Ident::new("p"))], @@ -2287,6 +2291,45 @@ fn parse_agg_with_order_by() { } } +#[test] +fn parse_window_rank_function() { + let supported_dialects = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + Box::new(HiveDialect {}), + Box::new(SnowflakeDialect {}), + ], + options: None, + }; + + for sql in [ + "SELECT column1, column2, FIRST_VALUE(column2) OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT column1, column2, FIRST_VALUE(column2) OVER (ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT col_1, col_2, LAG(col_2) OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) OVER (PARTITION BY col_3 ORDER BY col_1)", + ] { + supported_dialects.verified_stmt(sql); + } + + let supported_dialects_nulls = TestedDialects { + dialects: vec![Box::new(MsSqlDialect {}), Box::new(SnowflakeDialect {})], + options: None, + }; + + for sql in [ + "SELECT column1, column2, FIRST_VALUE(column2) IGNORE NULLS OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT column1, column2, FIRST_VALUE(column2) RESPECT NULLS OVER (PARTITION BY column1 ORDER BY column2 NULLS LAST) AS column2_first FROM t1", + "SELECT LAG(col_2, 1, 0) IGNORE NULLS OVER (ORDER BY col_1) FROM t1", + "SELECT LAG(col_2, 1, 0) RESPECT NULLS OVER (ORDER BY col_1) FROM t1", + ] { + supported_dialects_nulls.verified_stmt(sql); + } +} + #[test] fn parse_create_table() { let sql = "CREATE TABLE uk_cities (\ @@ -3346,6 +3389,7 @@ fn parse_scalar_function_in_projection() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("id")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -3466,6 +3510,7 @@ fn parse_named_argument_function() { ))), }, ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -3498,6 +3543,7 @@ fn parse_window_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("row_number")]), args: vec![], + null_treatment: None, filter: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], @@ -3542,6 +3588,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + null_treatment: None, filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window1".to_string(), @@ -3568,6 +3615,7 @@ fn test_parse_named_window() { quote_style: None, }), ))], + null_treatment: None, filter: None, over: Some(WindowType::NamedWindow(Ident { value: "window2".to_string(), @@ -4038,6 +4086,7 @@ fn parse_at_timezone() { quote_style: None, }]), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4066,6 +4115,7 @@ fn parse_at_timezone() { quote_style: None, },],), args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4078,6 +4128,7 @@ fn parse_at_timezone() { Value::SingleQuotedString("%Y-%m-%dT%H".to_string()), ),),), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4237,6 +4288,7 @@ fn parse_table_function() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( Value::SingleQuotedString("1".to_owned()), )))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4389,6 +4441,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4419,6 +4472,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("2")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("3")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -4431,6 +4485,7 @@ fn parse_unnest_in_from_clause() { FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("5")))), FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("6")))), ], + null_treatment: None, filter: None, over: None, distinct: false, @@ -6904,6 +6959,7 @@ fn parse_time_functions() { let select_localtime_func_call_ast = Function { name: ObjectName(vec![Ident::new(func_name)]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -7391,6 +7447,7 @@ fn parse_pivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("amount"),]) ))]), + null_treatment: None, filter: None, over: None, distinct: false, @@ -7541,6 +7598,7 @@ fn parse_pivot_unpivot_table() { args: (vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("population")) ))]), + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 534a224ea..66eef09e1 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -361,6 +361,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ebadf95f2..4aa993fa0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -334,6 +334,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8391bbadb..2788dfabe 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1112,6 +1112,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("description")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1126,6 +1127,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_create")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1140,6 +1142,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_read")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1154,6 +1157,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_update")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1168,6 +1172,7 @@ fn parse_insert_with_on_duplicate_update() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("perm_delete")) ))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -1558,6 +1563,7 @@ fn parse_table_colum_option_on_update() { option: ColumnOption::OnUpdate(Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_TIMESTAMP")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 64fcbd38a..18b5fe6f7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2275,6 +2275,7 @@ fn test_composite_value() { named: true } )))], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2437,6 +2438,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_CATALOG")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2449,6 +2451,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("CURRENT_USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2461,6 +2464,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("SESSION_USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2473,6 +2477,7 @@ fn parse_current_functions() { &Expr::Function(Function { name: ObjectName(vec![Ident::new("USER")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, @@ -2924,6 +2929,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 6238d1eca..6fa647d38 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -137,6 +137,7 @@ fn parse_delimited_identifiers() { &Expr::Function(Function { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], + null_treatment: None, filter: None, over: None, distinct: false, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7e6f18138..19a62b61d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -249,6 +249,7 @@ fn parse_delimited_identifiers() { name: ObjectName(vec![Ident::with_quote('"', "myfun")]), args: vec![], filter: None, + null_treatment: None, over: None, distinct: false, special: false, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index b657acddf..4935f1f50 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -359,6 +359,7 @@ fn parse_window_function_with_filter() { args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( Expr::Identifier(Ident::new("x")) ))], + null_treatment: None, over: Some(WindowType::WindowSpec(WindowSpec { partition_by: vec![], order_by: vec![],