Skip to content

feat: parse PostgreSQL CREATE/ALTER OPERATOR, OPERATOR CLASS/FAMILY and AGGREGATE - #2919

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-operator-aggregate-20261113
Nov 13, 2026
Merged

manticore-projects merged 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-operator-aggregate-20261113

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

PostgreSQL 18 operator and aggregate definitions are returned as UnsupportedStatement today, and the captured text breaks an operator name: CREATE OPERATOR === (...) renders as CREATE OPERATOR == = (...), which PostgreSQL does not read as the same operator. This PR parses them into typed statements.

CREATE OPERATOR s.<-> (FUNCTION = s.f, LEFTARG = point, RIGHTARG = point,
    COMMUTATOR = <->, NEGATOR = OPERATOR(s.<!>), RESTRICT = eqsel, HASHES);
ALTER OPERATOR === (int, int) SET (RESTRICT = eqsel, JOIN = NONE);
ALTER OPERATOR s.@@ (NONE, text) SET SCHEMA t;
CREATE OPERATOR CLASS oc DEFAULT FOR TYPE int4 USING gist FAMILY fam AS
    OPERATOR 1 < (int4, int4) FOR ORDER BY float_ops, FUNCTION 1 (int4, int4) f(int4, int4), STORAGE int4;
ALTER OPERATOR CLASS oc USING btree RENAME TO oc2;
CREATE OPERATOR FAMILY fam USING btree;
ALTER OPERATOR FAMILY fam USING btree ADD OPERATOR 1 < (int4, int2), FUNCTION 1 (int4, int2) cmp(int4, int2);
ALTER OPERATOR FAMILY fam USING btree DROP OPERATOR 1 (int4, int2), FUNCTION 1 (int4, int2);
CREATE OR REPLACE AGGREGATE agg (a int ORDER BY b text) (SFUNC = f, STYPE = internal, HYPOTHETICAL);
CREATE AGGREGATE agg (BASETYPE = int, SFUNC = f, STYPE = int);
ALTER AGGREGATE agg (*) OWNER TO u;

References: CREATE OPERATOR, ALTER OPERATOR, CREATE OPERATOR CLASS, ALTER OPERATOR CLASS, CREATE OPERATOR FAMILY, ALTER OPERATOR FAMILY, CREATE AGGREGATE, ALTER AGGREGATE, operator names in the lexical structure.

Current design (As-is)

  • CREATE/ALTER OPERATOR, OPERATOR CLASS, OPERATOR FAMILY and AGGREGATE fall through to UnsupportedStatement, in the PostgreSQL dialect too. The statement is a token list, so nothing rejects a malformed statement and an operator the lexer splits (===, <==>) is rendered with a space inside.
  • DROP OPERATOR, DROP AGGREGATE, DROP OPERATOR CLASS|FAMILY, COMMENT ON OPERATOR|AGGREGATE and CREATE TYPE name (definition) already have models: ExclusionOperator and OperatorSignature for an operator, RoutineReference for an aggregate signature, DefinitionElement/DefinitionArgument for a definition list.
  • The shared operator-name rule joins the lexer tokens that touch each other. It accepts names PostgreSQL's lexer does not produce (+-, =-), fails on <==> and @#@, and reads one schema only.
  • An aggregate signature may be empty: DROP AGGREGATE agg() parses, which PostgreSQL rejects.

Target design (To-be)

Eight statements, built from the existing models:

