Skip to content

PostgreSQL: add support for CREATE AGGREGATE#2316

Open
fmguerreiro wants to merge 20 commits into
apache:mainfrom
fmguerreiro:upstream/create-aggregate
Open

PostgreSQL: add support for CREATE AGGREGATE#2316
fmguerreiro wants to merge 20 commits into
apache:mainfrom
fmguerreiro:upstream/create-aggregate

Conversation

@fmguerreiro

@fmguerreiro fmguerreiro commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Parses PostgreSQL CREATE [OR REPLACE] AGGREGATE, both the modern name (arg_types) (options) form and the legacy BASETYPE = ... form. Options cover the full set from the docs: state and final functions, the moving-aggregate variants, PARALLEL, SORTOP, HYPOTHETICAL, FINALFUNC_MODIFY.

Ordered-set aggregates (ORDER BY in the argument list) are out of scope.

Round-trip tests cover both syntaxes, star and variadic args, the moving-aggregate options, and an unknown-option error.

Add Statement::CreateAggregate, CreateAggregate struct, CreateAggregateOption
enum, and AggregateModifyKind enum to represent PostgreSQL CREATE AGGREGATE
DDL. Options are stored as a typed enum covering all documented parameters
(SFUNC, STYPE, FINALFUNC, PARALLEL, moving-aggregate variants, etc.).
Wire AGGREGATE into the CREATE dispatch (before the or_replace error branch
so CREATE OR REPLACE AGGREGATE is accepted). parse_create_aggregate parses
the name, argument-type list, and the options block. Each recognised option
keyword dispatches to parse_create_aggregate_option which produces the typed
CreateAggregateOption variant.
Three tests covering: basic old-style aggregate (SFUNC/STYPE/FINALFUNC/INITCOND),
CREATE OR REPLACE with PARALLEL = SAFE, and moving-aggregate options
(MSFUNC/MINVFUNC/MSTYPE/MFINALFUNC_EXTRA/MFINALFUNC_MODIFY). All use
pg().verified_stmt() to assert parse-then-display round-trips identically.
PR #7 added the Statement::CreateAggregate variant but omitted the
corresponding match arm in the Spanned impl for Statement. Fork CI
never ran on the PR so this was not caught before merge.
- SORTOP now parses via parse_operator_name so bare operators
  (SORTOP = <) work correctly.
- INITCOND / MINITCOND now store ValueWithSpan, preserving source
  location and matching the rest of the DDL layer.
- Replace the hand-rolled option loop with parse_comma_separated,
  rejecting leading and doubled commas.
- Simplify empty arg-list detection (no more prev_token dance).
- Replace the PARALLEL match-with-fallthrough with the
  if/else-if shape used elsewhere in the parser.
- Extend the 'after CREATE OR REPLACE' error message to mention
  AGGREGATE.
Return the name's span instead of Span::empty() to match the
sibling Create* arms.
- Rename abbreviated CreateAggregateOption variants to full words
- Preserve (*) wildcard arg list via CreateAggregate::star_args
- Implement Spanned for CreateAggregate and delegate from Statement
- Parse aggregate args as OperateFunctionArg so VARIADIC/named args
  round-trip instead of being dropped
- Add verified_stmt coverage for HYPOTHETICAL, SORTOP, schema-qualified
  SORTOP, FINALFUNC_MODIFY, SSPACE, COMBINEFUNC, SERIALFUNC/DESERIALFUNC,
  star args, and named/variadic args
- Switch CREATE AGGREGATE tests to pg_and_generic(), use
  display_comma_separated for options, extract parse_function_parallel
- Replace em-dashes in CreateAggregateOption doc comments
- Add FunctionParallel::as_str and reuse it in Display and the PARALLEL option
- Support old-syntax BASETYPE option (parse and Display round-trip)
- Clarify the CreateAggregate args field doc
- Test the unknown-option error path and the old BASETYPE syntax
…gate

# Conflicts:
#	src/ast/mod.rs
#	tests/sqlparser_postgres.rs
CREATE WAREHOUSE landed upstream and claimed the same CREATE dispatch arm.
Kept both branches; the CREATE OR REPLACE error message now lists AGGREGATE
and WAREHOUSE.
- record the syntax form in the AST. `CreateAggregateArgs` (Legacy/Star/List)
  replaces `Vec<OperateFunctionArg>` plus a `star_args` flag, so Display no
  longer infers the legacy form by scanning the options for BASETYPE. That
  scan round-tripped `foo (SFUNC = f, STYPE = INT)` back out with an added
  empty argument list, and let `(*)` coexist with a non-empty argument list.
- parse the modern argument list with `maybe_parse` instead of a hand-rolled
  rewind. An error inside the speculative parse propagated instead of falling
  back, so legacy statements whose option values cannot start an argument
  definition (`SORTOP = <`) failed to parse.
- promote the option keys to keywords, matching the CREATE OPERATOR parsing
  next to it. Quoted keys and duplicate options are now rejected, and the
  errors carry a source location.
- fold the three copies of PARALLEL { SAFE | RESTRICTED | UNSAFE } parsing
  into one helper.
Speculatively parsing the argument list and falling back on error meant a
typo inside a modern argument list was reinterpreted as the legacy form, so
`CREATE AGGREGATE foo (INT, ) (SFUNC = f)` pointed at `INT` and demanded an
aggregate option. Count the parenthesized lists instead, which is the only
thing that distinguishes the two forms, and let each branch report its own
error.

- name the duplicate option in its error instead of "Expected: no duplicate"
- move the option keys next to the match arms they must stay in sync with
- stop asserting the whole 22-keyword "one of" list in tests, which breaks
  on every future option for no signal
…n syntax

The lookahead only committed to the modern form when it found a matching
close paren, so `CREATE AGGREGATE foo (INT` fell through to the legacy
single-list branch and reported `INT` as an unknown aggregate option. An
unclosed list is not the legacy form either. Report the missing `)`.

- scan the token slice once instead of re-peeking from the current index
- cover parenthesized types, which are what the depth counter is for
- anchor the option-key error assertions at both ends
…list

Committing to either form on an unclosed list buries the real error under
whichever one that branch hits first. Forcing the modern branch turned
`CREATE AGGREGATE foo (SORTOP = <, SFUNC = f, STYPE = INT` into "expected an
expression"; forcing the legacy branch blamed the options list for `foo (INT`.
Neither form applies to a list that never closes, so say that instead of
picking one.

- rename the lookahead, which no longer answers only "is there a second list"
- stop the scan at a statement boundary
- move the parenthesized-type case to its own test, and drop the legacy
  variant of it, which reached the right branch even with the depth counter
  broken
…emicolon

The statement-boundary bail shared the end-of-input error, so
`CREATE AGGREGATE foo (INT; SELECT 1` reported `found: EOF` for a semicolon
at column 26. Keep the spans while scanning so that path can report the token
it actually stopped at, and cover it with a test.

Also restore the legacy-form parenthesized-type case dropped when that
assertion moved to its own test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant