diff --git a/src/ast/query.rs b/src/ast/query.rs index 440928ed7..b0e68de51 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2860,6 +2860,8 @@ impl fmt::Display for OrderBy { pub struct OrderByExpr { /// The expression to order by. pub expr: Expr, + /// Optional PostgreSQL `USING ` clause. + pub using_operator: Option, /// Ordering options such as `ASC`/`DESC` and `NULLS` behavior. pub options: OrderByOptions, /// Optional `WITH FILL` clause (ClickHouse extension) which specifies how to fill gaps. @@ -2870,6 +2872,7 @@ impl From for OrderByExpr { fn from(ident: Ident) -> Self { OrderByExpr { expr: Expr::Identifier(ident), + using_operator: None, options: OrderByOptions::default(), with_fill: None, } @@ -2878,7 +2881,15 @@ impl From for OrderByExpr { impl fmt::Display for OrderByExpr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", self.expr, self.options)?; + write!(f, "{}", self.expr)?; + if let Some(using_operator) = &self.using_operator { + if using_operator.0.len() > 1 { + write!(f, " USING OPERATOR({using_operator})")?; + } else { + write!(f, " USING {using_operator}")?; + } + } + write!(f, "{}", self.options)?; if let Some(ref with_fill) = self.with_fill { write!(f, " {with_fill}")? } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7c0751976..c09a2c054 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2095,6 +2095,7 @@ impl Spanned for OrderByExpr { fn span(&self) -> Span { let OrderByExpr { expr, + using_operator: _, options: _, with_fill, } = self; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8703e402c..ab01a4e37 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1345,6 +1345,14 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports PostgreSQL-style ordering operators: + /// `ORDER BY expr USING `. + /// + /// For example: `SELECT * FROM t ORDER BY a USING <`. + fn supports_order_by_using_operator(&self) -> bool { + false + } + /// Returns true if the dialect supports `SET NAMES [COLLATE ]`. /// /// - [MySQL](https://dev.mysql.com/doc/refman/8.4/en/set-names.html) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index b99a8b5c3..3f9c58bd7 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -278,6 +278,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_order_by_using_operator(&self) -> bool { + true + } + fn supports_set_names(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2749969c0..f9268fac0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18156,7 +18156,32 @@ impl<'a> Parser<'a> { None }; - let options = self.parse_order_by_options()?; + let using_operator = if !with_operator_class + && self.dialect.supports_order_by_using_operator() + && self.parse_keyword(Keyword::USING) + { + Some(self.parse_order_by_using_operator()?) + } else { + None + }; + + let options = if using_operator.is_some() { + if self + .peek_one_of_keywords(&[Keyword::ASC, Keyword::DESC]) + .is_some() + { + return parser_err!( + "ASC/DESC cannot be used together with USING in ORDER BY".to_string(), + self.peek_token_ref().span.start + ); + } + OrderByOptions { + asc: None, + nulls_first: self.parse_order_by_nulls_first_last(), + } + } else { + self.parse_order_by_options()? + }; let with_fill = if self.dialect.supports_with_fill() && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) @@ -18169,6 +18194,7 @@ impl<'a> Parser<'a> { Ok(( OrderByExpr { expr, + using_operator, options, with_fill, }, @@ -18176,16 +18202,53 @@ impl<'a> Parser<'a> { )) } - fn parse_order_by_options(&mut self) -> Result { - let asc = self.parse_asc_desc(); + fn parse_order_by_using_operator(&mut self) -> Result { + let dialect = self.dialect; - let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { + if self.parse_keyword(Keyword::OPERATOR) { + self.expect_token(&Token::LParen)?; + let operator_name = self.parse_operator_name()?; + let Some(last_part) = operator_name.0.last() else { + return self.expected_ref("an operator name", self.peek_token_ref()); + }; + let operator = last_part.to_string(); + if operator.is_empty() + || !operator + .chars() + .all(|ch| dialect.is_custom_operator_part(ch)) + { + return self.expected_ref("an operator name", self.peek_token_ref()); + } + self.expect_token(&Token::RParen)?; + return Ok(operator_name); + } + + let token = self.next_token(); + let operator = token.token.to_string(); + if !operator.is_empty() + && operator + .chars() + .all(|ch| dialect.is_custom_operator_part(ch)) + { + Ok(ObjectName::from(vec![Ident::new(operator)])) + } else { + self.expected_ref("an ordering operator after USING", &token) + } + } + + fn parse_order_by_nulls_first_last(&mut self) -> Option { + if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { Some(true) } else if self.parse_keywords(&[Keyword::NULLS, Keyword::LAST]) { Some(false) } else { None - }; + } + } + + fn parse_order_by_options(&mut self) -> Result { + let asc = self.parse_asc_desc(); + let nulls_first = self.parse_order_by_nulls_first_last(); Ok(OrderByOptions { asc, nulls_first }) } @@ -20399,6 +20462,7 @@ mod tests { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, operator_class: None, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ce962cb80..208dee652 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2721,6 +2721,7 @@ fn test_export_data() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, },]), interpolate: None, @@ -2827,6 +2828,7 @@ fn test_export_data() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, },]), interpolate: None, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 82f79577b..69624fabe 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -334,6 +334,7 @@ fn parse_alter_table_add_projection() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }]), interpolate: None, @@ -1162,6 +1163,7 @@ fn parse_select_order_by_with_fill_interpolate() { asc: Some(true), nulls_first: Some(true), }, + using_operator: None, with_fill: Some(WithFill { from: Some(Expr::value(number("10"))), to: Some(Expr::value(number("20"))), @@ -1174,6 +1176,7 @@ fn parse_select_order_by_with_fill_interpolate() { asc: Some(false), nulls_first: Some(false), }, + using_operator: None, with_fill: Some(WithFill { from: Some(Expr::value(number("30"))), to: Some(Expr::value(number("40"))), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7bf276407..a592dd86a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2577,6 +2577,7 @@ fn parse_select_order_by() { asc: Some(true), nulls_first: None, }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -2585,6 +2586,7 @@ fn parse_select_order_by() { asc: Some(false), nulls_first: None, }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -2593,6 +2595,7 @@ fn parse_select_order_by() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, ]), @@ -2618,6 +2621,7 @@ fn parse_select_order_by_limit() { asc: Some(true), nulls_first: None, }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -2626,6 +2630,7 @@ fn parse_select_order_by_limit() { asc: Some(false), nulls_first: None, }, + using_operator: None, with_fill: None, }, ]), @@ -2739,6 +2744,7 @@ fn parse_select_order_by_not_support_all() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }]), ), @@ -2750,6 +2756,7 @@ fn parse_select_order_by_not_support_all() { asc: Some(true), nulls_first: Some(true), }, + using_operator: None, with_fill: None, }]), ), @@ -2761,6 +2768,7 @@ fn parse_select_order_by_not_support_all() { asc: Some(false), nulls_first: Some(false), }, + using_operator: None, with_fill: None, }]), ), @@ -2784,6 +2792,7 @@ fn parse_select_order_by_nulls_order() { asc: Some(true), nulls_first: Some(true), }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -2792,6 +2801,7 @@ fn parse_select_order_by_nulls_order() { asc: Some(false), nulls_first: Some(false), }, + using_operator: None, with_fill: None, }, ]), @@ -3014,6 +3024,7 @@ fn parse_select_qualify() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }], window_frame: None, @@ -3459,6 +3470,7 @@ fn parse_listagg() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -3471,6 +3483,7 @@ fn parse_listagg() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, ] @@ -5730,6 +5743,7 @@ fn parse_window_functions() { asc: Some(false), nulls_first: None, }, + using_operator: None, with_fill: None, }], window_frame: None, @@ -5956,6 +5970,7 @@ fn test_parse_named_window() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }], window_frame: None, @@ -9440,6 +9455,7 @@ fn parse_create_index() { operator_class: None, column: OrderByExpr { expr: Expr::Identifier(Ident::new("name")), + using_operator: None, with_fill: None, options: OrderByOptions { asc: None, @@ -9451,6 +9467,7 @@ fn parse_create_index() { operator_class: None, column: OrderByExpr { expr: Expr::Identifier(Ident::new("age")), + using_operator: None, with_fill: None, options: OrderByOptions { asc: Some(false), @@ -9486,6 +9503,7 @@ fn test_create_index_with_using_function() { operator_class: None, column: OrderByExpr { expr: Expr::Identifier(Ident::new("name")), + using_operator: None, with_fill: None, options: OrderByOptions { asc: None, @@ -9497,6 +9515,7 @@ fn test_create_index_with_using_function() { operator_class: None, column: OrderByExpr { expr: Expr::Identifier(Ident::new("age")), + using_operator: None, with_fill: None, options: OrderByOptions { asc: Some(false), @@ -9547,6 +9566,7 @@ fn test_create_index_with_with_clause() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, operator_class: None, @@ -13173,6 +13193,7 @@ fn test_match_recognize() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }], measures: vec![ diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 1b0948518..1c9e114a2 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -174,6 +174,7 @@ fn create_table_with_clustered_by() { asc: Some(true), nulls_first: None, }, + using_operator: None, with_fill: None, }, OrderByExpr { @@ -182,6 +183,7 @@ fn create_table_with_clustered_by() { asc: Some(false), nulls_first: None, }, + using_operator: None, with_fill: None, }, ]), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 541f7df6e..a80088821 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -682,6 +682,7 @@ fn table_constraint_unique_primary_ctor( asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, operator_class: None, @@ -2761,6 +2762,7 @@ fn parse_delete_with_order_by() { asc: Some(false), nulls_first: None, }, + using_operator: None, with_fill: None, }], order_by diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7dd624a27..0111c086a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -24,7 +24,7 @@ mod test_utils; use helpers::attached_token::AttachedToken; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; use sqlparser::tokenizer::Span; use test_utils::*; @@ -2725,6 +2725,7 @@ fn parse_create_indices_with_operator_classes() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, operator_class: expected_operator_class.clone(), @@ -2789,6 +2790,7 @@ fn parse_create_indices_with_operator_classes() { asc: None, nulls_first: None, }, + using_operator: None, with_fill: None, }, operator_class: None @@ -5727,6 +5729,101 @@ fn parse_array_agg() { pg().verified_stmt(sql4); } +#[test] +fn parse_pg_aggregate_order_by_using_operator() { + let sql = "SELECT aggfns(DISTINCT a, a, c ORDER BY c USING ~<~, a) FROM t"; + let select = pg().verified_only_select(sql); + let SelectItem::UnnamedExpr(Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { clauses, .. }), + .. + })) = &select.projection[0] + else { + unreachable!("expected aggregate function in projection"); + }; + + let Some(FunctionArgumentClause::OrderBy(order_by_exprs)) = clauses + .iter() + .find(|clause| matches!(clause, FunctionArgumentClause::OrderBy(_))) + else { + unreachable!("expected ORDER BY clause in aggregate function argument list"); + }; + + assert_eq!( + order_by_exprs[0].using_operator, + Some(ObjectName::from(vec!["~<~".into()])) + ); + assert_eq!(order_by_exprs[1].using_operator, None); +} + +#[test] +fn parse_pg_order_by_using_operator_syntax() { + pg().one_statement_parses_to( + "SELECT a FROM t ORDER BY a USING OPERATOR(<)", + "SELECT a FROM t ORDER BY a USING <", + ); + + let query = + pg().verified_query("SELECT a FROM t ORDER BY a USING OPERATOR(pg_catalog.<) NULLS LAST"); + let order_by = query.order_by.expect("expected ORDER BY clause"); + let OrderByKind::Expressions(exprs) = order_by.kind else { + unreachable!("expected ORDER BY expressions"); + }; + + assert_eq!( + exprs[0].using_operator, + Some(ObjectName::from(vec![ + Ident::new("pg_catalog"), + Ident::new("<"), + ])) + ); + assert_eq!(exprs[0].options.asc, None); + assert_eq!(exprs[0].options.nulls_first, Some(false)); +} + +#[test] +fn parse_pg_order_by_using_operator_invalid_cases() { + let err = pg() + .parse_sql_statements("SELECT a FROM t ORDER BY a USING ;") + .unwrap_err(); + assert!( + matches!(err, ParserError::ParserError(msg) if msg.contains("an ordering operator after USING")) + ); + + let err = pg() + .parse_sql_statements("SELECT a FROM t ORDER BY a USING OPERATOR();") + .unwrap_err(); + assert!(matches!(err, ParserError::ParserError(msg) if msg.contains("an operator name"))); + + let err = pg() + .parse_sql_statements("SELECT a FROM t ORDER BY a USING < DESC;") + .unwrap_err(); + assert!( + matches!(err, ParserError::ParserError(msg) if msg.contains("ASC/DESC cannot be used together with USING in ORDER BY")) + ); + + #[derive(Debug)] + struct OrderByUsingDisabledDialect; + + impl Dialect for OrderByUsingDisabledDialect { + fn is_identifier_start(&self, ch: char) -> bool { + PostgreSqlDialect {}.is_identifier_start(ch) + } + + fn is_identifier_part(&self, ch: char) -> bool { + PostgreSqlDialect {}.is_identifier_part(ch) + } + + fn supports_order_by_using_operator(&self) -> bool { + false + } + } + + let without_order_by_using = TestedDialects::new(vec![Box::new(OrderByUsingDisabledDialect)]); + assert!(without_order_by_using + .parse_sql_statements("SELECT a FROM t ORDER BY a USING <;") + .is_err()); +} + #[test] fn parse_mat_cte() { let sql = r#"WITH cte AS MATERIALIZED (SELECT id FROM accounts) SELECT id FROM cte"#;