SQL Model
CREATE OPERATOR [schema.]op (definition) CreateOperator: getOperator() (ExclusionOperator), getDefinition() (DefinitionElement list), getDefinitionElement(name)
ALTER OPERATOR op (left, right) {OWNER TO role | SET SCHEMA schema | SET (...)} AlterOperator: getOperator() (OperatorSignature), getAction(), getNewName(), getProperties()
CREATE OPERATOR CLASS name [DEFAULT] FOR TYPE type USING method [FAMILY family] AS member [, ...] CreateOperatorClass: getName(), isDefaultClass(), getDataType(), getAccessMethod(), getFamily(), getItems()
ALTER OPERATOR CLASS name USING method {RENAME TO | OWNER TO | SET SCHEMA} AlterOperatorClass: getName(), getAccessMethod(), getAction(), getNewName()
CREATE OPERATOR FAMILY name USING method CreateOperatorFamily: getName(), getAccessMethod()
ALTER OPERATOR FAMILY name USING method {ADD member [, ...] | DROP member [, ...] | RENAME TO | OWNER TO | SET SCHEMA} AlterOperatorFamily: getName(), getAccessMethod(), getAction(), getItems(), getNewName()
CREATE [OR REPLACE] AGGREGATE name (arguments) (definition), CREATE AGGREGATE name (BASETYPE = type, ...) CreateAggregate: isOrReplace(), getAggregate() (RoutineReference), isOldStyle(), getDefinition(), getDefinitionElement(name)
ALTER AGGREGATE name (arguments) {RENAME TO | OWNER TO | SET SCHEMA} AlterAggregate: getAggregate(), getAction(), getNewName()
  • OperatorClassItem is one member of an operator class or family: Kind OPERATOR, FUNCTION or STORAGE, the strategy or support number, the operator with optional operand types (null for NONE), the Purpose (FOR SEARCH, or FOR ORDER BY with getSortFamily()), the function as a RoutineReference, or the storage type. A member that DROP removes has a kind, a number and operand types.
  • The grammar follows gram.y of PostgreSQL 18 (DefineStmt, AlterOperatorStmt, CreateOpClassStmt, CreateOpFamilyStmt, AlterOpFamilyStmt and the RENAME / OWNER / SET SCHEMA alternatives). The definition lists use the PostgreSqlDefinition() rule of CREATE TYPE, operators the reference and signature rules of DROP OPERATOR, aggregates the signature rule of DROP AGGREGATE. The names and values of a definition are not checked, as PostgreSQL checks them when it runs the statement. The old aggregate form takes identifiers with a value only (old_aggr_elem), so a keyword such as parallel is rejected there unless it is quoted.
  • An operator name is one name with its exact text, in these statements and in DROP OPERATOR, COMMENT ON OPERATOR, ALTER EXTENSION ... OPERATOR, exclusion constraints and definition values, which share the rule. It now follows PostgreSQL's lexer:
    • <==> (the lexer's <= and =>) and @#@ (lexed as a variable name) are operators;
    • a name of several characters ends in + or - only if it contains one of ~ ! @ # % ^ & | ?, so +-, =- and *- are rejected, as PostgreSQL reads them as two tokens;
    • => alone is not an operator, and a name has at most 63 characters;
    • a database may precede the schema (db.schema.===); ExclusionOperator.getSchemaName() then returns db.schema.
  • An aggregate signature is * or has at least one argument (aggr_args), wherever an aggregate is named.
  • No token and no reserved word is added. OPERATOR, CLASS, FAMILY and OWNER stay identifiers.
  • The statements are wired into StatementDeParser (sharing appendTo with toString), StatementVisitor, TablesNamesFinder (they name no table), StatementValidator (createOperator, alterOperator, createOperatorClass, alterOperatorClass, createOperatorFamily, alterOperatorFamily, createAggregate, alterAggregate, enabled for PostgreSQL) and StatementFeatureVisitor (schema change).

Dialects:

  • With Dialect.POSTGRESQL, CREATE/ALTER OPERATOR and CREATE/ALTER AGGREGATE are always read this way, so a malformed statement is a parse error instead of an UnsupportedStatement.
  • Without a dialect, CREATE/ALTER OPERATOR is read this way when an operator symbol or CLASS/FAMILY follows. Oracle's CREATE OPERATOR name BINDING ... and ALTER OPERATOR name COMPILE stay UnsupportedStatement, and so do MySQL's CREATE AGGREGATE FUNCTION and SQL Server's CREATE AGGREGATE name (@parameter type) ....

Behaviour changes for SQL that parsed before:

  • DROP OPERATOR +- (int, int), COMMENT ON OPERATOR =- (...) and similar names PostgreSQL does not take as one operator are parse errors.
  • DROP AGGREGATE agg() and COMMENT ON AGGREGATE agg() IS ... are parse errors.
  • DROP OPERATOR <==> (...), DROP OPERATOR @#@ (...) and DROP OPERATOR db.schema.+ (...) now parse.

Not covered:

  • An operator that contains a backtick, such as <`>: the lexer reads a backtick as the start of a quoted identifier.
  • Without Dialect.POSTGRESQL, the lexer reads # before #, @ or a letter as a name, so CREATE OPERATOR ## (...) stays an UnsupportedStatement there; the dialect reads it.
  • PostgreSQL reports four structural errors after parsing with the syntax error code, which the parser accepts as gram.y does: ALTER OPERATOR FAMILY ... ADD with an operator without operand types or with STORAGE, a member with more than two operand types, and a right-unary operator signature (type, NONE) in an operator class.

The change is additive for the Java API: DefinitionElement.find(elements, name) is new, and BaseTypeDefinition.getElement(name) uses it.

Validation

  • New PostgreSqlOperatorDdlTest (277 tests) and PostgreSqlAggregateDdlTest (73 tests): AST fields, toString/StatementDeParser/reparse round trips with and without the dialect, 88 operator spellings through CREATE OPERATOR, ALTER OPERATOR, ALTER OPERATOR FAMILY and DROP OPERATOR, statement boundaries in a script, visitors, TablesNamesFinder, features, validation, rejected forms, and the Oracle, MySQL and SQL Server forms that stay unsupported. ASTNodeCoverageTest and StatementFeatureVisitorMetadataDdlTest cover the new statements; UnsupportedStatementTest no longer lists the three statements that are typed now.

  • ./gradlew spotlessApply cleanTest check jar on Java 17: 14,845 tests, 0 failures, 25 skipped.

  • Compared with master (cf057dd33) on the DDL/DCL and other-statement survey corpora for MySQL and PostgreSQL (dialect and default mode, keyword and script corpora): 0 regressions and no changed result. The corpora contain none of these statements; a separate list of 138 accepted and 143 rejected forms covers them.

  • PostgreSQL 18.6: of 281 statements, the 129 whose syntax the server accepts are all typed with Dialect.POSTGRESQL (they stop only at execution, for example with a missing function, role or access method), and 147 of the 152 it rejects are parse errors. The other five are accepted by gram.y and rejected by a check after parsing that reports a syntax error (SQLSTATE 42601); the same text against a missing object reaches the catalog lookup instead: an unrecognized attribute in ALTER OPERATOR … SET (x = …), a right-unary signature OPERATOR 1 ! (int, NONE) in an operator class, and in ALTER OPERATOR FAMILY an ADD OPERATOR 1 < without operand types, ADD STORAGE int, and DROP FUNCTION 2 (int4, int2, int8) with three operand types.

  • Operator names: 88 operator spellings in four statements each and 18 texts that are not operator names in seven statements each (478 statements) all agree with the server.

AI assistance: prepared with Claude Code.

…nd AGGREGATE

CREATE and ALTER OPERATOR, OPERATOR CLASS, OPERATOR FAMILY and AGGREGATE
were unsupported statements, whose text broke an operator such as === into
"== =".

They parse to CreateOperator, AlterOperator, CreateOperatorClass,
AlterOperatorClass, CreateOperatorFamily, AlterOperatorFamily,
CreateAggregate and AlterAggregate. These reuse the models DROP OPERATOR,
DROP AGGREGATE and CREATE TYPE have: ExclusionOperator and
OperatorSignature for an operator, RoutineReference for an aggregate
signature and DefinitionElement for a definition list. OperatorClassItem
is a member of an operator class or family. The statements are wired into
the deparser, visitors, table finder, validation (eight features, enabled
for PostgreSQL) and feature analysis.

An operator name follows PostgreSQL's lexer wherever DDL names an
operator: <==> and @#@ are one name, a name such as +- or => alone is
rejected, and a database may precede the schema. An aggregate signature
needs * or an argument, so DROP AGGREGATE agg() is now a parse error.

Without the PostgreSQL dialect, CREATE and ALTER OPERATOR are read this
way when an operator symbol follows, so Oracle's CREATE OPERATOR name
BINDING stays unsupported, as do MySQL's CREATE AGGREGATE FUNCTION and
SQL Server's CREATE AGGREGATE name (@parameter type).

Signed-off-by: minleejae <mmj9808@gmail.com>
@manticore-projects
manticore-projects merged commit bccb1c8 into JSQLParser:master Nov 13, 2026
8 checks passed
@manticore-projects

Copy link
Copy Markdown
Contributor

Thank you!

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.

2 participants