From 7f313b071ff70414c7199115a9e5dba026bfce35 Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Wed, 16 Sep 2026 19:18:06 +0300 Subject: [PATCH] PostgreSQL: Support GROUP BY ALL and DISTINCT --- src/ast/mod.rs | 18 +++++++++--------- src/ast/query.rs | 36 +++++++++++++++++++++++++++++++++++- src/ast/spans.rs | 3 ++- src/dialect/mod.rs | 9 +++++++++ src/dialect/postgresql.rs | 4 ++++ src/parser/mod.rs | 30 ++++++++++++++++++++++++++---- tests/sqlparser_common.rs | 5 +++-- tests/sqlparser_postgres.rs | 10 ++++++++++ 8 files changed, 98 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 20058b83ab..77d027481a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -97,15 +97,15 @@ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectByKind, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, - ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, - IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, - JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, - MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - OrderBySort, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, + ForJson, ForXml, FormatClause, GroupByExpr, GroupByModifier, GroupByWithModifier, + IdentWithAlias, IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, + JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, + JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, + MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, + NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, + OrderByOptions, OrderBySort, PipeOperator, PivotValueSource, ProjectionSelect, Query, + RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, + Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, diff --git a/src/ast/query.rs b/src/ast/query.rs index 296e4e8ca6..8c30fd26e1 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -594,7 +594,8 @@ impl fmt::Display for Select { SpaceOrNewline.fmt(f)?; self.group_by.fmt(f)?; } - GroupByExpr::Expressions(exprs, _) => { + GroupByExpr::Expressions(exprs, _) + | GroupByExpr::ExpressionsWithModifier(_, exprs, _) => { if !exprs.is_empty() { SpaceOrNewline.fmt(f)?; self.group_by.fmt(f)?; @@ -3801,6 +3802,26 @@ impl fmt::Display for GroupByWithModifier { } } +/// `ALL` or `DISTINCT` modifier for `GROUP BY` expressions. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum GroupByModifier { + /// Preserve duplicate grouping sets. + All, + /// Remove duplicate grouping sets. + Distinct, +} + +impl fmt::Display for GroupByModifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GroupByModifier::All => f.write_str("ALL"), + GroupByModifier::Distinct => f.write_str("DISTINCT"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3820,6 +3841,10 @@ pub enum GroupByExpr { All(Vec), /// `GROUP BY ` with optional modifiers. Expressions(Vec, Vec), + /// `GROUP BY ALL | DISTINCT ` with optional modifiers. + /// + /// [PostgreSQL]: + ExpressionsWithModifier(GroupByModifier, Vec, Vec), } impl fmt::Display for GroupByExpr { @@ -3841,6 +3866,15 @@ impl fmt::Display for GroupByExpr { } Ok(()) } + GroupByExpr::ExpressionsWithModifier(modifier, col_names, modifiers) => { + write!(f, "GROUP BY {modifier}")?; + SpaceOrNewline.fmt(f)?; + Indent(display_comma_separated(col_names)).fmt(f)?; + if !modifiers.is_empty() { + write!(f, " {}", display_separated(modifiers, " "))?; + } + Ok(()) + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7acbd7d0b4..25df97477b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1308,7 +1308,8 @@ impl Spanned for GroupByExpr { fn span(&self) -> Span { match self { GroupByExpr::All(_) => Span::empty(), - GroupByExpr::Expressions(exprs, _modifiers) => { + GroupByExpr::Expressions(exprs, _modifiers) + | GroupByExpr::ExpressionsWithModifier(_, exprs, _modifiers) => { union_spans(exprs.iter().map(|i| i.span())) } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7c4744c5a7..b02f0de58b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -383,6 +383,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `ALL` or `DISTINCT` before `GROUP BY` expressions. + fn supports_group_by_modifier(&self) -> bool { + false + } + /// Returns true if the dialects supports `GROUP BY` modifiers prefixed by a `WITH` keyword. /// Example: `GROUP BY value WITH ROLLUP`. fn supports_group_by_with_modifier(&self) -> bool { @@ -2099,6 +2104,10 @@ mod tests { self.0.supports_group_by_expr() } + fn supports_group_by_modifier(&self) -> bool { + self.0.supports_group_by_modifier() + } + fn supports_in_empty_list(&self) -> bool { self.0.supports_in_empty_list() } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 3bec6ceba3..f86f166f51 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -180,6 +180,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_group_by_modifier(&self) -> bool { + true + } + fn supports_alter_user_as_alter_role(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 15f135fffa..953115dbe1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13502,7 +13502,20 @@ impl<'a> Parser<'a> { /// Parse an optional `GROUP BY` clause, returning `Some(GroupByExpr)` when present. pub fn parse_optional_group_by(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) { - let expressions = if self.parse_keyword(Keyword::ALL) { + let modifier = if self.dialect.supports_group_by_modifier() { + if self.parse_keyword(Keyword::ALL) { + Some(GroupByModifier::All) + } else if self.parse_keyword(Keyword::DISTINCT) { + Some(GroupByModifier::Distinct) + } else { + None + } + } else { + None + }; + let expressions = if modifier.is_some() { + Some(self.parse_comma_separated(Parser::parse_group_by_expr)?) + } else if self.parse_keyword(Keyword::ALL) { None } else { Some(self.parse_comma_separated(Parser::parse_group_by_expr)?) @@ -13546,9 +13559,18 @@ impl<'a> Parser<'a> { result, ))); }; - let group_by = match expressions { - None => GroupByExpr::All(modifiers), - Some(exprs) => GroupByExpr::Expressions(exprs, modifiers), + let group_by = match (modifier, expressions) { + (None, None) => GroupByExpr::All(modifiers), + (Some(modifier), Some(exprs)) => { + GroupByExpr::ExpressionsWithModifier(modifier, exprs, modifiers) + } + (None, Some(exprs)) => GroupByExpr::Expressions(exprs, modifiers), + (Some(_), None) => { + return parser_err!( + "BUG: GROUP BY modifier requires expressions", + self.peek_token_ref().span.start + ) + } }; Ok(Some(group_by)) } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2de6062b28..0831bef1ab 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3158,10 +3158,11 @@ fn parse_select_group_by() { #[test] fn parse_select_group_by_all() { let sql = "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL"; - let select = verified_only_select(sql); + let dialects = all_dialects_where(|d| !d.supports_group_by_modifier()); + let select = dialects.verified_only_select(sql); assert_eq!(GroupByExpr::All(vec![]), select.group_by); - one_statement_parses_to( + dialects.one_statement_parses_to( "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL", "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL", ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d71e49b27a..cc1dbf229d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9953,3 +9953,13 @@ fn parse_insert_by_name_keywords_as_table_and_alias() { statement => panic!("Expected INSERT statement, got: {statement:?}"), } } + +#[test] +fn parse_group_by_modifier() { + for modifier in ["ALL", "DISTINCT"] { + pg().verified_stmt(&format!("SELECT a FROM t GROUP BY {modifier} a")); + assert!(pg() + .parse_sql_statements(&format!("SELECT a FROM t GROUP BY {modifier}")) + .is_err()); + } +}