From f01ccf264dffd092a476819a0ec47b2efb9f9cb0 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 25 Sep 2026 22:46:17 +0200 Subject: [PATCH 1/8] PostgreSQL: add support for CREATE AGGREGATE --- src/ast/ddl.rs | 162 +++++++++++++++++++++ src/ast/mod.rs | 44 ++++-- src/ast/spans.rs | 70 ++++++++- src/keywords.rs | 23 +++ src/parser/mod.rs | 241 ++++++++++++++++++++++++++++--- tests/sqlparser_postgres.rs | 274 ++++++++++++++++++++++++++++++++++++ 6 files changed, 772 insertions(+), 42 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 111439cf93..05417a6b0a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6078,3 +6078,165 @@ impl Spanned for CreateForeignTable { ) } } + +/// CREATE AGGREGATE statement. +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateAggregate { + /// True if `OR REPLACE` was specified. + pub or_replace: bool, + /// The aggregate name (can be schema-qualified). + pub name: ObjectName, + /// The argument list preceding the options list. + pub args: CreateAggregateArgs, + /// The options listed inside the required parentheses after the argument + /// list (e.g. `SFUNC`, `STYPE`, `FINALFUNC`, `PARALLEL`, …). + pub options: Vec, +} + +impl fmt::Display for CreateAggregate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + write!(f, " AGGREGATE {}", self.name)?; + match &self.args { + CreateAggregateArgs::Legacy => {} + CreateAggregateArgs::Star => write!(f, " (*)")?, + CreateAggregateArgs::List(args) => write!(f, " ({})", display_comma_separated(args))?, + } + write!(f, " ({})", display_comma_separated(&self.options)) + } +} + +/// The argument list of a [`CreateAggregate`] statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateAggregateArgs { + /// No argument-list parentheses: the historical form that packs everything, + /// usually including a `BASETYPE` option, into a single list. + Legacy, + /// The wildcard form `(*)`, used by zero-argument aggregates such as + /// `count(*)`. + Star, + /// An explicit argument list, possibly empty: `()`, `(NUMERIC)`, + /// `(input INT, VARIADIC tail TEXT)`. + List(Vec), +} + +impl From for crate::ast::Statement { + fn from(v: CreateAggregate) -> Self { + crate::ast::Statement::CreateAggregate(v) + } +} + +/// A single option in a `CREATE AGGREGATE` options list. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateAggregateOption { + /// `SFUNC = state_transition_function` + StateTransitionFunction(ObjectName), + /// `STYPE = state_data_type` + StateDataType(DataType), + /// `SSPACE = state_data_size` (in bytes) + StateDataSize(u64), + /// `FINALFUNC = final_function` + FinalFunction(ObjectName), + /// `FINALFUNC_EXTRA`. Passes extra dummy arguments to the final function. + FinalFunctionExtra, + /// `FINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + FinalFunctionModify(AggregateModifyKind), + /// `COMBINEFUNC = combine_function` + CombineFunction(ObjectName), + /// `SERIALFUNC = serial_function` + SerialFunction(ObjectName), + /// `DESERIALFUNC = deserial_function` + DeserialFunction(ObjectName), + /// `INITCOND = initial_condition` (a string literal) + InitialCondition(ValueWithSpan), + /// `MSFUNC = moving_state_transition_function` + MovingStateTransitionFunction(ObjectName), + /// `MINVFUNC = moving_inverse_transition_function` + MovingInverseTransitionFunction(ObjectName), + /// `MSTYPE = moving_state_data_type` + MovingStateDataType(DataType), + /// `MSSPACE = moving_state_data_size` (in bytes) + MovingStateDataSize(u64), + /// `MFINALFUNC = moving_final_function` + MovingFinalFunction(ObjectName), + /// `MFINALFUNC_EXTRA` + MovingFinalFunctionExtra, + /// `MFINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + MovingFinalFunctionModify(AggregateModifyKind), + /// `MINITCOND = moving_initial_condition` (a string literal) + MovingInitialCondition(ValueWithSpan), + /// `SORTOP = sort_operator` + SortOperator(ObjectName), + /// `PARALLEL = { SAFE | RESTRICTED | UNSAFE }` + Parallel(FunctionParallel), + /// `HYPOTHETICAL`. Marks the aggregate as hypothetical-set. + Hypothetical, + /// `BASETYPE = base_type` (old aggregate syntax). + BaseType(DataType), +} + +impl fmt::Display for CreateAggregateOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::StateTransitionFunction(name) => write!(f, "SFUNC = {name}"), + Self::StateDataType(data_type) => write!(f, "STYPE = {data_type}"), + Self::StateDataSize(size) => write!(f, "SSPACE = {size}"), + Self::FinalFunction(name) => write!(f, "FINALFUNC = {name}"), + Self::FinalFunctionExtra => write!(f, "FINALFUNC_EXTRA"), + Self::FinalFunctionModify(kind) => write!(f, "FINALFUNC_MODIFY = {kind}"), + Self::CombineFunction(name) => write!(f, "COMBINEFUNC = {name}"), + Self::SerialFunction(name) => write!(f, "SERIALFUNC = {name}"), + Self::DeserialFunction(name) => write!(f, "DESERIALFUNC = {name}"), + Self::InitialCondition(cond) => write!(f, "INITCOND = {cond}"), + Self::MovingStateTransitionFunction(name) => write!(f, "MSFUNC = {name}"), + Self::MovingInverseTransitionFunction(name) => write!(f, "MINVFUNC = {name}"), + Self::MovingStateDataType(data_type) => write!(f, "MSTYPE = {data_type}"), + Self::MovingStateDataSize(size) => write!(f, "MSSPACE = {size}"), + Self::MovingFinalFunction(name) => write!(f, "MFINALFUNC = {name}"), + Self::MovingFinalFunctionExtra => write!(f, "MFINALFUNC_EXTRA"), + Self::MovingFinalFunctionModify(kind) => write!(f, "MFINALFUNC_MODIFY = {kind}"), + Self::MovingInitialCondition(cond) => write!(f, "MINITCOND = {cond}"), + Self::SortOperator(name) => write!(f, "SORTOP = {name}"), + Self::Parallel(parallel) => write!(f, "PARALLEL = {}", parallel.as_str()), + Self::Hypothetical => write!(f, "HYPOTHETICAL"), + Self::BaseType(data_type) => write!(f, "BASETYPE = {data_type}"), + } + } +} + +/// Modifier kind for `FINALFUNC_MODIFY` / `MFINALFUNC_MODIFY` in `CREATE AGGREGATE`. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AggregateModifyKind { + /// The final function does not modify the transition state. + ReadOnly, + /// The transition state may be shared between aggregate calls. + Shareable, + /// The final function may modify the transition state. + ReadWrite, +} + +impl fmt::Display for AggregateModifyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ReadOnly => write!(f, "READ_ONLY"), + Self::Shareable => write!(f, "SHAREABLE"), + Self::ReadWrite => write!(f, "READ_WRITE"), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a899d3ec3a..c33ce6645b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -62,16 +62,17 @@ pub use self::dcl::{ SetConfigValue, Use, }; pub use self::ddl::{ - Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, - AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation, - AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, - AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, - AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, - AlterTextSearchOption, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, + AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, + AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, + AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, + AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, + AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, + AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, + AlterTextSearch, AlterTextSearchOperation, AlterTextSearchOption, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, + CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, @@ -3832,6 +3833,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/textsearch-intro.html) CreateTextSearch(CreateTextSearch), /// ```sql + /// CREATE AGGREGATE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createaggregate.html) + CreateAggregate(CreateAggregate), + /// ```sql /// ALTER TABLE /// ``` AlterTable(AlterTable), @@ -5670,6 +5676,7 @@ impl fmt::Display for Statement { create_operator_family.fmt(f) } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.fmt(f), + Statement::CreateAggregate(create_aggregate) => create_aggregate.fmt(f), Statement::CreateTextSearch(create_text_search) => create_text_search.fmt(f), Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { @@ -10315,16 +10322,23 @@ pub enum FunctionParallel { Safe, } -impl fmt::Display for FunctionParallel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl FunctionParallel { + /// Returns the bare keyword for this parallel mode, without the `PARALLEL` prefix. + pub fn as_str(&self) -> &'static str { match self { - FunctionParallel::Unsafe => write!(f, "PARALLEL UNSAFE"), - FunctionParallel::Restricted => write!(f, "PARALLEL RESTRICTED"), - FunctionParallel::Safe => write!(f, "PARALLEL SAFE"), + FunctionParallel::Unsafe => "UNSAFE", + FunctionParallel::Restricted => "RESTRICTED", + FunctionParallel::Safe => "SAFE", } } } +impl fmt::Display for FunctionParallel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PARALLEL {}", self.as_str()) + } +} + /// [BigQuery] Determinism specifier used in a UDF definition. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 945cbb0985..659ca158d4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -18,8 +18,8 @@ use crate::{ ast::{ ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, - ColumnOptions, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreateView, - ExportData, Owner, TypedString, + ColumnOptions, CreateAggregate, CreateAggregateArgs, CreateAggregateOption, CreateOperator, + CreateOperatorClass, CreateOperatorFamily, CreateView, ExportData, Owner, TypedString, }, tokenizer::TokenWithSpan, }; @@ -41,9 +41,9 @@ use super::{ MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, - PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, - RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, + Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, @@ -524,6 +524,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::CreateAggregate(create_aggregate) => create_aggregate.span(), } } } @@ -2532,6 +2533,65 @@ impl Spanned for AlterTable { } } +impl Spanned for CreateAggregate { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(core::iter::once(self.args.span())) + .chain(self.options.iter().map(|option| option.span())), + ) + } +} + +impl Spanned for CreateAggregateArgs { + fn span(&self) -> Span { + match self { + CreateAggregateArgs::Legacy | CreateAggregateArgs::Star => Span::empty(), + CreateAggregateArgs::List(args) => union_spans(args.iter().map(|arg| arg.span())), + } + } +} + +impl Spanned for OperateFunctionArg { + fn span(&self) -> Span { + union_spans( + self.name + .iter() + .map(|name| name.span) + .chain(self.default_expr.iter().map(|expr| expr.span())), + ) + } +} + +impl Spanned for CreateAggregateOption { + fn span(&self) -> Span { + match self { + CreateAggregateOption::StateTransitionFunction(name) + | CreateAggregateOption::FinalFunction(name) + | CreateAggregateOption::CombineFunction(name) + | CreateAggregateOption::SerialFunction(name) + | CreateAggregateOption::DeserialFunction(name) + | CreateAggregateOption::MovingStateTransitionFunction(name) + | CreateAggregateOption::MovingInverseTransitionFunction(name) + | CreateAggregateOption::MovingFinalFunction(name) + | CreateAggregateOption::SortOperator(name) => name.span(), + CreateAggregateOption::InitialCondition(value) + | CreateAggregateOption::MovingInitialCondition(value) => value.span, + CreateAggregateOption::StateDataType(_) + | CreateAggregateOption::StateDataSize(_) + | CreateAggregateOption::FinalFunctionExtra + | CreateAggregateOption::FinalFunctionModify(_) + | CreateAggregateOption::MovingStateDataType(_) + | CreateAggregateOption::MovingStateDataSize(_) + | CreateAggregateOption::MovingFinalFunctionExtra + | CreateAggregateOption::MovingFinalFunctionModify(_) + | CreateAggregateOption::Parallel(_) + | CreateAggregateOption::Hypothetical + | CreateAggregateOption::BaseType(_) => Span::empty(), + } + } +} + impl Spanned for CreateOperator { fn span(&self) -> Span { Span::empty() diff --git a/src/keywords.rs b/src/keywords.rs index 7b91eecba0..ede47e9345 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -150,6 +150,7 @@ define_keywords!( BACKUP, BACKWARD, BASE64, + BASETYPE, BASE_LOCATION, BEFORE, BEGIN, @@ -237,6 +238,7 @@ define_keywords!( COLUMN, COLUMNS, COLUMNSTORE, + COMBINEFUNC, COMMENT, COMMIT, COMMITTED, @@ -333,6 +335,7 @@ define_keywords!( DEREF, DESC, DESCRIBE, + DESERIALFUNC, DETACH, DETAIL, DETERMINISTIC, @@ -429,6 +432,9 @@ define_keywords!( FILL, FILTER, FINAL, + FINALFUNC, + FINALFUNC_EXTRA, + FINALFUNC_MODIFY, FIRST, FIRST_VALUE, FIXEDSTRING, @@ -495,6 +501,7 @@ define_keywords!( HOUR, HOURS, HUGEINT, + HYPOTHETICAL, IAM_ROLE, ICEBERG, ID, @@ -519,6 +526,7 @@ define_keywords!( INDICATOR, INHERIT, INHERITS, + INITCOND, INITIALIZE, INITIALLY, INNER, @@ -647,6 +655,9 @@ define_keywords!( METRIC, METRICS, MFA, + MFINALFUNC, + MFINALFUNC_EXTRA, + MFINALFUNC_MODIFY, MICROSECOND, MICROSECONDS, MILLENIUM, @@ -654,10 +665,12 @@ define_keywords!( MILLISECOND, MILLISECONDS, MIN, + MINITCOND, MINUS, MINUTE, MINUTES, MINVALUE, + MINVFUNC, MIN_ROWS, MOD, MODE, @@ -669,6 +682,9 @@ define_keywords!( MONTH, MONTHS, MSCK, + MSFUNC, + MSSPACE, + MSTYPE, MULTIRANGE_TYPE_NAME, MULTISET, MUTATION, @@ -843,6 +859,7 @@ define_keywords!( READ, READS, READ_ONLY, + READ_WRITE, REAL, RECEIVE, RECLUSTER, @@ -946,6 +963,7 @@ define_keywords!( SEQUENCES, SERDE, SERDEPROPERTIES, + SERIALFUNC, SERIALIZABLE, SERVER, SERVICE, @@ -956,7 +974,9 @@ define_keywords!( SETOF, SETS, SETTINGS, + SFUNC, SHARE, + SHAREABLE, SHARED, SHARING, SHOW, @@ -972,6 +992,7 @@ define_keywords!( SORT, SORTED, SORTKEY, + SORTOP, SOURCE, SPATIAL, SPECIFIC, @@ -988,6 +1009,7 @@ define_keywords!( SQL_SMALL_RESULT, SQRT, SRID, + SSPACE, STABLE, STAGE, START, @@ -1014,6 +1036,7 @@ define_keywords!( STRICT, STRING, STRUCT, + STYPE, SUBMULTISET, SUBSCRIPT, SUBSTR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9ae1de545b..c052ddcbb0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5337,13 +5337,15 @@ impl<'a> Parser<'a> { self.parse_create_secret(or_replace, temporary, persistent) } else if self.parse_keyword(Keyword::USER) { self.parse_create_user(or_replace).map(Into::into) + } else if self.parse_keyword(Keyword::AGGREGATE) { + self.parse_create_aggregate(or_replace).map(Into::into) } else if self.parse_keyword(Keyword::SCHEMA) { self.parse_create_schema(or_replace) } else if self.parse_keyword(Keyword::WAREHOUSE) { self.parse_create_warehouse(or_replace).map(Into::into) } else if or_replace { self.expected_ref( - "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or SCHEMA or WAREHOUSE after CREATE OR REPLACE", + "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or AGGREGATE or SCHEMA or WAREHOUSE after CREATE OR REPLACE", self.peek_token_ref(), ) } else if self.parse_keyword(Keyword::EXTENSION) { @@ -5974,16 +5976,7 @@ impl<'a> Parser<'a> { body.called_on_null = Some(FunctionCalledOnNull::Strict); } else if self.parse_keyword(Keyword::PARALLEL) { ensure_not_set(&body.parallel, "PARALLEL { UNSAFE | RESTRICTED | SAFE }")?; - if self.parse_keyword(Keyword::UNSAFE) { - body.parallel = Some(FunctionParallel::Unsafe); - } else if self.parse_keyword(Keyword::RESTRICTED) { - body.parallel = Some(FunctionParallel::Restricted); - } else if self.parse_keyword(Keyword::SAFE) { - body.parallel = Some(FunctionParallel::Safe); - } else { - return self - .expected_ref("one of UNSAFE | RESTRICTED | SAFE", self.peek_token_ref()); - } + body.parallel = Some(self.parse_function_parallel()?); } else if self.parse_keyword(Keyword::SECURITY) { ensure_not_set(&body.security, "SECURITY { DEFINER | INVOKER }")?; if self.parse_keyword(Keyword::DEFINER) { @@ -7499,6 +7492,218 @@ impl<'a> Parser<'a> { }) } + /// Parse a [Statement::CreateAggregate] + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createaggregate.html) + pub fn parse_create_aggregate( + &mut self, + or_replace: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + + // The legacy and modern forms (see [`CreateAggregateArgs::Legacy`]) differ + // only in whether a second parenthesized list follows the first, so look + // that far ahead before committing to either branch. + let args = if self.peek_create_aggregate_arg_list()? { + self.parse_create_aggregate_args()? + } else { + CreateAggregateArgs::Legacy + }; + self.expect_token(&Token::LParen)?; + + let mut seen: Vec = Vec::new(); + let options = self.parse_comma_separated(|parser| { + let start = parser.peek_token_ref().span.start; + let keyword = parser.parse_create_aggregate_option_key()?; + if seen.contains(&keyword) { + return parser_err!( + format!("Duplicate CREATE AGGREGATE option: {keyword:?}"), + start + ); + } + seen.push(keyword); + parser.parse_create_aggregate_option(keyword) + })?; + self.expect_token(&Token::RParen)?; + + Ok(CreateAggregate { + or_replace, + name, + args, + options, + }) + } + + /// Parse the argument list of a `CREATE AGGREGATE`: `(*)` or `(arg [, ...])`. + fn parse_create_aggregate_args(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::Mul) { + CreateAggregateArgs::Star + } else { + CreateAggregateArgs::List( + self.parse_comma_separated0(Parser::parse_function_arg, Token::RParen)?, + ) + }; + self.expect_token(&Token::RParen)?; + Ok(args) + } + + /// True when the list at the current position is the modern form's argument + /// list, meaning a second parenthesized list follows it. + /// + /// Errors when the list is never closed rather than guessing a form, since + /// committing to either one buries the missing `)` under whichever error + /// that branch happens to hit first. + fn peek_create_aggregate_arg_list(&self) -> Result { + let mut tokens = self + .tokens + .iter() + .skip(self.index) + .filter(|token| !matches!(token.token, Token::Whitespace(_))); + if tokens.next().map(|token| &token.token) != Some(&Token::LParen) { + return Ok(false); + } + let mut depth = 1usize; + while let Some(token) = tokens.next() { + match token.token { + Token::LParen => depth += 1, + Token::RParen => { + depth -= 1; + if depth == 0 { + return Ok(tokens.next().map(|token| &token.token) == Some(&Token::LParen)); + } + } + // The list is still open at the statement boundary, so stop + // before the next statement's parentheses answer for this one. + Token::SemiColon => return self.expected_ref(")", token), + _ => {} + } + } + self.expected_ref(")", &EOF_TOKEN) + } + + /// Parse `PARALLEL` qualifier value: `{ SAFE | RESTRICTED | UNSAFE }`. + fn parse_function_parallel(&mut self) -> Result { + match self.expect_one_of_keywords(&[Keyword::SAFE, Keyword::RESTRICTED, Keyword::UNSAFE])? { + Keyword::SAFE => Ok(FunctionParallel::Safe), + Keyword::RESTRICTED => Ok(FunctionParallel::Restricted), + Keyword::UNSAFE => Ok(FunctionParallel::Unsafe), + _ => self.expected_ref("one of SAFE | RESTRICTED | UNSAFE", self.peek_token_ref()), + } + } + + /// Every key listed here needs an arm in [`Self::parse_create_aggregate_option`]. + fn parse_create_aggregate_option_key(&mut self) -> Result { + self.expect_one_of_keywords(&[ + Keyword::SFUNC, + Keyword::STYPE, + Keyword::SSPACE, + Keyword::FINALFUNC, + Keyword::FINALFUNC_EXTRA, + Keyword::FINALFUNC_MODIFY, + Keyword::COMBINEFUNC, + Keyword::SERIALFUNC, + Keyword::DESERIALFUNC, + Keyword::INITCOND, + Keyword::MSFUNC, + Keyword::MINVFUNC, + Keyword::MSTYPE, + Keyword::MSSPACE, + Keyword::MFINALFUNC, + Keyword::MFINALFUNC_EXTRA, + Keyword::MFINALFUNC_MODIFY, + Keyword::MINITCOND, + Keyword::SORTOP, + Keyword::PARALLEL, + Keyword::HYPOTHETICAL, + Keyword::BASETYPE, + ]) + } + + /// Parse the value of a single `CREATE AGGREGATE` option, given its + /// already-consumed key. + fn parse_create_aggregate_option( + &mut self, + keyword: Keyword, + ) -> Result { + // Every option but the three valueless flags is spelled `KEY = value`. + if !matches!( + keyword, + Keyword::FINALFUNC_EXTRA | Keyword::MFINALFUNC_EXTRA | Keyword::HYPOTHETICAL + ) { + self.expect_token(&Token::Eq)?; + } + + Ok(match keyword { + Keyword::FINALFUNC_EXTRA => CreateAggregateOption::FinalFunctionExtra, + Keyword::MFINALFUNC_EXTRA => CreateAggregateOption::MovingFinalFunctionExtra, + Keyword::HYPOTHETICAL => CreateAggregateOption::Hypothetical, + Keyword::SFUNC => { + CreateAggregateOption::StateTransitionFunction(self.parse_object_name(false)?) + } + Keyword::STYPE => CreateAggregateOption::StateDataType(self.parse_data_type()?), + Keyword::SSPACE => CreateAggregateOption::StateDataSize(self.parse_literal_uint()?), + Keyword::FINALFUNC => { + CreateAggregateOption::FinalFunction(self.parse_object_name(false)?) + } + Keyword::FINALFUNC_MODIFY => { + CreateAggregateOption::FinalFunctionModify(self.parse_aggregate_modify_kind()?) + } + Keyword::COMBINEFUNC => { + CreateAggregateOption::CombineFunction(self.parse_object_name(false)?) + } + Keyword::SERIALFUNC => { + CreateAggregateOption::SerialFunction(self.parse_object_name(false)?) + } + Keyword::DESERIALFUNC => { + CreateAggregateOption::DeserialFunction(self.parse_object_name(false)?) + } + Keyword::INITCOND => CreateAggregateOption::InitialCondition(self.parse_value()?), + Keyword::MSFUNC => { + CreateAggregateOption::MovingStateTransitionFunction(self.parse_object_name(false)?) + } + Keyword::MINVFUNC => CreateAggregateOption::MovingInverseTransitionFunction( + self.parse_object_name(false)?, + ), + Keyword::MSTYPE => CreateAggregateOption::MovingStateDataType(self.parse_data_type()?), + Keyword::MSSPACE => { + CreateAggregateOption::MovingStateDataSize(self.parse_literal_uint()?) + } + Keyword::MFINALFUNC => { + CreateAggregateOption::MovingFinalFunction(self.parse_object_name(false)?) + } + Keyword::MFINALFUNC_MODIFY => CreateAggregateOption::MovingFinalFunctionModify( + self.parse_aggregate_modify_kind()?, + ), + Keyword::MINITCOND => { + CreateAggregateOption::MovingInitialCondition(self.parse_value()?) + } + Keyword::SORTOP => CreateAggregateOption::SortOperator(self.parse_operator_name()?), + Keyword::PARALLEL => CreateAggregateOption::Parallel(self.parse_function_parallel()?), + Keyword::BASETYPE => CreateAggregateOption::BaseType(self.parse_data_type()?), + _ => { + return self.expected_ref("a CREATE AGGREGATE option", self.peek_token_ref()); + } + }) + } + + /// Parse the value of `FINALFUNC_MODIFY` / `MFINALFUNC_MODIFY`. + fn parse_aggregate_modify_kind(&mut self) -> Result { + match self.expect_one_of_keywords(&[ + Keyword::READ_ONLY, + Keyword::SHAREABLE, + Keyword::READ_WRITE, + ])? { + Keyword::READ_ONLY => Ok(AggregateModifyKind::ReadOnly), + Keyword::SHAREABLE => Ok(AggregateModifyKind::Shareable), + Keyword::READ_WRITE => Ok(AggregateModifyKind::ReadWrite), + _ => self.expected_ref( + "one of READ_ONLY | SHAREABLE | READ_WRITE", + self.peek_token_ref(), + ), + } + } + /// Parse a [Statement::CreateOperatorFamily] /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createopfamily.html) @@ -11406,17 +11611,9 @@ impl<'a> Parser<'a> { security, }) } else if self.parse_keyword(Keyword::PARALLEL) { - let parallel = if self.parse_keyword(Keyword::UNSAFE) { - FunctionParallel::Unsafe - } else if self.parse_keyword(Keyword::RESTRICTED) { - FunctionParallel::Restricted - } else if self.parse_keyword(Keyword::SAFE) { - FunctionParallel::Safe - } else { - return self - .expected_ref("one of UNSAFE | RESTRICTED | SAFE", self.peek_token_ref()); - }; - Some(AlterFunctionAction::Parallel(parallel)) + Some(AlterFunctionAction::Parallel( + self.parse_function_parallel()?, + )) } else if self.parse_keyword(Keyword::COST) { Some(AlterFunctionAction::Cost(self.parse_number()?)) } else if self.parse_keyword(Keyword::ROWS) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b97c55df37..afff1f8ebe 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9652,6 +9652,280 @@ fn parse_lock_table() { } } +/// The rendered argument list of a `CREATE AGGREGATE`, which must not be the +/// legacy or wildcard form. +fn aggregate_args(args: &CreateAggregateArgs) -> Vec { + match args { + CreateAggregateArgs::List(args) => args.iter().map(ToString::to_string).collect(), + other => panic!("Expected an argument list, got: {other:?}"), + } +} + +#[test] +fn parse_create_aggregate_basic() { + let sql = "CREATE AGGREGATE myavg (NUMERIC) (SFUNC = numeric_avg_accum, STYPE = internal, FINALFUNC = numeric_avg, INITCOND = '0')"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "myavg"); + assert_eq!(aggregate_args(&agg.args), ["NUMERIC"]); + assert_eq!(agg.options.len(), 4); + assert_eq!(agg.options[0].to_string(), "SFUNC = numeric_avg_accum"); + assert_eq!(agg.options[1].to_string(), "STYPE = internal"); + assert_eq!(agg.options[2].to_string(), "FINALFUNC = numeric_avg"); + assert_eq!(agg.options[3].to_string(), "INITCOND = '0'"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_span() { + let sql = "CREATE AGGREGATE myavg (NUMERIC) (SFUNC = numeric_avg_accum, STYPE = internal, FINALFUNC = numeric_avg, INITCOND = '0')"; + let mut parser = Parser::new(&PostgreSqlDialect {}) + .try_with_sql(sql) + .unwrap(); + // From the aggregate name through the last spanned option value. + assert_eq!( + parser.parse_statement().unwrap().span(), + Span::new(Location::new(1, 18), Location::new(1, 119)) + ); +} + +#[test] +fn parse_create_aggregate_or_replace_with_parallel() { + let sql = "CREATE OR REPLACE AGGREGATE sum2 (INT4, INT4) (SFUNC = int4pl, STYPE = INT4, PARALLEL = SAFE)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(agg.or_replace); + assert_eq!(agg.name.to_string(), "sum2"); + assert_eq!(aggregate_args(&agg.args), ["INT4", "INT4"]); + assert_eq!(agg.options.len(), 3); + assert_eq!(agg.options[2].to_string(), "PARALLEL = SAFE"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_with_moving_aggregate_options() { + let sql = "CREATE AGGREGATE moving_sum (FLOAT8) (SFUNC = float8pl, STYPE = FLOAT8, MSFUNC = float8pl, MINVFUNC = float8mi, MSTYPE = FLOAT8, MFINALFUNC_EXTRA, MFINALFUNC_MODIFY = READ_ONLY)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "moving_sum"); + assert_eq!(aggregate_args(&agg.args), ["FLOAT8"]); + assert_eq!(agg.options.len(), 7); + assert_eq!(agg.options[4].to_string(), "MSTYPE = FLOAT8"); + assert_eq!(agg.options[5].to_string(), "MFINALFUNC_EXTRA"); + assert_eq!(agg.options[6].to_string(), "MFINALFUNC_MODIFY = READ_ONLY"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_star_args() { + let canonical = "CREATE AGGREGATE my_count (*) (SFUNC = int8inc, STYPE = INT8, INITCOND = '0')"; + let stmt = pg_and_generic().verified_stmt(canonical); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Star); + assert_eq!(agg.name.to_string(), "my_count"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + pg_and_generic().one_statement_parses_to( + "CREATE AGGREGATE my_count ( * ) (SFUNC = int8inc, STYPE = INT8)", + "CREATE AGGREGATE my_count (*) (SFUNC = int8inc, STYPE = INT8)", + ); +} + +#[test] +fn parse_create_aggregate_empty_args() { + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_agg () (SFUNC = my_sfunc, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::List(vec![])); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_named_and_variadic_args() { + let sql = + "CREATE AGGREGATE my_agg (input INT, VARIADIC tail TEXT) (SFUNC = my_sfunc, STYPE = INT)"; + let stmt = pg_and_generic().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!( + aggregate_args(&agg.args), + ["input INT", "VARIADIC tail TEXT"] + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_additional_options() { + pg_and_generic().verified_stmt( + "CREATE AGGREGATE percentile (FLOAT8) (SFUNC = ordered_set_transition, STYPE = internal, FINALFUNC = percentile_final, FINALFUNC_MODIFY = READ_WRITE, HYPOTHETICAL)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min (INT) (SFUNC = my_sfunc, STYPE = INT, SSPACE = 128, SORTOP = <)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min2 (INT) (SFUNC = my_sfunc, STYPE = INT, SORTOP = pg_catalog.<)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_sum (INT) (SFUNC = my_sfunc, STYPE = internal, COMBINEFUNC = my_combine, SERIALFUNC = my_serial, DESERIALFUNC = my_deserial)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_extra (INT) (SFUNC = my_sfunc, STYPE = internal, FINALFUNC = my_final, FINALFUNC_EXTRA)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_shareable (INT) (SFUNC = my_sfunc, STYPE = internal, FINALFUNC_MODIFY = SHAREABLE, MFINALFUNC = my_mfinal, MSSPACE = 64, MINITCOND = '0')", + ); +} + +#[test] +fn parse_create_aggregate_parenthesized_arg_type() { + // The two forms are told apart by counting parenthesized lists, so a + // parenthesized type must not be mistaken for the end of the argument list. + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_numeric (NUMERIC(10,2)) (SFUNC = my_sfunc, STYPE = NUMERIC(10,2))", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_numeric2 (BASETYPE = NUMERIC(10,2), SFUNC = my_sfunc, STYPE = INT)", + ); +} + +#[test] +fn parse_create_aggregate_legacy_syntax() { + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_avg (BASETYPE = INT, SFUNC = my_sfunc, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Legacy); + assert_eq!(agg.options.len(), 3); + assert_eq!( + agg.options[0], + CreateAggregateOption::BaseType(DataType::Int(None)) + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + let stmt = pg_and_generic() + .verified_stmt("CREATE AGGREGATE my_avg (SFUNC = my_sfunc, BASETYPE = INT, STYPE = INT)"); + match stmt { + Statement::CreateAggregate(agg) => { + assert_eq!(agg.args, CreateAggregateArgs::Legacy); + assert_eq!(agg.options.len(), 3); + assert_eq!( + agg.options[1], + CreateAggregateOption::BaseType(DataType::Int(None)) + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } + + // An option whose value cannot start an argument definition. + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_min (BASETYPE = INT, SFUNC = my_sfunc, STYPE = INT, SORTOP = <)", + ); + + // Without BASETYPE the statement is still a single-list one, and must not + // gain an empty argument list on the way back out. + pg_and_generic().verified_stmt("CREATE AGGREGATE my_avg (SFUNC = my_sfunc, STYPE = INT)"); +} + +#[test] +fn parse_create_aggregate_rejects_bad_options() { + let unknown = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (UNKNOWN_OPTION = bar)") + .unwrap_err() + .to_string(); + assert!( + unknown.starts_with("sql parser error: Expected: one of SFUNC") + && unknown.ends_with(", found: UNKNOWN_OPTION"), + "unexpected error: {unknown}" + ); + + // Option keys are keywords here, as they are in CREATE OPERATOR, so a + // quoted key is an ordinary identifier and does not name an option. + let quoted = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (\"SFUNC\" = my_sfunc, STYPE = INT)") + .unwrap_err() + .to_string(); + assert!( + quoted.starts_with("sql parser error: Expected: one of SFUNC") + && quoted.ends_with(", found: \"SFUNC\""), + "unexpected error: {quoted}" + ); + + let duplicate = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT) (SFUNC = f, SFUNC = g, STYPE = INT)") + .unwrap_err(); + assert_eq!( + duplicate.to_string(), + "sql parser error: Duplicate CREATE AGGREGATE option: SFUNC" + ); +} + +#[test] +fn parse_create_aggregate_reports_argument_list_errors() { + // A malformed argument list must report itself, not be reinterpreted as the + // legacy single-list form and blamed on the options. + let err = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT, ) (SFUNC = f, STYPE = INT)") + .unwrap_err(); + assert_eq!( + err.to_string(), + "sql parser error: Expected: a data type name, found: )" + ); + + // Every unclosed-list shape reports the same missing `)`, rather than the + // form-specific error a guessed branch would hit first. + for sql in [ + "CREATE AGGREGATE foo (INT", + "CREATE AGGREGATE foo (SORTOP = <, SFUNC = f, STYPE = INT", + "CREATE AGGREGATE foo (BASETYPE = INT, SFUNC = f", + ] { + let unclosed = pg_and_generic().parse_sql_statements(sql).unwrap_err(); + assert_eq!( + unclosed.to_string(), + "sql parser error: Expected: ), found: EOF", + "unexpected error for {sql}" + ); + } + + // A statement boundary ends the search, so the following statement's + // parentheses cannot answer for this one. + let semicolon = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo (INT; SELECT (1) (2)") + .unwrap_err(); + assert_eq!( + semicolon.to_string(), + "sql parser error: Expected: ), found: ;" + ); + + let missing = pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE foo )") + .unwrap_err(); + assert_eq!( + missing.to_string(), + "sql parser error: Expected: (, found: )" + ); +} + #[test] fn parse_create_foreign_table() { // Each of these round-trips through Display, so verified_stmt already pins From 41ef183deef5d75adee9de94a6e80b4ed33af6f1 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 26 Sep 2026 18:36:31 +0900 Subject: [PATCH 2/8] PostgreSQL: fix CREATE AGGREGATE SORTOP and empty-arg handling Accept and render OPERATOR(schema.op) for SORTOP, matching pg_dump. Require at least one explicit argument; empty () is a syntax error. Add CREATE AGGREGATE fuzz seeds. --- fuzz/fuzz_seeds/create_aggregate.sql | 21 +++++++++++++++++++++ fuzz/fuzz_seeds/create_aggregate_args.sql | 1 + fuzz/fuzz_seeds/create_aggregate_legacy.sql | 1 + fuzz/fuzz_seeds/create_aggregate_sortop.sql | 1 + fuzz/fuzz_seeds/create_aggregate_star.sql | 1 + src/ast/ddl.rs | 5 ++++- src/parser/mod.rs | 16 ++++++++++++---- tests/sqlparser_postgres.rs | 19 +++++++++---------- 8 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 fuzz/fuzz_seeds/create_aggregate.sql create mode 100644 fuzz/fuzz_seeds/create_aggregate_args.sql create mode 100644 fuzz/fuzz_seeds/create_aggregate_legacy.sql create mode 100644 fuzz/fuzz_seeds/create_aggregate_sortop.sql create mode 100644 fuzz/fuzz_seeds/create_aggregate_star.sql diff --git a/fuzz/fuzz_seeds/create_aggregate.sql b/fuzz/fuzz_seeds/create_aggregate.sql new file mode 100644 index 0000000000..616095bd2d --- /dev/null +++ b/fuzz/fuzz_seeds/create_aggregate.sql @@ -0,0 +1,21 @@ +CREATE AGGREGATE public.my_avg(numeric) ( + SFUNC = numeric_avg_accum, + STYPE = internal, + SSPACE = 128, + INITCOND = '0', + FINALFUNC = numeric_avg, + FINALFUNC_EXTRA, + FINALFUNC_MODIFY = SHAREABLE, + COMBINEFUNC = numeric_avg_combine, + SERIALFUNC = numeric_avg_serialize, + DESERIALFUNC = numeric_avg_deserialize, + MSFUNC = numeric_avg_accum, + MINVFUNC = numeric_accum_inv, + MSTYPE = internal, + MSSPACE = 64, + MFINALFUNC = numeric_avg, + MFINALFUNC_EXTRA, + MFINALFUNC_MODIFY = READ_WRITE, + MINITCOND = '0', + PARALLEL = safe +) diff --git a/fuzz/fuzz_seeds/create_aggregate_args.sql b/fuzz/fuzz_seeds/create_aggregate_args.sql new file mode 100644 index 0000000000..9a1c7c1972 --- /dev/null +++ b/fuzz/fuzz_seeds/create_aggregate_args.sql @@ -0,0 +1 @@ +CREATE AGGREGATE s.my_agg(IN a numeric(10,2), VARIADIC b text[]) (SFUNC = s.f, STYPE = integer[], INITCOND = '{}', SORTOP = <, HYPOTHETICAL) diff --git a/fuzz/fuzz_seeds/create_aggregate_legacy.sql b/fuzz/fuzz_seeds/create_aggregate_legacy.sql new file mode 100644 index 0000000000..4da5da7a60 --- /dev/null +++ b/fuzz/fuzz_seeds/create_aggregate_legacy.sql @@ -0,0 +1 @@ +CREATE AGGREGATE my_sum (BASETYPE = int4, SFUNC = int4pl, STYPE = int4, INITCOND = '0') diff --git a/fuzz/fuzz_seeds/create_aggregate_sortop.sql b/fuzz/fuzz_seeds/create_aggregate_sortop.sql new file mode 100644 index 0000000000..f38202fc8f --- /dev/null +++ b/fuzz/fuzz_seeds/create_aggregate_sortop.sql @@ -0,0 +1 @@ +CREATE AGGREGATE public.my_min(integer) (SFUNC = int4smaller, STYPE = integer, SORTOP = OPERATOR(pg_catalog.<)) diff --git a/fuzz/fuzz_seeds/create_aggregate_star.sql b/fuzz/fuzz_seeds/create_aggregate_star.sql new file mode 100644 index 0000000000..300081dc91 --- /dev/null +++ b/fuzz/fuzz_seeds/create_aggregate_star.sql @@ -0,0 +1 @@ +CREATE OR REPLACE AGGREGATE my_count(*) (SFUNC = int8inc, STYPE = bigint, INITCOND = '0') diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 05417a6b0a..2e3db0ecc0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6123,7 +6123,7 @@ pub enum CreateAggregateArgs { /// The wildcard form `(*)`, used by zero-argument aggregates such as /// `count(*)`. Star, - /// An explicit argument list, possibly empty: `()`, `(NUMERIC)`, + /// An explicit argument list: `(NUMERIC)`, /// `(input INT, VARIADIC tail TEXT)`. List(Vec), } @@ -6208,6 +6208,9 @@ impl fmt::Display for CreateAggregateOption { Self::MovingFinalFunctionExtra => write!(f, "MFINALFUNC_EXTRA"), Self::MovingFinalFunctionModify(kind) => write!(f, "MFINALFUNC_MODIFY = {kind}"), Self::MovingInitialCondition(cond) => write!(f, "MINITCOND = {cond}"), + Self::SortOperator(name) if name.0.len() > 1 => { + write!(f, "SORTOP = OPERATOR({name})") + } Self::SortOperator(name) => write!(f, "SORTOP = {name}"), Self::Parallel(parallel) => write!(f, "PARALLEL = {}", parallel.as_str()), Self::Hypothetical => write!(f, "HYPOTHETICAL"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c052ddcbb0..8c66aec6c3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7540,9 +7540,7 @@ impl<'a> Parser<'a> { let args = if self.consume_token(&Token::Mul) { CreateAggregateArgs::Star } else { - CreateAggregateArgs::List( - self.parse_comma_separated0(Parser::parse_function_arg, Token::RParen)?, - ) + CreateAggregateArgs::List(self.parse_comma_separated(Parser::parse_function_arg)?) }; self.expect_token(&Token::RParen)?; Ok(args) @@ -7678,7 +7676,17 @@ impl<'a> Parser<'a> { Keyword::MINITCOND => { CreateAggregateOption::MovingInitialCondition(self.parse_value()?) } - Keyword::SORTOP => CreateAggregateOption::SortOperator(self.parse_operator_name()?), + Keyword::SORTOP => { + let operator = if self.parse_keyword(Keyword::OPERATOR) { + self.expect_token(&Token::LParen)?; + let operator = self.parse_operator_name()?; + self.expect_token(&Token::RParen)?; + operator + } else { + self.parse_operator_name()? + }; + CreateAggregateOption::SortOperator(operator) + } Keyword::PARALLEL => CreateAggregateOption::Parallel(self.parse_function_parallel()?), Keyword::BASETYPE => CreateAggregateOption::BaseType(self.parse_data_type()?), _ => { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index afff1f8ebe..cf36dd9713 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9746,15 +9746,14 @@ fn parse_create_aggregate_star_args() { } #[test] -fn parse_create_aggregate_empty_args() { - let stmt = pg_and_generic() - .verified_stmt("CREATE AGGREGATE my_agg () (SFUNC = my_sfunc, STYPE = INT)"); - match stmt { - Statement::CreateAggregate(agg) => { - assert_eq!(agg.args, CreateAggregateArgs::List(vec![])); - } - _ => panic!("Expected CreateAggregate, got: {stmt:?}"), - } +fn parse_create_aggregate_rejects_empty_args() { + assert_eq!( + pg_and_generic() + .parse_sql_statements("CREATE AGGREGATE my_agg () (SFUNC = my_sfunc, STYPE = INT)") + .unwrap_err() + .to_string(), + "sql parser error: Expected: a data type name, found: )" + ); } #[test] @@ -9782,7 +9781,7 @@ fn parse_create_aggregate_additional_options() { "CREATE AGGREGATE my_min (INT) (SFUNC = my_sfunc, STYPE = INT, SSPACE = 128, SORTOP = <)", ); pg_and_generic().verified_stmt( - "CREATE AGGREGATE my_min2 (INT) (SFUNC = my_sfunc, STYPE = INT, SORTOP = pg_catalog.<)", + "CREATE AGGREGATE my_min2 (INT) (SFUNC = my_sfunc, STYPE = INT, SORTOP = OPERATOR(pg_catalog.<))", ); pg_and_generic().verified_stmt( "CREATE AGGREGATE my_sum (INT) (SFUNC = my_sfunc, STYPE = internal, COMBINEFUNC = my_combine, SERIALFUNC = my_serial, DESERIALFUNC = my_deserial)", From 1c8df0eebb7de3358ef3971666e896866f5fec2d Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 28 Sep 2026 18:35:33 +0900 Subject: [PATCH 3/8] feat(postgres): support ordered-set aggregate args --- fuzz/fuzz_seeds/create_aggregate_args.sql | 2 +- src/ast/ddl.rs | 12 ++++++++ src/ast/spans.rs | 3 ++ src/parser/mod.rs | 34 ++++++++++++----------- tests/sqlparser_postgres.rs | 17 ++++++------ 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/fuzz/fuzz_seeds/create_aggregate_args.sql b/fuzz/fuzz_seeds/create_aggregate_args.sql index 9a1c7c1972..7b5763c1ec 100644 --- a/fuzz/fuzz_seeds/create_aggregate_args.sql +++ b/fuzz/fuzz_seeds/create_aggregate_args.sql @@ -1 +1 @@ -CREATE AGGREGATE s.my_agg(IN a numeric(10,2), VARIADIC b text[]) (SFUNC = s.f, STYPE = integer[], INITCOND = '{}', SORTOP = <, HYPOTHETICAL) +CREATE AGGREGATE s.my_agg(VARIADIC "any" ORDER BY VARIADIC "any") (SFUNC = s.f, STYPE = integer[], INITCOND = '{}', SORTOP = <, HYPOTHETICAL) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2e3db0ecc0..c1f5c84a0e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6107,6 +6107,13 @@ impl fmt::Display for CreateAggregate { CreateAggregateArgs::Legacy => {} CreateAggregateArgs::Star => write!(f, " (*)")?, CreateAggregateArgs::List(args) => write!(f, " ({})", display_comma_separated(args))?, + CreateAggregateArgs::OrderedSet { direct, aggregated } => { + write!(f, " (")?; + if !direct.is_empty() { + write!(f, "{} ", display_comma_separated(direct))?; + } + write!(f, "ORDER BY {})", display_comma_separated(aggregated))?; + } } write!(f, " ({})", display_comma_separated(&self.options)) } @@ -6126,6 +6133,11 @@ pub enum CreateAggregateArgs { /// An explicit argument list: `(NUMERIC)`, /// `(input INT, VARIADIC tail TEXT)`. List(Vec), + /// PostgreSQL ordered-set form: args before `ORDER BY` are direct args. + OrderedSet { + direct: Vec, + aggregated: Vec, + }, } impl From for crate::ast::Statement { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 659ca158d4..a39ece78b7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2548,6 +2548,9 @@ impl Spanned for CreateAggregateArgs { match self { CreateAggregateArgs::Legacy | CreateAggregateArgs::Star => Span::empty(), CreateAggregateArgs::List(args) => union_spans(args.iter().map(|arg| arg.span())), + CreateAggregateArgs::OrderedSet { direct, aggregated } => { + union_spans(direct.iter().chain(aggregated).map(|arg| arg.span())) + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8c66aec6c3..9a5e973059 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6296,12 +6296,12 @@ impl<'a> Parser<'a> { // first token is a name and not a type in itself. let data_type_idx = self.get_current_index(); - // DEFAULT will be parsed as `DataType::Custom`, which is undesirable in this context - fn parse_data_type_no_default(parser: &mut Parser) -> Result { - if parser.peek_keyword(Keyword::DEFAULT) { + // DEFAULT and ORDER parse as `DataType::Custom`, which is undesirable here. + fn parse_data_type_no_arg_boundary(parser: &mut Parser) -> Result { + if parser.peek_keyword(Keyword::DEFAULT) || parser.peek_keyword(Keyword::ORDER) { // This dummy error is ignored in `maybe_parse` parser_err!( - "The DEFAULT keyword is not a type", + "The DEFAULT or ORDER keyword is not a type", parser.peek_token_ref().span.start ) } else { @@ -6309,7 +6309,7 @@ impl<'a> Parser<'a> { } } - if let Some(next_data_type) = self.maybe_parse(parse_data_type_no_default)? { + if let Some(next_data_type) = self.maybe_parse(parse_data_type_no_arg_boundary)? { let token = self.token_at(data_type_idx).clone(); // We ensure that the token is a `Word` token, and not other special tokens. @@ -7511,17 +7511,8 @@ impl<'a> Parser<'a> { }; self.expect_token(&Token::LParen)?; - let mut seen: Vec = Vec::new(); let options = self.parse_comma_separated(|parser| { - let start = parser.peek_token_ref().span.start; let keyword = parser.parse_create_aggregate_option_key()?; - if seen.contains(&keyword) { - return parser_err!( - format!("Duplicate CREATE AGGREGATE option: {keyword:?}"), - start - ); - } - seen.push(keyword); parser.parse_create_aggregate_option(keyword) })?; self.expect_token(&Token::RParen)?; @@ -7534,13 +7525,24 @@ impl<'a> Parser<'a> { }) } - /// Parse the argument list of a `CREATE AGGREGATE`: `(*)` or `(arg [, ...])`. fn parse_create_aggregate_args(&mut self) -> Result { self.expect_token(&Token::LParen)?; let args = if self.consume_token(&Token::Mul) { CreateAggregateArgs::Star } else { - CreateAggregateArgs::List(self.parse_comma_separated(Parser::parse_function_arg)?) + let direct = if self.peek_keyword(Keyword::ORDER) { + vec![] + } else { + self.parse_comma_separated(Parser::parse_function_arg)? + }; + if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) { + CreateAggregateArgs::OrderedSet { + direct, + aggregated: self.parse_comma_separated(Parser::parse_function_arg)?, + } + } else { + CreateAggregateArgs::List(direct) + } }; self.expect_token(&Token::RParen)?; Ok(args) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index cf36dd9713..fd2dd4ba1d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9775,7 +9775,13 @@ fn parse_create_aggregate_named_and_variadic_args() { #[test] fn parse_create_aggregate_additional_options() { pg_and_generic().verified_stmt( - "CREATE AGGREGATE percentile (FLOAT8) (SFUNC = ordered_set_transition, STYPE = internal, FINALFUNC = percentile_final, FINALFUNC_MODIFY = READ_WRITE, HYPOTHETICAL)", + "CREATE AGGREGATE percentile (FLOAT8 ORDER BY FLOAT8) (SFUNC = ordered_set_transition, STYPE = internal, FINALFUNC = percentile_final, FINALFUNC_MODIFY = READ_WRITE)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_mode (ORDER BY anyelement) (SFUNC = ordered_set_transition, STYPE = internal, FINALFUNC = mode_final, FINALFUNC_EXTRA)", + ); + pg_and_generic().verified_stmt( + "CREATE AGGREGATE my_rank (VARIADIC \"any\" ORDER BY VARIADIC \"any\") (SFUNC = ordered_set_transition_multi, STYPE = internal, FINALFUNC = rank_final, FINALFUNC_EXTRA, HYPOTHETICAL)", ); pg_and_generic().verified_stmt( "CREATE AGGREGATE my_min (INT) (SFUNC = my_sfunc, STYPE = INT, SSPACE = 128, SORTOP = <)", @@ -9870,13 +9876,8 @@ fn parse_create_aggregate_rejects_bad_options() { "unexpected error: {quoted}" ); - let duplicate = pg_and_generic() - .parse_sql_statements("CREATE AGGREGATE foo (INT) (SFUNC = f, SFUNC = g, STYPE = INT)") - .unwrap_err(); - assert_eq!( - duplicate.to_string(), - "sql parser error: Duplicate CREATE AGGREGATE option: SFUNC" - ); + pg_and_generic() + .verified_stmt("CREATE AGGREGATE foo (INT) (SFUNC = f, SFUNC = g, STYPE = INT)"); } #[test] From 7b7d9d3dd996d73591a516b8d8c62e0f78b1221c Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 28 Sep 2026 18:53:46 +0900 Subject: [PATCH 4/8] docs(postgres): document ordered-set aggregate fields --- src/ast/ddl.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c1f5c84a0e..e3779fc16d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6135,7 +6135,9 @@ pub enum CreateAggregateArgs { List(Vec), /// PostgreSQL ordered-set form: args before `ORDER BY` are direct args. OrderedSet { + /// Arguments before `ORDER BY`. direct: Vec, + /// Arguments after `ORDER BY`. aggregated: Vec, }, } From 23d5b8a4cf8ec46455846d946e5a1fadeb1417e0 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 16 Oct 2026 12:13:37 +0900 Subject: [PATCH 5/8] PostgreSQL: cover ordered-set aggregate arguments --- tests/sqlparser_postgres.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6c27dbc96f..f3b1d27cb2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9749,6 +9749,25 @@ fn parse_create_aggregate_additional_options() { "CREATE AGGREGATE my_shareable (INT) (SFUNC = my_sfunc, STYPE = internal, FINALFUNC_MODIFY = SHAREABLE, MFINALFUNC = my_mfinal, MSSPACE = 64, MINITCOND = '0')", ); } +#[test] +fn parse_create_aggregate_ordered_set_args() { + let sql = "CREATE AGGREGATE percentile (FLOAT8 ORDER BY FLOAT8) (SFUNC = ordered_set_transition, STYPE = internal)"; + let stmt = pg_and_generic().verified_stmt(sql); + let Statement::CreateAggregate(aggregate) = stmt else { + unreachable!() + }; + let CreateAggregateArgs::OrderedSet { direct, aggregated } = aggregate.args else { + unreachable!() + }; + assert_eq!( + direct.iter().map(ToString::to_string).collect::>(), + ["FLOAT8"] + ); + assert_eq!( + aggregated.iter().map(ToString::to_string).collect::>(), + ["FLOAT8"] + ); +} #[test] fn parse_create_aggregate_parenthesized_arg_type() { From c08269661f23f274dc77c13985744f097105af42 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:59:36 +0900 Subject: [PATCH 6/8] test(postgres): avoid brittle parallel error assertion --- tests/sqlparser_postgres.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5b99653400..3b6a0872d7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5271,12 +5271,10 @@ fn parse_create_function_with_set_params() { #[test] fn parse_incorrect_create_function_parallel() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL PARALLEL BLAH AS 'select $1 + $2;'"; - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError( - "Expected: one of UNSAFE | RESTRICTED | SAFE, found: BLAH".to_string() - ) - ); + assert!(matches!( + pg().parse_sql_statements(sql), + Err(ParserError::ParserError(_)) + )); } #[test] @@ -9738,7 +9736,10 @@ fn parse_create_aggregate_ordered_set_args() { ["FLOAT8"] ); assert_eq!( - aggregated.iter().map(ToString::to_string).collect::>(), + aggregated + .iter() + .map(ToString::to_string) + .collect::>(), ["FLOAT8"] ); } From d47c8cc516d5fc07bcc3cf9d239f36b925a2d8e9 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 17:25:18 +0900 Subject: [PATCH 7/8] fix(postgres): preserve ORDER custom function types --- src/parser/mod.rs | 6 +++--- tests/sqlparser_postgres.rs | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 965e95ec07..8da8aa4547 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6368,12 +6368,12 @@ impl<'a> Parser<'a> { // first token is a name and not a type in itself. let data_type_idx = self.get_current_index(); - // DEFAULT and ORDER parse as `DataType::Custom`, which is undesirable here. + // DEFAULT parses as `DataType::Custom`, which is undesirable here. fn parse_data_type_no_arg_boundary(parser: &mut Parser) -> Result { - if parser.peek_keyword(Keyword::DEFAULT) || parser.peek_keyword(Keyword::ORDER) { + if parser.peek_keyword(Keyword::DEFAULT) { // This dummy error is ignored in `maybe_parse` parser_err!( - "The DEFAULT or ORDER keyword is not a type", + "The DEFAULT keyword is not a type", parser.peek_token_ref().span.start ) } else { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 50c44fefd7..b24b31540c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10493,3 +10493,10 @@ fn parse_string_literal_alias_rejected() { } pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } + +#[test] +fn parse_create_function_with_order_type() { + pg_and_generic().verified_stmt( + "CREATE FUNCTION f(x ORDER) RETURNS INT LANGUAGE SQL AS 'SELECT 1'", + ); +} From ea2bfe8830988d53b96e3d64d94477ea61ce527b Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Wed, 21 Oct 2026 15:55:59 +0900 Subject: [PATCH 8/8] fix(postgres): stop function arguments at ORDER --- src/parser/mod.rs | 7 +++---- tests/sqlparser_postgres.rs | 7 ------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9aa189a177..3b8e5a6e4a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6374,12 +6374,11 @@ impl<'a> Parser<'a> { // first token is a name and not a type in itself. let data_type_idx = self.get_current_index(); - // DEFAULT parses as `DataType::Custom`, which is undesirable here. + // Argument-list boundaries cannot be parameter types. fn parse_data_type_no_arg_boundary(parser: &mut Parser) -> Result { - if parser.peek_keyword(Keyword::DEFAULT) { - // This dummy error is ignored in `maybe_parse` + if parser.peek_keyword(Keyword::DEFAULT) || parser.peek_keyword(Keyword::ORDER) { parser_err!( - "The DEFAULT keyword is not a type", + "Argument boundary keyword is not a type", parser.peek_token_ref().span.start ) } else { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b24b31540c..50c44fefd7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10493,10 +10493,3 @@ fn parse_string_literal_alias_rejected() { } pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } - -#[test] -fn parse_create_function_with_order_type() { - pg_and_generic().verified_stmt( - "CREATE FUNCTION f(x ORDER) RETURNS INT LANGUAGE SQL AS 'SELECT 1'", - ); -}