diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3cb5f8041..0882bc9dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -99,7 +99,7 @@ pub use self::query::{ JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + OrderBySort, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, diff --git a/src/ast/query.rs b/src/ast/query.rs index bbdd7540a..9b41ccc60 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2928,7 +2928,7 @@ impl fmt::Display for OrderBy { pub struct OrderByExpr { /// The expression to order by. pub expr: Expr, - /// Ordering options such as `ASC`/`DESC` and `NULLS` behavior. + /// Ordering options such as `ASC`/`DESC`/`USING ` and `NULLS` behavior. pub options: OrderByOptions, /// Optional `WITH FILL` clause (ClickHouse extension) which specifies how to fill gaps. pub with_fill: Option, @@ -2946,7 +2946,8 @@ 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)?; + write!(f, "{}", self.options)?; if let Some(ref with_fill) = self.with_fill { write!(f, " {with_fill}")? } @@ -3021,22 +3022,47 @@ impl fmt::Display for InterpolateExpr { } } -#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +/// The sort order for an `ORDER BY` expression. +/// +/// See PostgreSQL `USING` operator: +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -/// Options for an `ORDER BY` expression (ASC/DESC and NULLS FIRST/LAST). +pub enum OrderBySort { + /// `ASC` + Asc, + /// `DESC` + Desc, + /// PostgreSQL `USING ` ordering. + /// + /// See + Using(ObjectName), +} + +#[derive(Default, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Options for an `ORDER BY` expression. pub struct OrderByOptions { - /// Optional `ASC` (`Some(true)`) or `DESC` (`Some(false)`). - pub asc: Option, + /// Optional sort order: `ASC`, `DESC`, or `USING `. + pub sort: Option, /// Optional `NULLS FIRST` (`Some(true)`) or `NULLS LAST` (`Some(false)`). pub nulls_first: Option, } impl fmt::Display for OrderByOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.asc { - Some(true) => write!(f, " ASC")?, - Some(false) => write!(f, " DESC")?, + match &self.sort { + Some(OrderBySort::Asc) => write!(f, " ASC")?, + Some(OrderBySort::Desc) => write!(f, " DESC")?, + Some(OrderBySort::Using(op)) => { + if op.0.len() > 1 { + write!(f, " USING OPERATOR({op})")?; + } else { + write!(f, " USING {op}")?; + } + } None => (), } match self.nulls_first { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ab6cb15e..4f0644776 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1404,6 +1404,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 fda676eb2..c40d6d674 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 668c520e5..2f6d42211 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1378,7 +1378,7 @@ impl<'a> Parser<'a> { } let alias = self.parse_optional_alias_inner(None, validator)?; let order_by = OrderByOptions { - asc: self.parse_asc_desc(), + sort: self.parse_optional_order_by_sort(), nulls_first: None, }; Ok(ExprWithAliasAndOrderBy { @@ -18778,6 +18778,15 @@ impl<'a> Parser<'a> { } } + /// Parse ASC or DESC and map to [OrderBySort]. + fn parse_optional_order_by_sort(&mut self) -> Option { + match self.parse_asc_desc() { + Some(true) => Some(OrderBySort::Asc), + Some(false) => Some(OrderBySort::Desc), + None => None, + } + } + /// Parse an [OrderByExpr] expression. pub fn parse_order_by_expr(&mut self) -> Result { self.parse_order_by_expr_inner(false) @@ -18814,7 +18823,18 @@ impl<'a> Parser<'a> { None }; - let options = self.parse_order_by_options()?; + let options = if !with_operator_class + && self.dialect.supports_order_by_using_operator() + && self.parse_keyword(Keyword::USING) + { + let op = self.parse_order_by_using_operator()?; + OrderByOptions { + sort: Some(OrderBySort::Using(op)), + nulls_first: self.parse_null_ordering_modifier(), + } + } else { + self.parse_order_by_options()? + }; let with_fill = if self.dialect.supports_with_fill() && self.parse_keywords(&[Keyword::WITH, Keyword::FILL]) @@ -18834,18 +18854,33 @@ 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 { + if self.parse_keyword(Keyword::OPERATOR) { + self.expect_token(&Token::LParen)?; + let operator_name = self.parse_operator_name()?; + self.expect_token(&Token::RParen)?; + return Ok(operator_name); + } + + let token = self.next_token(); + Ok(ObjectName::from(vec![Ident::new(token.token.to_string())])) + } - let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { + fn parse_null_ordering_modifier(&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 sort = self.parse_optional_order_by_sort(); + let nulls_first = self.parse_null_ordering_modifier(); - Ok(OrderByOptions { asc, nulls_first }) + Ok(OrderByOptions { sort, nulls_first }) } // Parse a WITH FILL clause (ClickHouse dialect) @@ -21096,7 +21131,7 @@ mod tests { column: OrderByExpr { expr: Expr::Identifier(name.into()), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index afb619082..212607e0c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2731,7 +2731,7 @@ fn test_export_data() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("field1")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2837,7 +2837,7 @@ fn test_export_data() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("field1")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f16a1f8e4..716a3919f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -390,7 +390,7 @@ fn parse_alter_table_add_projection() { kind: OrderByKind::Expressions(vec![OrderByExpr { expr: Identifier(Ident::new("b")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -1218,7 +1218,7 @@ fn parse_select_order_by_with_fill_interpolate() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: Some(WithFill { @@ -1230,7 +1230,7 @@ fn parse_select_order_by_with_fill_interpolate() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: Some(WithFill { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 739238c82..326fbf678 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2596,7 +2596,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2604,7 +2604,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -2612,7 +2612,7 @@ fn parse_select_order_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("id")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2637,7 +2637,7 @@ fn parse_select_order_by_limit() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2645,7 +2645,7 @@ fn parse_select_order_by_limit() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -2675,63 +2675,63 @@ fn parse_select_order_by_all() { ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL NULLS LAST", OrderByKind::All(OrderByOptions { - asc: None, + sort: None, nulls_first: Some(false), }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer ORDER BY ALL ASC NULLS LAST", OrderByKind::All(OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(false), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS FIRST", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(true), }), ), ( "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY ALL DESC NULLS LAST", OrderByKind::All(OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }), ), @@ -2758,7 +2758,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2769,7 +2769,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: None, @@ -2780,7 +2780,7 @@ fn parse_select_order_by_not_support_all() { OrderByKind::Expressions(vec![OrderByExpr { expr: Expr::Identifier(Ident::new("ALL")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: None, @@ -2803,7 +2803,7 @@ fn parse_select_order_by_nulls_order() { OrderByExpr { expr: Expr::Identifier(Ident::new("lname")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: Some(true), }, with_fill: None, @@ -2811,7 +2811,7 @@ fn parse_select_order_by_nulls_order() { OrderByExpr { expr: Expr::Identifier(Ident::new("fname")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: Some(false), }, with_fill: None, @@ -2827,6 +2827,67 @@ fn parse_select_order_by_nulls_order() { assert_eq!(Some(expected_limit_clause), select.limit_clause); } +#[test] +fn parse_aggregate_order_by_using_operator() { + let sql = "SELECT aggfns(DISTINCT a, a, c ORDER BY c USING ~<~, a) FROM t"; + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + let select = dialects.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].options.sort, + Some(OrderBySort::Using(ObjectName::from(vec!["~<~".into()]))) + ); + assert_eq!(order_by_exprs[1].options.sort, None); +} + +#[test] +fn parse_order_by_using_operator_syntax() { + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + dialects.one_statement_parses_to( + "SELECT a FROM t ORDER BY a USING OPERATOR(<)", + "SELECT a FROM t ORDER BY a USING <", + ); + + let query = dialects + .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].options.sort, + Some(OrderBySort::Using(ObjectName::from(vec![ + Ident::new("pg_catalog"), + Ident::new("<"), + ]))) + ); + assert_eq!(exprs[0].options.nulls_first, Some(false)); +} + +#[test] +fn parse_order_by_using_operator_invalid_cases() { + let dialects = all_dialects_where(|d| d.supports_order_by_using_operator()); + let err = dialects + .parse_sql_statements("SELECT a FROM t ORDER BY a USING OPERATOR();") + .unwrap_err(); + assert!(matches!(err, ParserError::ParserError(_))); +} + #[test] fn parse_select_group_by() { let sql = "SELECT id, fname, lname FROM customer GROUP BY lname, fname"; @@ -3033,7 +3094,7 @@ fn parse_select_qualify() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3482,7 +3543,7 @@ fn parse_listagg() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3494,7 +3555,7 @@ fn parse_listagg() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -5753,7 +5814,7 @@ fn parse_window_functions() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("dt")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, @@ -5979,7 +6040,7 @@ fn test_parse_named_window() { span: Span::empty(), }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -9494,7 +9555,7 @@ fn parse_create_index() { expr: Expr::Identifier(Ident::new("name")), with_fill: None, options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, }, @@ -9505,7 +9566,7 @@ fn parse_create_index() { expr: Expr::Identifier(Ident::new("age")), with_fill: None, options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, }, @@ -9540,7 +9601,7 @@ fn test_create_index_with_using_function() { expr: Expr::Identifier(Ident::new("name")), with_fill: None, options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, }, @@ -9551,7 +9612,7 @@ fn test_create_index_with_using_function() { expr: Expr::Identifier(Ident::new("age")), with_fill: None, options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, }, @@ -9596,7 +9657,7 @@ fn test_create_index_with_with_clause() { column: OrderByExpr { expr: Expr::Identifier(Ident::new("title")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -13276,7 +13337,7 @@ fn test_match_recognize() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("price_date")), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 1b0948518..c0a15d5b9 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -23,7 +23,8 @@ use sqlparser::ast::{ ClusteredBy, CommentDef, CreateFunction, CreateFunctionBody, CreateFunctionUsing, CreateTable, Expr, Function, FunctionArgumentList, FunctionArguments, Ident, ObjectName, OrderByExpr, - OrderByOptions, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, Value, + OrderByOptions, OrderBySort, SelectItem, Set, Statement, TableFactor, UnaryOperator, Use, + Value, }; use sqlparser::dialect::{AnsiDialect, GenericDialect, HiveDialect}; use sqlparser::parser::ParserError; @@ -171,7 +172,7 @@ fn create_table_with_clustered_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("a")), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -179,7 +180,7 @@ fn create_table_with_clustered_by() { OrderByExpr { expr: Expr::Identifier(Ident::new("b")), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5c4f4a8c3..a511c0dc8 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -691,7 +691,7 @@ fn table_constraint_unique_primary_ctor( column: OrderByExpr { expr: Expr::Identifier(ident), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -2813,7 +2813,7 @@ fn parse_update_with_order_by() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2840,7 +2840,7 @@ fn parse_update_with_order_by_and_limit() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(true), + sort: Some(OrderBySort::Asc), nulls_first: None, }, with_fill: None, @@ -2866,7 +2866,7 @@ fn parse_delete_with_order_by() { span: Span::empty(), }), options: OrderByOptions { - asc: Some(false), + sort: Some(OrderBySort::Desc), nulls_first: None, }, with_fill: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef..3c1e34969 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2943,7 +2943,7 @@ fn parse_create_indices_with_operator_classes() { within_group: vec![], }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None, @@ -3007,7 +3007,7 @@ fn parse_create_indices_with_operator_classes() { span: Span::empty() }), options: OrderByOptions { - asc: None, + sort: None, nulls_first: None, }, with_fill: None,