From e5808b0b3941e664b382c07c5884afe28cb7dda0 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 13 Nov 2026 11:02:32 +0900 Subject: [PATCH] feat: parse PostgreSQL CREATE/ALTER OPERATOR, OPERATOR CLASS/FAMILY and 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 --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 48 ++ .../statement/DefinitionElement.java | 16 +- .../statement/StatementFeatureVisitor.java | 48 ++ .../statement/StatementVisitor.java | 72 ++ .../statement/alter/AlterAggregate.java | 89 +++ .../statement/alter/AlterOperator.java | 107 +++ .../statement/alter/AlterOperatorClass.java | 98 +++ .../statement/alter/AlterOperatorFamily.java | 121 +++ .../create/aggregate/CreateAggregate.java | 100 +++ .../create/operator/CreateOperator.java | 80 ++ .../create/operator/CreateOperatorClass.java | 116 +++ .../create/operator/CreateOperatorFamily.java | 60 ++ .../create/operator/OperatorClassItem.java | 191 +++++ .../create/type/BaseTypeDefinition.java | 3 +- .../sf/jsqlparser/util/TablesNamesFinder.java | 48 ++ .../util/deparser/StatementDeParser.java | 48 ++ .../validation/feature/FeaturesAllowed.java | 4 + .../validation/feature/PostgresqlVersion.java | 9 +- .../validator/StatementValidator.java | 56 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 442 ++++++++++- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 88 +++ .../parser/ASTNodeCoverageTest.java | 17 + ...tatementFeatureVisitorMetadataDdlTest.java | 22 + .../statement/UnsupportedStatementTest.java | 5 +- .../create/PostgreSqlAggregateDdlTest.java | 307 ++++++++ .../create/PostgreSqlOperatorDdlTest.java | 719 ++++++++++++++++++ 28 files changed, 2903 insertions(+), 14 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterAggregate.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterOperator.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorClass.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorFamily.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/aggregate/CreateAggregate.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperator.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorClass.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorFamily.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/operator/OperatorClassItem.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAggregateDdlTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlOperatorDdlTest.java diff --git a/README.md b/README.md index 9eda48d5b..c928a08d0 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **MySQL utility statements** | `EXPLAIN`/`DESCRIBE [ANALYZE] FORMAT = …` · `EXPLAIN FOR CONNECTION` · `DESCRIBE t col` · `PREPARE … FROM` · `EXECUTE … USING` · `DROP PREPARE` · `DO` | | **PostgreSQL administration** | `CREATE`/`ALTER EVENT TRIGGER` · `REINDEX` · `SECURITY LABEL` | | **PostgreSQL types and rules** | `CREATE TYPE` (shell, composite, enum, range and base types) · `ALTER TYPE` · `CREATE RULE` · `ALTER RULE` | +| **PostgreSQL operators and aggregates** | `CREATE`/`ALTER OPERATOR` · `CREATE`/`ALTER OPERATOR CLASS` · `CREATE`/`ALTER OPERATOR FAMILY` · `CREATE`/`ALTER AGGREGATE` | | **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index a3a161774..f977a9d3d 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -1001,6 +1001,54 @@ public enum Feature { * @see net.sf.jsqlparser.statement.alter.AlterRule */ alterRule, + /** + * PostgreSQL "CREATE OPERATOR" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.operator.CreateOperator + */ + createOperator, + /** + * PostgreSQL "CREATE OPERATOR CLASS" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.operator.CreateOperatorClass + */ + createOperatorClass, + /** + * PostgreSQL "CREATE OPERATOR FAMILY" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily + */ + createOperatorFamily, + /** + * PostgreSQL "CREATE AGGREGATE" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.aggregate.CreateAggregate + */ + createAggregate, + /** + * PostgreSQL "ALTER OPERATOR" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterOperator + */ + alterOperator, + /** + * PostgreSQL "ALTER OPERATOR CLASS" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterOperatorClass + */ + alterOperatorClass, + /** + * PostgreSQL "ALTER OPERATOR FAMILY" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterOperatorFamily + */ + alterOperatorFamily, + /** + * PostgreSQL "ALTER AGGREGATE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterAggregate + */ + alterAggregate, /** * SQL block starting with "BEGIN" and ends with "END" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java b/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java index 3c420ff49..22345c48e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java +++ b/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java @@ -15,8 +15,8 @@ /** * One element of a PostgreSQL definition list, {@code name [= value]}, as in - * {@code CREATE TYPE name (INPUT = fn, PASSEDBYVALUE)} or - * {@code ALTER TYPE name SET (SEND = NONE)}. The name keeps its spelling and quoting. + * {@code CREATE TYPE name (INPUT = fn, PASSEDBYVALUE)}, {@code ALTER TYPE name SET (SEND = NONE)}, + * {@code CREATE OPERATOR} or {@code CREATE AGGREGATE}. The name keeps its spelling and quoting. */ public class DefinitionElement implements Serializable { private String name; @@ -62,6 +62,18 @@ public static String toString(List elements) { return elements.stream().map(Object::toString).collect(Collectors.joining(", ", "(", ")")); } + /** + * Finds an element of a definition list by its name, ignoring case. + * + * @param elements the elements + * @param name the name, such as {@code INPUT} + * @return the first element of that name, or {@code null} + */ + public static DefinitionElement find(List elements, String name) { + return elements.stream().filter(element -> name.equalsIgnoreCase(element.getName())) + .findFirst().orElse(null); + } + @Override public String toString() { return argument == null ? name : name + " = " + argument; diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index f8b9a912c..fb5547dc8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -36,6 +36,14 @@ import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterPublication; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; @@ -1557,4 +1565,44 @@ public Void visit(CreateCollation statement, S context) { public Void visit(AlterRule statement, S context) { return schemaOnly(); } + + @Override + public Void visit(CreateOperator statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateOperatorClass statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateOperatorFamily statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateAggregate statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterOperator statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterOperatorClass statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterOperatorFamily statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterAggregate statement, S context) { + return schemaOnly(); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index ada0eb7a8..995549583 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -24,6 +24,14 @@ import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.create.collation.CreateCollation; import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterTrigger; @@ -1182,4 +1190,68 @@ default T visit(AlterRule statement, S context) { default void visit(AlterRule statement) { visit(statement, null); } + + default T visit(CreateOperator statement, S context) { + return null; + } + + default void visit(CreateOperator statement) { + visit(statement, null); + } + + default T visit(CreateOperatorClass statement, S context) { + return null; + } + + default void visit(CreateOperatorClass statement) { + visit(statement, null); + } + + default T visit(CreateOperatorFamily statement, S context) { + return null; + } + + default void visit(CreateOperatorFamily statement) { + visit(statement, null); + } + + default T visit(CreateAggregate statement, S context) { + return null; + } + + default void visit(CreateAggregate statement) { + visit(statement, null); + } + + default T visit(AlterOperator statement, S context) { + return null; + } + + default void visit(AlterOperator statement) { + visit(statement, null); + } + + default T visit(AlterOperatorClass statement, S context) { + return null; + } + + default void visit(AlterOperatorClass statement) { + visit(statement, null); + } + + default T visit(AlterOperatorFamily statement, S context) { + return null; + } + + default void visit(AlterOperatorFamily statement) { + visit(statement, null); + } + + default T visit(AlterAggregate statement, S context) { + return null; + } + + default void visit(AlterAggregate statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterAggregate.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterAggregate.java new file mode 100644 index 000000000..87dfc8a54 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterAggregate.java @@ -0,0 +1,89 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code ALTER AGGREGATE name (arguments)} with one of {@code RENAME TO new_name}, + * {@code OWNER TO role} and {@code SET SCHEMA schema}. + */ +public class AlterAggregate extends ASTNodeAccessImpl implements Statement { + public enum Action { + RENAME, OWNER, SET_SCHEMA + } + + private RoutineReference aggregate; + private Action action; + private String newName; + + /** + * The name and the argument signature, read as in DROP AGGREGATE. + * + * @return the aggregate + */ + public RoutineReference getAggregate() { + return aggregate; + } + + public void setAggregate(RoutineReference aggregate) { + this.aggregate = aggregate; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** + * The new name of {@code RENAME TO}, the role of {@code OWNER TO} or the schema of + * {@code SET SCHEMA}. + * + * @return the name, or {@code null} for another action + */ + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER AGGREGATE ").append(aggregate); + switch (action) { + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(newName); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(newName); + default: + throw new IllegalStateException("Unknown aggregate alteration: " + action); + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperator.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperator.java new file mode 100644 index 000000000..1f321e382 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperator.java @@ -0,0 +1,107 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.DefinitionElement; +import net.sf.jsqlparser.statement.OperatorSignature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code ALTER OPERATOR [schema.]op ({left_type | NONE}, right_type)} with one of + * {@code OWNER TO role}, {@code SET SCHEMA schema} and {@code SET (property [= value], ...)}. + * PostgreSQL checks the property names when it runs the statement, so they are not restricted. + */ +public class AlterOperator extends ASTNodeAccessImpl implements Statement { + public enum Action { + OWNER, SET_SCHEMA, SET_PROPERTIES + } + + private OperatorSignature operator; + private Action action; + private String newName; + private List properties = new ArrayList<>(); + + /** + * The operator and its operand types, read as in DROP OPERATOR. + * + * @return the operator signature + */ + public OperatorSignature getOperator() { + return operator; + } + + public void setOperator(OperatorSignature operator) { + this.operator = operator; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** + * The properties of {@code SET (property [= value], ...)}, such as {@code RESTRICT = NONE}. + * + * @return the properties in source order, empty for another action + */ + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + + /** + * The new name of {@code RENAME TO}, the role of {@code OWNER TO} or the schema of + * {@code SET SCHEMA}. + * + * @return the name, or {@code null} for another action + */ + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER OPERATOR ").append(operator); + switch (action) { + case OWNER: + return builder.append(" OWNER TO ").append(newName); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(newName); + case SET_PROPERTIES: + return builder.append(" SET ").append(DefinitionElement.toString(properties)); + default: + throw new IllegalStateException("Unknown operator alteration: " + action); + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorClass.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorClass.java new file mode 100644 index 000000000..56148e93c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorClass.java @@ -0,0 +1,98 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code ALTER OPERATOR CLASS name USING index_method} with one of + * {@code RENAME TO new_name}, {@code OWNER TO role} and {@code SET SCHEMA schema}. Names retain SQL + * identifier quoting. + */ +public class AlterOperatorClass extends ASTNodeAccessImpl implements Statement { + public enum Action { + RENAME, OWNER, SET_SCHEMA + } + + private String name; + private String accessMethod; + private Action action; + private String newName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The index access method after {@code USING}, such as {@code btree}. + * + * @return the access method + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** + * The new name of {@code RENAME TO}, the role of {@code OWNER TO} or the schema of + * {@code SET SCHEMA}. + * + * @return the name, or {@code null} for another action + */ + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER OPERATOR CLASS ").append(name).append(" USING ").append(accessMethod); + switch (action) { + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(newName); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(newName); + default: + throw new IllegalStateException("Unknown operator class alteration: " + action); + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorFamily.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorFamily.java new file mode 100644 index 000000000..de5fa4644 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperatorFamily.java @@ -0,0 +1,121 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.operator.OperatorClassItem; + +/** + * PostgreSQL {@code ALTER OPERATOR FAMILY name USING index_method} with one of + * {@code ADD member [, ...]}, {@code DROP member [, ...]}, {@code RENAME TO new_name}, + * {@code OWNER TO role} and {@code SET SCHEMA schema}. Names retain SQL identifier quoting. + */ +public class AlterOperatorFamily extends ASTNodeAccessImpl implements Statement { + public enum Action { + ADD, DROP, RENAME, OWNER, SET_SCHEMA + } + + private String name; + private String accessMethod; + private Action action; + private String newName; + private List items = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The index access method after {@code USING}, such as {@code btree}. + * + * @return the access method + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** + * The members that {@code ADD} adds or {@code DROP} removes. A member to remove has a kind, a + * number and operand types only. + * + * @return the members in source order, empty for another action + */ + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + /** + * The new name of {@code RENAME TO}, the role of {@code OWNER TO} or the schema of + * {@code SET SCHEMA}. + * + * @return the name, or {@code null} for another action + */ + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER OPERATOR FAMILY ").append(name).append(" USING ") + .append(accessMethod); + switch (action) { + case ADD: + return builder.append(" ADD ").append(OperatorClassItem.toString(items)); + case DROP: + return builder.append(" DROP ").append(OperatorClassItem.toString(items)); + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(newName); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(newName); + default: + throw new IllegalStateException("Unknown operator family alteration: " + action); + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/aggregate/CreateAggregate.java b/src/main/java/net/sf/jsqlparser/statement/create/aggregate/CreateAggregate.java new file mode 100644 index 000000000..ec0dc4f28 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/aggregate/CreateAggregate.java @@ -0,0 +1,100 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.aggregate; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.DefinitionElement; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code CREATE [OR REPLACE] AGGREGATE name (arguments) (SFUNC = fn, STYPE = type, + * ...)}, and the form from before PostgreSQL 8.2 that names the input type in the definition, + * {@code CREATE AGGREGATE name (BASETYPE = type, SFUNC = fn, STYPE = type, ...)}. PostgreSQL checks + * the names and values of the definition when it runs the statement, so they are not restricted. + */ +public class CreateAggregate extends ASTNodeAccessImpl implements Statement { + private boolean orReplace; + private RoutineReference aggregate; + private List definition = new ArrayList<>(); + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean orReplace) { + this.orReplace = orReplace; + } + + /** + * The name and the argument signature, read as in DROP AGGREGATE and COMMENT ON AGGREGATE. The + * old form has a name only: its arguments, {@code *} flag and ORDER BY arguments are unset. + * + * @return the aggregate + */ + public RoutineReference getAggregate() { + return aggregate; + } + + public void setAggregate(RoutineReference aggregate) { + this.aggregate = aggregate; + } + + /** + * Whether this is the old form, which has no argument signature before the definition. + * + * @return {@code true} for {@code CREATE AGGREGATE name (BASETYPE = type, ...)} + */ + public boolean isOldStyle() { + return aggregate != null && aggregate.getArguments() == null; + } + + /** + * The definition elements in source order, such as {@code SFUNC = fn} or {@code HYPOTHETICAL}. + * + * @return the elements + */ + public List getDefinition() { + return definition; + } + + public void setDefinition(List definition) { + this.definition = definition; + } + + /** + * Finds a definition element by its name, ignoring case. + * + * @param name the name, such as {@code SFUNC} + * @return the first element of that name, or {@code null} + */ + public DefinitionElement getDefinitionElement(String name) { + return DefinitionElement.find(definition, name); + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return builder.append(orReplace ? "CREATE OR REPLACE AGGREGATE " : "CREATE AGGREGATE ") + .append(aggregate).append(' ').append(DefinitionElement.toString(definition)); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperator.java b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperator.java new file mode 100644 index 000000000..3b1e22420 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperator.java @@ -0,0 +1,80 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.operator; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.DefinitionElement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.ExclusionOperator; + +/** + * PostgreSQL {@code CREATE OPERATOR [schema.]op (FUNCTION = fn, LEFTARG = type, ...)}. The operator + * keeps its exact spelling. PostgreSQL checks the names and values of the definition when it runs + * the statement, so they are not restricted. + */ +public class CreateOperator extends ASTNodeAccessImpl implements Statement { + private ExclusionOperator operator; + private List definition = new ArrayList<>(); + + /** + * The operator symbol and its schema, read as in DROP OPERATOR and COMMENT ON OPERATOR. + * + * @return the operator + */ + public ExclusionOperator getOperator() { + return operator; + } + + public void setOperator(ExclusionOperator operator) { + this.operator = operator; + } + + /** + * The definition elements in source order, such as {@code FUNCTION = fn} or {@code HASHES}. + * + * @return the elements + */ + public List getDefinition() { + return definition; + } + + public void setDefinition(List definition) { + this.definition = definition; + } + + /** + * Finds a definition element by its name, ignoring case. + * + * @param name the name, such as {@code COMMUTATOR} + * @return the first element of that name, or {@code null} + */ + public DefinitionElement getDefinitionElement(String name) { + return DefinitionElement.find(definition, name); + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("CREATE OPERATOR ").append(operator.getQualifiedName()).append(' ') + .append(DefinitionElement.toString(definition)); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorClass.java b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorClass.java new file mode 100644 index 000000000..68184f208 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorClass.java @@ -0,0 +1,116 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.operator; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +/** + * PostgreSQL {@code CREATE OPERATOR CLASS name [DEFAULT] FOR TYPE type USING index_method + * [FAMILY family] AS member [, ...]}. Names retain SQL identifier quoting. + */ +public class CreateOperatorClass extends ASTNodeAccessImpl implements Statement { + private String name; + private boolean defaultClass; + private ColDataType dataType; + private String accessMethod; + private String family; + private List items = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Whether the class is the {@code DEFAULT} operator class of its type. + * + * @return {@code true} when DEFAULT is written + */ + public boolean isDefaultClass() { + return defaultClass; + } + + public void setDefaultClass(boolean defaultClass) { + this.defaultClass = defaultClass; + } + + public ColDataType getDataType() { + return dataType; + } + + public void setDataType(ColDataType dataType) { + this.dataType = dataType; + } + + /** + * The index access method after {@code USING}, such as {@code btree}. + * + * @return the access method + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + /** + * The operator family the class is added to. + * + * @return the family name, or {@code null} without a FAMILY clause + */ + public String getFamily() { + return family; + } + + public void setFamily(String family) { + this.family = family; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE OPERATOR CLASS ").append(name); + if (defaultClass) { + builder.append(" DEFAULT"); + } + builder.append(" FOR TYPE ").append(dataType).append(" USING ").append(accessMethod); + if (family != null) { + builder.append(" FAMILY ").append(family); + } + return builder.append(" AS ").append(OperatorClassItem.toString(items)); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorFamily.java b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorFamily.java new file mode 100644 index 000000000..a1b5a93e2 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/operator/CreateOperatorFamily.java @@ -0,0 +1,60 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.operator; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code CREATE OPERATOR FAMILY name USING index_method}. Names retain SQL identifier + * quoting. + */ +public class CreateOperatorFamily extends ASTNodeAccessImpl implements Statement { + private String name; + private String accessMethod; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The index access method after {@code USING}, such as {@code btree}. + * + * @return the access method + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("CREATE OPERATOR FAMILY ").append(name).append(" USING ") + .append(accessMethod); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/operator/OperatorClassItem.java b/src/main/java/net/sf/jsqlparser/statement/create/operator/OperatorClassItem.java new file mode 100644 index 000000000..40f9cd0f4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/operator/OperatorClassItem.java @@ -0,0 +1,191 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.operator; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ExclusionOperator; + +/** + * One member of a PostgreSQL operator class or family, as listed by CREATE OPERATOR CLASS and by + * ALTER OPERATOR FAMILY ... ADD or DROP: + *
    + *
  • {@code OPERATOR n op [(left, right)] [FOR SEARCH | FOR ORDER BY sort_family]}
  • + *
  • {@code FUNCTION n [(type [, ...])] fn [(argument [, ...])]}
  • + *
  • {@code STORAGE type}
  • + *
  • {@code OPERATOR n (type [, ...])} and {@code FUNCTION n (type [, ...])}, which name the + * member that ALTER OPERATOR FAMILY ... DROP removes.
  • + *
+ */ +public class OperatorClassItem implements Serializable { + public enum Kind { + OPERATOR, FUNCTION, STORAGE + } + + /** What an operator of an operator class is used for. */ + public enum Purpose { + SEARCH, ORDER_BY + } + + private Kind kind; + private Integer number; + private ExclusionOperator operator; + private List operandTypes; + private Purpose purpose; + private String sortFamily; + private RoutineReference function; + private ColDataType storageType; + + public OperatorClassItem() { + // the parser or the setters fill the item + } + + public OperatorClassItem(Kind kind) { + this.kind = kind; + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + /** + * The strategy number of an operator or the support number of a function. + * + * @return the number, or {@code null} for {@code STORAGE} + */ + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + /** + * The operator of an {@code OPERATOR} member. + * + * @return the operator, or {@code null} for another kind and for a member to drop + */ + public ExclusionOperator getOperator() { + return operator; + } + + public void setOperator(ExclusionOperator operator) { + this.operator = operator; + } + + /** + * The operand types in parentheses. After an operator they are its left and right operand type, + * where {@code null} stands for {@code NONE}. + * + * @return the types, or {@code null} when the member has no such list + */ + public List getOperandTypes() { + return operandTypes; + } + + public void setOperandTypes(List operandTypes) { + this.operandTypes = operandTypes; + } + + /** + * The purpose of an operator, {@code FOR SEARCH} or {@code FOR ORDER BY sort_family}. + * + * @return the purpose, or {@code null} when none is written + */ + public Purpose getPurpose() { + return purpose; + } + + public void setPurpose(Purpose purpose) { + this.purpose = purpose; + } + + /** + * The operator family of {@code FOR ORDER BY}. + * + * @return the family name, or {@code null} + */ + public String getSortFamily() { + return sortFamily; + } + + public void setSortFamily(String sortFamily) { + this.sortFamily = sortFamily; + } + + /** + * The function of a {@code FUNCTION} member. Its arguments are {@code null} when the function + * is written without an argument list. + * + * @return the function, or {@code null} for another kind and for a member to drop + */ + public RoutineReference getFunction() { + return function; + } + + public void setFunction(RoutineReference function) { + this.function = function; + } + + public ColDataType getStorageType() { + return storageType; + } + + public void setStorageType(ColDataType storageType) { + this.storageType = storageType; + } + + /** + * Renders a member list. + * + * @param items the members + * @return the members, separated by commas + */ + public static String toString(List items) { + return items.stream().map(Object::toString).collect(Collectors.joining(", ")); + } + + private void appendOperandTypes(StringBuilder builder) { + if (operandTypes != null) { + builder.append(" (").append(operandTypes.stream() + .map(type -> type == null ? "NONE" : type.toString()) + .collect(Collectors.joining(", "))).append(')'); + } + } + + @Override + public String toString() { + if (kind == Kind.STORAGE) { + return "STORAGE " + storageType; + } + StringBuilder builder = new StringBuilder(kind.name()).append(' ').append(number); + if (operator != null) { + builder.append(' ').append(operator.getQualifiedName()); + } + appendOperandTypes(builder); + if (function != null) { + builder.append(' ').append(function); + } + if (purpose == Purpose.SEARCH) { + builder.append(" FOR SEARCH"); + } else if (purpose == Purpose.ORDER_BY) { + builder.append(" FOR ORDER BY ").append(sortFamily); + } + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java index 11e81f2a3..faaba6e81 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java @@ -43,8 +43,7 @@ public void setElements(List elements) { * @return the first element of that name, or {@code null} */ public DefinitionElement getElement(String name) { - return elements.stream().filter(element -> name.equalsIgnoreCase(element.getName())) - .findFirst().orElse(null); + return DefinitionElement.find(elements, name); } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index b9fc69a08..27c77b80b 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -130,6 +130,14 @@ import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterPublication; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; @@ -3532,4 +3540,44 @@ public Void visit(AlterRule statement, S context) { visit(statement.getTable(), context); return null; } + + @Override + public Void visit(CreateOperator statement, S context) { + return null; + } + + @Override + public Void visit(CreateOperatorClass statement, S context) { + return null; + } + + @Override + public Void visit(CreateOperatorFamily statement, S context) { + return null; + } + + @Override + public Void visit(CreateAggregate statement, S context) { + return null; + } + + @Override + public Void visit(AlterOperator statement, S context) { + return null; + } + + @Override + public Void visit(AlterOperatorClass statement, S context) { + return null; + } + + @Override + public Void visit(AlterOperatorFamily statement, S context) { + return null; + } + + @Override + public Void visit(AlterAggregate statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 3f0b98ead..cb72acbac 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -32,6 +32,14 @@ import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; @@ -1153,4 +1161,44 @@ public StringBuilder visit(AlterTrigger statement, S context) { public StringBuilder visit(AlterRule statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(CreateOperator statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateOperatorClass statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateOperatorFamily statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateAggregate statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterOperator statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterOperatorClass statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterOperatorFamily statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterAggregate statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index e8992ce57..88e36a40e 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -104,6 +104,8 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.createTextSearchConfiguration, Feature.createRule, Feature.createCollation, + Feature.createOperator, Feature.createOperatorClass, Feature.createOperatorFamily, + Feature.createAggregate, Feature.createSchema, Feature.createSequence, Feature.createTable, Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, Feature.createAccessMethod, Feature.createAccessMethodTable, @@ -122,6 +124,8 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, Feature.alterTextSearchConfiguration, Feature.alterCollation, Feature.alterRule, + Feature.alterOperator, Feature.alterOperatorClass, + Feature.alterOperatorFamily, Feature.alterAggregate, Feature.alterTablespace, Feature.alterEventTrigger, Feature.alterEvent, Feature.alterPolicy, Feature.alterStatistics, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index d85a3b438..53a0071a3 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -122,7 +122,14 @@ public enum PostgresqlVersion implements Version { Feature.alterUserMapping, Feature.createTextSearchConfiguration, Feature.alterTextSearchConfiguration, Feature.createRule, Feature.notifyStatement, - Feature.createCollation, Feature.alterCollation, + Feature.createCollation, Feature.alterCollation, // https://www.postgresql.org/docs/current/sql-createoperator.html + // https://www.postgresql.org/docs/current/sql-createopclass.html + // https://www.postgresql.org/docs/current/sql-createopfamily.html + // https://www.postgresql.org/docs/current/sql-createaggregate.html + Feature.createOperator, Feature.alterOperator, + Feature.createOperatorClass, Feature.alterOperatorClass, + Feature.createOperatorFamily, Feature.alterOperatorFamily, + Feature.createAggregate, Feature.alterAggregate, Feature.createPublication, Feature.alterPublication, Feature.createSubscription, Feature.alterSubscription, Feature.createType, Feature.alterType, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index f586fa9f4..eb036aa53 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -10,6 +10,14 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; @@ -1648,4 +1656,52 @@ public Void visit(AlterRule statement, S context) { validateOptionalFromItem(statement.getTable()); return null; } + + @Override + public Void visit(CreateOperator statement, S context) { + validateFeature(Feature.createOperator); + return null; + } + + @Override + public Void visit(CreateOperatorClass statement, S context) { + validateFeature(Feature.createOperatorClass); + return null; + } + + @Override + public Void visit(CreateOperatorFamily statement, S context) { + validateFeature(Feature.createOperatorFamily); + return null; + } + + @Override + public Void visit(CreateAggregate statement, S context) { + validateFeature(Feature.createAggregate); + return null; + } + + @Override + public Void visit(AlterOperator statement, S context) { + validateFeature(Feature.alterOperator); + return null; + } + + @Override + public Void visit(AlterOperatorClass statement, S context) { + validateFeature(Feature.alterOperatorClass); + return null; + } + + @Override + public Void visit(AlterOperatorFamily statement, S context) { + validateFeature(Feature.alterOperatorFamily); + return null; + } + + @Override + public Void visit(AlterAggregate statement, S context) { + validateFeature(Feature.alterAggregate); + return null; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f2ad5ec52..c933a96b8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -59,6 +59,8 @@ import net.sf.jsqlparser.statement.create.rule.CreateRule; import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.notify.ListenStatement; import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.create.operator.*; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; @@ -2975,6 +2977,174 @@ public class CCJSqlParser extends AbstractJSqlParser { "XMLELEMENT", "XMLEXISTS", "XMLFOREST", "XMLNAMESPACES", "XMLPARSE", "XMLPI", "XMLROOT", "XMLSERIALIZE", "XMLTABLE")); + /** + * PostgreSQL 18's unreserved keywords and its column name keywords that are type names: the + * keywords that are in none of the sets above. + */ + private static final Set POSTGRESQL_OTHER_KEYWORDS = new HashSet(Arrays.asList( + "ABORT", "ABSENT", "ABSOLUTE", "ACCESS", "ACTION", "ADD", "ADMIN", "AFTER", + "AGGREGATE", "ALSO", "ALTER", "ALWAYS", "ASENSITIVE", "ASSERTION", "ASSIGNMENT", + "AT", "ATOMIC", "ATTACH", "ATTRIBUTE", "BACKWARD", "BEFORE", "BEGIN", "BIGINT", + "BIT", "BOOLEAN", "BREADTH", "BY", "CACHE", "CALL", "CALLED", "CASCADE", "CASCADED", + "CATALOG", "CHAIN", "CHAR", "CHARACTER", "CHARACTERISTICS", "CHECKPOINT", "CLASS", + "CLOSE", "CLUSTER", "COLUMNS", "COMMENT", "COMMENTS", "COMMIT", "COMMITTED", + "COMPRESSION", "CONDITIONAL", "CONFIGURATION", "CONFLICT", "CONNECTION", + "CONSTRAINTS", "CONTENT", "CONTINUE", "CONVERSION", "COPY", "COST", "CSV", "CUBE", + "CURRENT", "CURSOR", "CYCLE", "DATA", "DATABASE", "DAY", "DEALLOCATE", "DEC", + "DECIMAL", "DECLARE", "DEFAULTS", "DEFERRED", "DEFINER", "DELETE", "DELIMITER", + "DELIMITERS", "DEPENDS", "DEPTH", "DETACH", "DICTIONARY", "DISABLE", "DISCARD", + "DOCUMENT", "DOMAIN", "DOUBLE", "DROP", "EACH", "EMPTY", "ENABLE", "ENCODING", + "ENCRYPTED", "ENFORCED", "ENUM", "ERROR", "ESCAPE", "EVENT", "EXCLUDE", "EXCLUDING", + "EXCLUSIVE", "EXECUTE", "EXPLAIN", "EXPRESSION", "EXTENSION", "EXTERNAL", "FAMILY", + "FILTER", "FINALIZE", "FIRST", "FLOAT", "FOLLOWING", "FORCE", "FORMAT", "FORWARD", + "FUNCTION", "FUNCTIONS", "GENERATED", "GLOBAL", "GRANTED", "GROUPS", "HANDLER", + "HEADER", "HOLD", "HOUR", "IDENTITY", "IF", "IMMEDIATE", "IMMUTABLE", "IMPLICIT", + "IMPORT", "INCLUDE", "INCLUDING", "INCREMENT", "INDENT", "INDEX", "INDEXES", + "INHERIT", "INHERITS", "INLINE", "INPUT", "INSENSITIVE", "INSERT", "INSTEAD", "INT", + "INTEGER", "INTERVAL", "INVOKER", "ISOLATION", "JSON", "KEEP", "KEY", "KEYS", + "LABEL", "LANGUAGE", "LARGE", "LAST", "LEAKPROOF", "LEVEL", "LISTEN", "LOAD", + "LOCAL", "LOCATION", "LOCK", "LOCKED", "LOGGED", "MAPPING", "MATCH", "MATCHED", + "MATERIALIZED", "MAXVALUE", "MERGE", "METHOD", "MINUTE", "MINVALUE", "MODE", + "MONTH", "MOVE", "NAME", "NAMES", "NCHAR", "NESTED", "NEW", "NEXT", "NFC", "NFD", + "NFKC", "NFKD", "NO", "NONE", "NORMALIZED", "NOTHING", "NOTIFY", "NOWAIT", "NULLS", + "NUMERIC", "OBJECT", "OBJECTS", "OF", "OFF", "OIDS", "OLD", "OMIT", "OPERATOR", + "OPTION", "OPTIONS", "ORDINALITY", "OTHERS", "OVER", "OVERRIDING", "OWNED", "OWNER", + "PARALLEL", "PARAMETER", "PARSER", "PARTIAL", "PARTITION", "PASSING", "PASSWORD", + "PATH", "PERIOD", "PLAN", "PLANS", "POLICY", "PRECEDING", "PREPARE", "PREPARED", + "PRESERVE", "PRIOR", "PRIVILEGES", "PROCEDURAL", "PROCEDURE", "PROCEDURES", + "PROGRAM", "PUBLICATION", "QUOTE", "QUOTES", "RANGE", "READ", "REAL", "REASSIGN", + "RECURSIVE", "REF", "REFERENCING", "REFRESH", "REINDEX", "RELATIVE", "RELEASE", + "RENAME", "REPEATABLE", "REPLACE", "REPLICA", "RESET", "RESTART", "RESTRICT", + "RETURN", "RETURNS", "REVOKE", "ROLE", "ROLLBACK", "ROLLUP", "ROUTINE", "ROUTINES", + "ROWS", "RULE", "SAVEPOINT", "SCALAR", "SCHEMA", "SCHEMAS", "SCROLL", "SEARCH", + "SECOND", "SECURITY", "SEQUENCE", "SEQUENCES", "SERIALIZABLE", "SERVER", "SESSION", + "SET", "SETS", "SHARE", "SHOW", "SIMPLE", "SKIP", "SMALLINT", "SNAPSHOT", "SOURCE", + "SQL", "STABLE", "STANDALONE", "START", "STATEMENT", "STATISTICS", "STDIN", + "STDOUT", "STORAGE", "STORED", "STRICT", "STRING", "STRIP", "SUBSCRIPTION", + "SUPPORT", "SYSID", "SYSTEM", "TABLES", "TABLESPACE", "TARGET", "TEMP", "TEMPLATE", + "TEMPORARY", "TEXT", "TIES", "TIME", "TIMESTAMP", "TRANSACTION", "TRANSFORM", + "TRIGGER", "TRUNCATE", "TRUSTED", "TYPE", "TYPES", "UESCAPE", "UNBOUNDED", + "UNCOMMITTED", "UNCONDITIONAL", "UNENCRYPTED", "UNKNOWN", "UNLISTEN", "UNLOGGED", + "UNTIL", "UPDATE", "VACUUM", "VALID", "VALIDATE", "VALIDATOR", "VALUE", "VARCHAR", + "VARYING", "VERSION", "VIEW", "VIEWS", "VIRTUAL", "VOLATILE", "WHITESPACE", + "WITHIN", "WITHOUT", "WORK", "WRAPPER", "WRITE", "XML", "YEAR", "YES", "ZONE")); + + /** Whether the word is a keyword of PostgreSQL 18, of any kind. A quoted identifier is none. */ + private static boolean isPostgreSqlKeyword(String word) { + String upper = word.toUpperCase(Locale.ROOT); + return POSTGRESQL_RESERVED_KEYWORDS.contains(upper) + || POSTGRESQL_TYPE_FUNC_NAME_KEYWORDS.contains(upper) + || POSTGRESQL_NON_TYPE_COL_NAME_KEYWORDS.contains(upper) + || POSTGRESQL_OTHER_KEYWORDS.contains(upper); + } + + /** + * Checks an operator name the way PostgreSQL's lexer delimits one. A name of several characters + * ends in + or - only if it has one of ~ ! @ # % ^ & | ` ?, since the lexer otherwise leaves + * the sign to the next token; "=>" is not an operator, and a name has at most 63 characters. + */ + private static void requirePostgreSqlOperatorName(String name) throws ParseException { + char last = name.charAt(name.length() - 1); + boolean signLeftOver = name.length() > 1 && (last == '+' || last == '-') + && !name.matches(".*[~!@#%^&|`?].*"); + if (signLeftOver || "=>".equals(name) || name.length() > 63) { + throw new ParseException("PostgreSQL does not take " + name + " as an operator name"); + } + } + + /** Whether OPERATOR is followed by CLASS or FAMILY as a keyword, not as the schema of an operator. */ + private boolean isOperatorClassOrFamilyAhead() { + return ("CLASS".equalsIgnoreCase(getToken(2).image) || "FAMILY".equalsIgnoreCase(getToken(2).image)) + && !".".equals(getToken(3).image); + } + + /** + * Whether CREATE or ALTER is followed by a PostgreSQL operator, operator class or operator + * family. Without the PostgreSQL dialect, an operator is known by its symbol, because Oracle + * names its operators with identifiers, as in CREATE OPERATOR name BINDING (...). + */ + private boolean isPostgreSqlOperatorDdlAhead() { + if (!isKeywordAhead("OPERATOR")) { + return false; + } + if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + return true; + } + if (isOperatorClassOrFamilyAhead()) { + // an Oracle operator may be named class or family + String next = getToken(3).image; + return !"BINDING".equalsIgnoreCase(next) && !"COMPILE".equalsIgnoreCase(next) + && !"BINDING".equalsIgnoreCase(getToken(4).image); + } + return isOperatorNameAhead(2); + } + + /** Whether an operator symbol follows at the offset, possibly after schema names and dots. */ + private boolean isOperatorNameAhead(int offset) { + int name = offset; + while (".".equals(getToken(name + 1).image)) { + name += 2; + } + return DropLookahead.isOperatorSymbol(getToken(name)); + } + + /** + * Whether CREATE is followed by a PostgreSQL aggregate. Without the PostgreSQL dialect, MySQL's + * CREATE AGGREGATE FUNCTION and SQL Server's CREATE AGGREGATE name (@parameter type) are not. + */ + private boolean isPostgreSqlCreateAggregateAhead() { + if (getToken(1).kind != K_AGGREGATE) { + return false; + } + if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { + return true; + } + int name = 2; + while (".".equals(getToken(name + 1).image)) { + name += 2; + } + String parameter = getToken(name + 2).image; + return getToken(2).kind != K_FUNCTION && (parameter == null || !parameter.startsWith("@")); + } + + /** + * Whether an aggregate name is followed by the definition of the old CREATE AGGREGATE form, + * "(" name "=", and not by an argument signature. + */ + private boolean isOldAggregateDefinitionAhead() { + int name = 1; + while (".".equals(getToken(name + 1).image)) { + name += 2; + } + return getToken(name + 1).kind == OPENING_BRACKET && "=".equals(getToken(name + 3).image); + } + + /** PostgreSQL's aggr_args: an aggregate signature is "*" or has at least one argument. */ + private static boolean isAggregateSignature(RoutineReference aggregate) { + return aggregate.getArguments() != null && (aggregate.isAllArguments() + || !aggregate.getArguments().isEmpty() || aggregate.getOrderByArguments() != null); + } + + /** + * PostgreSQL's old_aggr_definition: every element has a value, and its name is an identifier, + * which no keyword is. + */ + private void requireOldAggregateDefinition(List definition) throws ParseException { + for (DefinitionElement element : definition) { + requireDdlSyntax(element.getArgument() != null && !isPostgreSqlKeyword(element.getName()), + "PostgreSQL expects an identifier and a value: " + element); + } + } + + /** PostgreSQL's Iconst: the strategy or support number of an operator class member. */ + private int operatorClassNumber(Token token) throws ParseException { + try { + return Integer.parseInt(token.image); + } catch (NumberFormatException exception) { + throw new ParseException("Expected an integer: " + token.image); + } + } + /** Whether the token is an identifier or a keyword: a word PostgreSQL takes as a label. */ private static boolean isPostgreSqlWord(Token token) { return token.kind == S_IDENTIFIER || token.kind == S_QUOTED_IDENTIFIER || token.kind == DATA_TYPE @@ -22793,7 +22963,8 @@ void DropTargets(Drop drop): transform=TransformSignature() { drop.setTransform(transform); } | LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.OPERATOR - && DropLookahead.isOperatorAhead(getToken(1), getToken(2), getToken(3)) }) + && (DropLookahead.isOperatorAhead(getToken(1), getToken(2), getToken(3)) + || isOperatorNameAhead(1)) }) operator=OperatorSignature() { drop.addOperators(operator); } ( "," operator=OperatorSignature() { drop.addOperators(operator); } )* | @@ -24718,6 +24889,10 @@ Statement Alter(): | LOOKAHEAD({ isKeywordAhead("RULE") }) statement = AlterRule() | + LOOKAHEAD({ isPostgreSqlOperatorDdlAhead() }) statement = PostgreSqlAlterOperator() + | + LOOKAHEAD(1, ) statement = AlterAggregate() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | // Other routine forms, such as SQL Server definitions, remain unsupported. @@ -26818,6 +26993,12 @@ Statement Create(): | LOOKAHEAD({ isKeywordAhead("COLLATION") }) { requireDdlSyntax(!isUsingOrReplace, "CREATE COLLATION does not support OR REPLACE"); } statement = CreateCollation() | + LOOKAHEAD({ isPostgreSqlOperatorDdlAhead() }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE OPERATOR does not support OR REPLACE"); } + statement = PostgreSqlCreateOperator() + | + LOOKAHEAD({ isPostgreSqlCreateAggregateAhead() }) statement = CreateAggregate(isUsingOrReplace) + | LOOKAHEAD({ getToken(1).kind == K_TABLESPACE || isKeywordAhead("UNDO") && getToken(2).kind == K_TABLESPACE }) { requireDdlSyntax(!isUsingOrReplace, "CREATE TABLESPACE does not support OR REPLACE"); } @@ -27401,6 +27582,247 @@ DefinitionArgument PostgreSqlDefinitionArgument(): { return result; } } +/** PostgreSQL CREATE OPERATOR, CREATE OPERATOR CLASS or CREATE OPERATOR FAMILY. */ +Statement PostgreSqlCreateOperator(): +{ Statement statement; } +{ + ( + LOOKAHEAD({ isOperatorClassOrFamilyAhead() && "CLASS".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("OPERATOR") statement=CreateOperatorClass() + | + LOOKAHEAD({ isOperatorClassOrFamilyAhead() }) + ContextualKeyword("OPERATOR") statement=CreateOperatorFamily() + | + ContextualKeyword("OPERATOR") statement=CreateOperator() + ) + { return statement; } +} + +/** PostgreSQL's CREATE OPERATOR any_operator definition. */ +CreateOperator CreateOperator(): +{ CreateOperator statement = new CreateOperator(); ExclusionOperator operator; List definition; } +{ + operator=PostgreSqlOperatorReference() { statement.setOperator(operator); } + definition=PostgreSqlDefinition() { statement.setDefinition(definition); } + { return statement; } +} + +/** + * PostgreSQL's CREATE OPERATOR CLASS name [ DEFAULT ] FOR TYPE type USING index_method + * [ FAMILY family ] AS opclass_item_list. + */ +CreateOperatorClass CreateOperatorClass(): +{ + CreateOperatorClass statement = new CreateOperatorClass(); String name; ColDataType type; + OperatorClassItem item; +} +{ + ContextualKeyword("CLASS") name=TypeDdlName() { statement.setName(name); } + [ { statement.setDefaultClass(true); } ] + type=ColDataType() { statement.setDataType(type); } + name=RelObjectName() { statement.setAccessMethod(name); } + [ LOOKAHEAD({ isKeywordAhead("FAMILY") }) ContextualKeyword("FAMILY") + name=TypeDdlName() { statement.setFamily(name); } ] + item=OperatorClassItem() { statement.getItems().add(item); } + ( "," item=OperatorClassItem() { statement.getItems().add(item); } )* + { return statement; } +} + +/** PostgreSQL's CREATE OPERATOR FAMILY name USING index_method. */ +CreateOperatorFamily CreateOperatorFamily(): +{ CreateOperatorFamily statement = new CreateOperatorFamily(); String name; } +{ + ContextualKeyword("FAMILY") name=TypeDdlName() { statement.setName(name); } + name=RelObjectName() { statement.setAccessMethod(name); } + { return statement; } +} + +/** + * PostgreSQL's opclass_item: OPERATOR n operator [ (left, right) ] [ FOR SEARCH | FOR ORDER BY + * family ], FUNCTION n [ (type [, ...]) ] function [ (arguments) ], or STORAGE type. + */ +OperatorClassItem OperatorClassItem(): +{ + OperatorClassItem item = new OperatorClassItem(); Token number; String name; + ExclusionOperator operator; OperatorSignature signature; RoutineReference function; + List types; ColDataType type; +} +{ + ( + LOOKAHEAD({ isKeywordAhead("OPERATOR") }) ContextualKeyword("OPERATOR") + number= { item.setKind(OperatorClassItem.Kind.OPERATOR); item.setNumber(operatorClassNumber(number)); } + ( + LOOKAHEAD(PostgreSqlOperatorReference() "(") signature=OperatorSignature() + { + item.setOperator(signature.getOperator()); + item.setOperandTypes(new ArrayList( + Arrays.asList(signature.getLeftType(), signature.getRightType()))); + } + | + operator=PostgreSqlOperatorReference() { item.setOperator(operator); } + ) + [ + + ( { item.setPurpose(OperatorClassItem.Purpose.SEARCH); } + | name=TypeDdlName() + { item.setPurpose(OperatorClassItem.Purpose.ORDER_BY); item.setSortFamily(name); } ) + ] + | + + number= { item.setKind(OperatorClassItem.Kind.FUNCTION); item.setNumber(operatorClassNumber(number)); } + [ LOOKAHEAD(1) types=OperatorClassTypes() { item.setOperandTypes(types); } ] + function=RoutineReference(false) { item.setFunction(function); } + | + type=ColDataType() { item.setKind(OperatorClassItem.Kind.STORAGE); item.setStorageType(type); } + ) + { return item; } +} + +/** PostgreSQL's opclass_drop: OPERATOR n (type [, ...]) or FUNCTION n (type [, ...]). */ +OperatorClassItem OperatorClassDropItem(): +{ OperatorClassItem item = new OperatorClassItem(); Token number; List types; } +{ + ( + LOOKAHEAD({ isKeywordAhead("OPERATOR") }) ContextualKeyword("OPERATOR") + { item.setKind(OperatorClassItem.Kind.OPERATOR); } + | + { item.setKind(OperatorClassItem.Kind.FUNCTION); } + ) + number= { item.setNumber(operatorClassNumber(number)); } + types=OperatorClassTypes() { item.setOperandTypes(types); } + { return item; } +} + +/** PostgreSQL's type_list in parentheses. */ +List OperatorClassTypes(): +{ List types = new ArrayList(); ColDataType type; } +{ + "(" type=ColDataType() { types.add(type); } + ( "," type=ColDataType() { types.add(type); } )* ")" + { return types; } +} + +/** PostgreSQL ALTER OPERATOR, ALTER OPERATOR CLASS or ALTER OPERATOR FAMILY. */ +Statement PostgreSqlAlterOperator(): +{ Statement statement; } +{ + ( + LOOKAHEAD({ isOperatorClassOrFamilyAhead() && "CLASS".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("OPERATOR") statement=AlterOperatorClass() + | + LOOKAHEAD({ isOperatorClassOrFamilyAhead() }) + ContextualKeyword("OPERATOR") statement=AlterOperatorFamily() + | + ContextualKeyword("OPERATOR") statement=AlterOperator() + ) + { return statement; } +} + +/** + * PostgreSQL's ALTER OPERATOR operator_with_argtypes { OWNER TO role | SET SCHEMA schema | + * SET ( operator_def_list ) }. + */ +AlterOperator AlterOperator(): +{ + AlterOperator statement = new AlterOperator(); OperatorSignature operator; String name; + List properties; +} +{ + operator=OperatorSignature() { statement.setOperator(operator); } + ( + ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterOperator.Action.OWNER); statement.setNewName(name); } + | + + ( + name=RelObjectName() + { statement.setAction(AlterOperator.Action.SET_SCHEMA); statement.setNewName(name); } + | + properties=PostgreSqlDefinition() + { statement.setAction(AlterOperator.Action.SET_PROPERTIES); statement.setProperties(properties); } + ) + ) + { return statement; } +} + +/** + * PostgreSQL's ALTER OPERATOR CLASS name USING index_method { RENAME TO name | OWNER TO role | + * SET SCHEMA schema }. + */ +AlterOperatorClass AlterOperatorClass(): +{ AlterOperatorClass statement = new AlterOperatorClass(); String name; } +{ + ContextualKeyword("CLASS") name=TypeDdlName() { statement.setName(name); } + name=RelObjectName() { statement.setAccessMethod(name); } + ( + name=RelObjectName() { statement.setAction(AlterOperatorClass.Action.RENAME); } + | ContextualKeyword("OWNER") name=RelObjectName() { statement.setAction(AlterOperatorClass.Action.OWNER); } + | name=RelObjectName() { statement.setAction(AlterOperatorClass.Action.SET_SCHEMA); } + ) + { statement.setNewName(name); return statement; } +} + +/** + * PostgreSQL's ALTER OPERATOR FAMILY name USING index_method { ADD opclass_item_list | + * DROP opclass_drop_list | RENAME TO name | OWNER TO role | SET SCHEMA schema }. + */ +AlterOperatorFamily AlterOperatorFamily(): +{ AlterOperatorFamily statement = new AlterOperatorFamily(); String name = null; OperatorClassItem item; } +{ + ContextualKeyword("FAMILY") name=TypeDdlName() { statement.setName(name); } + name=RelObjectName() { statement.setAccessMethod(name); name = null; } + ( + { statement.setAction(AlterOperatorFamily.Action.ADD); } + item=OperatorClassItem() { statement.getItems().add(item); } + ( "," item=OperatorClassItem() { statement.getItems().add(item); } )* + | + { statement.setAction(AlterOperatorFamily.Action.DROP); } + item=OperatorClassDropItem() { statement.getItems().add(item); } + ( "," item=OperatorClassDropItem() { statement.getItems().add(item); } )* + | name=RelObjectName() { statement.setAction(AlterOperatorFamily.Action.RENAME); } + | ContextualKeyword("OWNER") name=RelObjectName() { statement.setAction(AlterOperatorFamily.Action.OWNER); } + | name=RelObjectName() { statement.setAction(AlterOperatorFamily.Action.SET_SCHEMA); } + ) + { statement.setNewName(name); return statement; } +} + +/** + * PostgreSQL's CREATE [ OR REPLACE ] AGGREGATE name aggr_args definition, and the old form + * CREATE AGGREGATE name old_aggr_definition, whose definition names the input type. + */ +CreateAggregate CreateAggregate(boolean orReplace): +{ + CreateAggregate statement = new CreateAggregate(); String name; + RoutineReference aggregate; List definition; +} +{ + { statement.setOrReplace(orReplace); } + ( + LOOKAHEAD({ isOldAggregateDefinitionAhead() }) + name=TypeDdlName() { aggregate = new RoutineReference(); aggregate.setName(name); } + definition=PostgreSqlDefinition() { requireOldAggregateDefinition(definition); } + | + aggregate=RoutineReference(true) definition=PostgreSqlDefinition() + ) + { statement.setAggregate(aggregate); statement.setDefinition(definition); return statement; } +} + +/** + * PostgreSQL's ALTER AGGREGATE name aggr_args { RENAME TO name | OWNER TO role | + * SET SCHEMA schema }. + */ +AlterAggregate AlterAggregate(): +{ AlterAggregate statement = new AlterAggregate(); RoutineReference aggregate; String name; } +{ + aggregate=RoutineReference(true) { statement.setAggregate(aggregate); } + ( + name=RelObjectName() { statement.setAction(AlterAggregate.Action.RENAME); } + | ContextualKeyword("OWNER") name=RelObjectName() { statement.setAction(AlterAggregate.Action.OWNER); } + | name=RelObjectName() { statement.setAction(AlterAggregate.Action.SET_SCHEMA); } + ) + { statement.setNewName(name); return statement; } +} + RangeTypeDefinition.Option RangeTypeOption(): { String name; RangeTypeDefinition.OptionKind kind; ColDataType type; } { @@ -27634,18 +28056,22 @@ RoutineReference RoutineReference(boolean aggregate): ) ")" ] - { requireTypeDdlSyntax(!aggregate || result.getArguments() != null, "An aggregate signature is required"); return result; } + { requireTypeDdlSyntax(!aggregate || isAggregateSignature(result), "An aggregate signature is required"); return result; } } String ExtensionOperatorName(): { ExclusionOperator operator; } { operator=PostgreSqlOperatorReference() { return operator.getQualifiedName(); } } -/** Shared schema-qualified operator spelling used in DDL references. */ +/** + * Shared schema-qualified operator spelling used in DDL references. A database may precede the + * schema, as in db.schema.+, and is then a part of the schema name. + */ ExclusionOperator PostgreSqlOperatorReference(): { String schema = null; String name; } { - [ LOOKAHEAD(RelObjectName() ".") schema=RelObjectName() "." ] + ( LOOKAHEAD(RelObjectName() ".") name=RelObjectName() "." + { schema = schema == null ? name : schema + "." + name; } )* name=PostgreSqlOperatorName() { return new ExclusionOperator().withSchemaName(schema).withName(name); } } @@ -27663,7 +28089,7 @@ String PostgreSqlOperatorName(): && getToken(1).beginColumn == getToken(0).endColumn + 1 }) token=PostgreSqlOperatorPart() { name.append(token.image); } )* - { return name.toString(); } + { requirePostgreSqlOperatorName(name.toString()); return name.toString(); } } Token PostgreSqlOperatorPart(): @@ -27679,8 +28105,14 @@ Token PostgreSqlOperatorPart(): | token="~~~" | token="!" | token= | token="@>" | token="<@" | token="@@" | token= | token="?" | token="?|" | token="?&" | token="->" | token="->>" | token= | token="#>" | token="#>>" | token= | token="-#" | token="<->" | token="<#>" + // "=>" is no operator by itself, but it is a part of one such as <==> + | token= + // the lexer reads @ before # as the start of a variable name, as in @#@ + | token= ) { + requireTypeDdlSyntax(token.kind != S_AT_IDENTIFIER || token.image.matches("[@#]+"), + "Expected an operator but found " + token.image); // PostgreSQL reads "< =" as two operators, while the lexer tolerates whitespace inside it requireTypeDdlSyntax(!token.image.matches("(?s).*\\s.*"), "Unexpected whitespace in operator " + token.image); diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index b03b08dbb..701632f4a 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -188,6 +188,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``CREATE``/``ALTER EVENT TRIGGER`` · ``REINDEX`` · ``SECURITY LABEL`` * - **PostgreSQL types and rules** - ``CREATE TYPE`` (shell, composite, enum, range and base types) · ``ALTER TYPE`` · ``CREATE RULE`` · ``ALTER RULE`` + * - **PostgreSQL operators and aggregates** + - ``CREATE``/``ALTER OPERATOR`` · ``CREATE``/``ALTER OPERATOR CLASS`` · ``CREATE``/``ALTER OPERATOR FAMILY`` · ``CREATE``/``ALTER AGGREGATE`` * - **Salesforce SOQL** - ``INCLUDES`` · ``EXCLUDES`` diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index b6de3a5d7..bd313601b 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1181,6 +1181,94 @@ Type names, domain names and routine signatures are not reported as tables by `` See PostgreSQL's documentation for `CREATE TYPE `_, `ALTER TYPE `_, `CREATE DOMAIN `_ and `ALTER EXTENSION `_. +Inspect operator and aggregate definitions +========================================== + +PostgreSQL's operator and aggregate DDL parses to typed statements that reuse the models of +``DROP OPERATOR``, ``DROP AGGREGATE`` and ``CREATE TYPE``: an operator is an ``ExclusionOperator`` +(schema and symbol) or an ``OperatorSignature`` (with its operand types, ``null`` for ``NONE``), an +aggregate is a ``RoutineReference``, and a definition list holds ``DefinitionElement`` items with a +``DefinitionArgument``. PostgreSQL checks the names and values of a definition when it runs the +statement, so they are not restricted. + +.. list-table:: + :header-rows: 1 + :widths: 45 55 + + * - Statement + - Model + * - ``CREATE OPERATOR [schema.]op (definition)`` + - ``CreateOperator``: ``getOperator()``, ``getDefinition()``, ``getDefinitionElement(name)`` + * - ``ALTER OPERATOR op (left, right) {OWNER TO | SET SCHEMA | SET (...)}`` + - ``AlterOperator``: ``getOperator()``, ``getAction()``, ``getNewName()``, ``getProperties()`` + * - ``CREATE OPERATOR CLASS name [DEFAULT] FOR TYPE type USING method [FAMILY family] AS ...`` + - ``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 ... | DROP ... | RENAME TO | OWNER TO | SET SCHEMA}`` + - ``AlterOperatorFamily``: ``getName()``, ``getAccessMethod()``, ``getAction()``, + ``getItems()``, ``getNewName()`` + * - ``CREATE [OR REPLACE] AGGREGATE name (arguments) (definition)`` and the old + ``CREATE AGGREGATE name (BASETYPE = type, ...)`` + - ``CreateAggregate``: ``isOrReplace()``, ``getAggregate()``, ``isOldStyle()``, + ``getDefinition()``, ``getDefinitionElement(name)`` + * - ``ALTER AGGREGATE name (arguments) {RENAME TO | OWNER TO | SET SCHEMA}`` + - ``AlterAggregate``: ``getAggregate()``, ``getAction()``, ``getNewName()`` + +``getNewName()`` is the new name, the role or the schema of the action. An ``OperatorClassItem`` +is one member of an operator class or family: its ``Kind`` is ``OPERATOR``, ``FUNCTION`` or +``STORAGE``, with the strategy or support number, the operator and its optional operand types, the +``Purpose`` (``FOR SEARCH`` or ``FOR ORDER BY`` with ``getSortFamily()``), the function as a +``RoutineReference``, or the storage type. A member that ``ALTER OPERATOR FAMILY ... DROP`` removes +has a kind, a number and operand types only. + +.. code-block:: java + + CreateOperator operator = (CreateOperator) CCJSqlParserUtil.parse( + "CREATE OPERATOR app.=== (FUNCTION = app.eq, LEFTARG = int, RIGHTARG = int, " + + "NEGATOR = OPERATOR(app.!==), HASHES)"); + operator.getOperator().getSchemaName(); // app + operator.getOperator().getName(); // === + operator.getDefinitionElement("NEGATOR").getArgument().getOperator(); // app.!== + operator.getDefinitionElement("HASHES").getArgument(); // null + + CreateOperatorClass operatorClass = (CreateOperatorClass) CCJSqlParserUtil.parse( + "CREATE OPERATOR CLASS oc DEFAULT FOR TYPE int4 USING btree AS " + + "OPERATOR 1 < (int4, int4), FUNCTION 1 btint4cmp(int4, int4)"); + OperatorClassItem item = operatorClass.getItems().get(0); + item.getNumber(); // 1 + item.getOperator().getName(); // < + item.getOperandTypes(); // [int4, int4] + + CreateAggregate aggregate = (CreateAggregate) CCJSqlParserUtil.parse( + "CREATE AGGREGATE app.total (numeric ORDER BY int) (SFUNC = app.add, STYPE = numeric)"); + aggregate.getAggregate().getName(); // app.total + aggregate.getAggregate().getOrderByArguments(); // [int] + aggregate.getDefinitionElement("STYPE").getArgument().getDataType(); // numeric + +An operator name is one name with its exact text, such as ``===``, ``<==>`` or ``@#@``, wherever +PostgreSQL DDL names an operator. It follows the rules of PostgreSQL's lexer: the parts must touch +each other, a name of several characters ends in ``+`` or ``-`` only if it contains one of +``~ ! @ # % ^ & | ?``, ``=>`` alone is not an operator, and a name has at most 63 characters. A +database may precede the schema (``db.schema.+``); ``getSchemaName()`` then returns both. An +operator that contains a backtick is not supported, and without ``Dialect.POSTGRESQL`` the lexer +reads ``#`` before ``#``, ``@`` or a letter as the start of a name. An aggregate signature is +``*`` or has at least one argument, so ``agg()`` is rejected wherever an aggregate is named. + +Without a dialect, ``CREATE OPERATOR`` and ``ALTER OPERATOR`` are read this way when an operator +symbol follows, so Oracle's ``CREATE OPERATOR name BINDING ...`` stays an +``UnsupportedStatement``, and so do MySQL's ``CREATE AGGREGATE FUNCTION`` and SQL Server's +``CREATE AGGREGATE name (@parameter type) ...``. With ``Dialect.POSTGRESQL`` they are parse +errors. Validators report ``createOperator``, ``alterOperator``, ``createOperatorClass``, +``alterOperatorClass``, ``createOperatorFamily``, ``alterOperatorFamily``, ``createAggregate`` +and ``alterAggregate``, which PostgreSQL enables. + +See `CREATE OPERATOR `_, `ALTER OPERATOR `_, `CREATE OPERATOR CLASS `_, `ALTER OPERATOR CLASS `_, `CREATE OPERATOR FAMILY `_, `ALTER OPERATOR FAMILY `_, `CREATE AGGREGATE `_ and `ALTER AGGREGATE `_. + Classify a Statement ============================== diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java index b4f179fc3..da29a316d 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -172,6 +172,23 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException { "DROP CAST IF EXISTS (int[] AS public.t) CASCADE", "DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint)", "DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u", + "CREATE OPERATOR public.=== (FUNCTION = s.f, LEFTARG = int[], COMMUTATOR = <==>, " + + "NEGATOR = OPERATOR(s.!==), HASHES)", + "ALTER OPERATOR s.@#@ (NONE, varchar(3)) SET (RESTRICT = NONE, JOIN = s.f, x = 1)", + "ALTER OPERATOR + (int, int) OWNER TO u", + "CREATE OPERATOR CLASS s.oc DEFAULT FOR TYPE varchar(3) USING gist FAMILY s.fam AS " + + "OPERATOR 1 s.< (int, int[]) FOR ORDER BY s.float_ops, OPERATOR 2 <=, " + + "FUNCTION 1 (int, text) s.f(IN a int, text), STORAGE double precision", + "ALTER OPERATOR CLASS s.oc USING btree SET SCHEMA t", + "CREATE OPERATOR FAMILY s.fam USING btree", + "ALTER OPERATOR FAMILY s.fam USING btree ADD OPERATOR 1 < (int4, int2) FOR SEARCH, " + + "FUNCTION 1 (int4) cmp(int4, int2)", + "ALTER OPERATOR FAMILY s.fam USING btree DROP OPERATOR 1 (int4, int2), " + + "FUNCTION 1 (varchar(3))", + "CREATE OR REPLACE AGGREGATE s.agg (a int, VARIADIC b text[] ORDER BY c numeric(3, 1)) " + + "(SFUNC = s.f, STYPE = int[], SORTOP = <, INITCOND = '0', MSSPACE = 8)", + "CREATE AGGREGATE agg (BASETYPE = varchar(3), SFUNC = f, STYPE = int)", + "ALTER AGGREGATE s.agg (int, varchar(3)) RENAME TO agg2", "SELECT a FROM t AS x USE KEY (i1) FORCE INDEX FOR JOIN (i2, i3) WHERE a = 1", "SELECT a FROM t USE INDEX () JOIN s IGNORE INDEX FOR ORDER BY (i1) ON t.a = s.a", "UPDATE t USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2) SET a = 1", diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java index d31b4586c..cc27c7c09 100644 --- a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java @@ -16,7 +16,11 @@ import java.util.Set; import java.util.stream.Stream; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -25,6 +29,10 @@ import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.test.TestUtils; @@ -49,6 +57,20 @@ static Stream definitions() { Arguments.of("ALTER FUNCTION f READS SQL DATA", AlterRoutine.class), Arguments.of("ALTER TRIGGER t ON tbl RENAME TO t2", AlterTrigger.class), Arguments.of("ALTER RULE r ON tbl RENAME TO r2", AlterRule.class), + Arguments.of("CREATE OPERATOR === (FUNCTION = f, LEFTARG = int)", + CreateOperator.class), + Arguments.of("ALTER OPERATOR === (int, int) SET (RESTRICT = NONE)", + AlterOperator.class), + Arguments.of("CREATE OPERATOR CLASS oc FOR TYPE int USING btree AS OPERATOR 1 <", + CreateOperatorClass.class), + Arguments.of("ALTER OPERATOR CLASS oc USING btree RENAME TO oc2", + AlterOperatorClass.class), + Arguments.of("CREATE OPERATOR FAMILY f USING btree", CreateOperatorFamily.class), + Arguments.of("ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 (int, int)", + AlterOperatorFamily.class), + Arguments.of("CREATE AGGREGATE agg(int) (SFUNC = f, STYPE = int)", + CreateAggregate.class), + Arguments.of("ALTER AGGREGATE agg(*) OWNER TO u", AlterAggregate.class), Arguments.of("ALTER SCHEMA s RENAME TO s2", AlterSchema.class), Arguments.of("ALTER SCHEMA s OWNER TO CURRENT_USER", AlterSchema.class), Arguments.of("ALTER STATISTICS s RENAME TO s2", AlterStatistics.class), diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 095fb8c0a..640cf0fcb 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -208,12 +208,9 @@ void testInformixSetStatementIssue1945() throws JSQLParserException { @ValueSource(strings = { "CREATE LANGUAGE plperl", "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler", - "CREATE OPERATOR CLASS gist__int_ops DEFAULT FOR TYPE _int4 USING gist AS OPERATOR 3 &&", - "CREATE OPERATOR FAMILY myfam USING btree", "CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM latin1_to_utf8", "CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (FROM SQL WITH FUNCTION " - + "hstore_to_plpython(internal), TO SQL WITH FUNCTION plpython_to_hstore(internal))", - "CREATE OR REPLACE AGGREGATE myavg(float8) (SFUNC = float8_accum, STYPE = float8[])"}) + + "hstore_to_plpython(internal), TO SQL WITH FUNCTION plpython_to_hstore(internal))"}) void unknownCreateFallsBackToUnsupportedStatement(String sql) throws JSQLParserException { for (Consumer dialect : Arrays.>asList(parser -> { }, parser -> parser.withDialect(Dialect.POSTGRESQL))) { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAggregateDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAggregateDdlTest.java new file mode 100644 index 000000000..798eb1102 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAggregateDdlTest.java @@ -0,0 +1,307 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.DefinitionArgument; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.AlterAggregate; +import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL CREATE AGGREGATE and ALTER AGGREGATE, see + * https://www.postgresql.org/docs/18/sql-createaggregate.html and + * https://www.postgresql.org/docs/18/sql-alteraggregate.html. + */ +class PostgreSqlAggregateDdlTest extends ValidationTestAsserts { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static Statement assertStatement(String sql, Class type) + throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertThat(statement).isInstanceOf(type).hasToString(sql); + assertThat(assertSqlCanBeParsedAndDeparsed(sql)).isInstanceOf(type); + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + statement.accept(deparser); + assertThat(deparser.getBuilder()).hasToString(sql); + assertThat(new TablesNamesFinder().getTables(statement)).isEmpty(); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + + Statements script = CCJSqlParserUtil.parseStatements( + sql + "; SELECT 1; " + sql + ";\n" + sql, POSTGRESQL); + assertThat(script).hasSize(4); + assertThat(script.get(0)).isInstanceOf(type).hasToString(sql); + assertThat(script.get(2)).isInstanceOf(type).hasToString(sql); + assertThat(script.get(3)).isInstanceOf(type).hasToString(sql); + return statement; + } + + private static void assertRejected(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE AGGREGATE agg(int) (SFUNC = f, STYPE = int, INITCOND = '0')", + "CREATE OR REPLACE AGGREGATE agg(ORDER BY int) (SFUNC = f, STYPE = internal, " + + "FINALFUNC = g, HYPOTHETICAL)", + "CREATE AGGREGATE s.agg(*) (SFUNC = f, STYPE = int, PARALLEL = SAFE)", + "CREATE AGGREGATE agg2(int, text) (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(a int, b text) (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(VARIADIC a int[]) (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(VARIADIC \"any\") (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(IN a int) (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(float8 ORDER BY float8) (SFUNC = f, STYPE = internal, " + + "FINALFUNC = g, FINALFUNC_EXTRA)", + "CREATE AGGREGATE agg2(a int, b text ORDER BY c int, VARIADIC d text[]) " + + "(SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(varchar (3), int[], double precision) (SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg2(int) (SFUNC = g, STYPE = int, SORTOP = <, MSFUNC = g, " + + "MINVFUNC = g, MSTYPE = int, MSSPACE = 8, SSPACE = 0, " + + "FINALFUNC_MODIFY = READ_ONLY, COMBINEFUNC = g)", + "CREATE AGGREGATE agg2(int) (SFUNC = g, STYPE = int, " + + "SORTOP = OPERATOR(pg_catalog.>), INITCOND = '0')", + "CREATE AGGREGATE agg2(int) (SFUNC = g, stype = int[], initcond = '{}', " + + "parallel = restricted)", + "CREATE AGGREGATE \"Agg\"(int) (\"SFUNC\" = g, STYPE = int)", + "CREATE AGGREGATE d.public.agg3(int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg7(int) (HYPOTHETICAL)", + // the old form + "CREATE AGGREGATE agg (BASETYPE = int, SFUNC = f, STYPE = int)", + "CREATE AGGREGATE agg4 (sfunc = g, stype = int, basetype = int, initcond = '0')", + "CREATE AGGREGATE agg5 (basetype = 'ANY', sfunc = g, stype = int)", + "CREATE AGGREGATE agg5 (\"parallel\" = safe, sfunc = g, stype = int)", + "CREATE AGGREGATE s.agg6 (sfunc1 = g, basetype = int, stype1 = int)", + "CREATE OR REPLACE AGGREGATE agg (sfunc = g, stype = int, basetype = int)"}) + void testCreateAggregate(String sql) throws Exception { + assertStatement(sql, CreateAggregate.class); + } + + @Test + void testAggregateNamedFunction() throws Exception { + // FUNCTION is no reserved word in PostgreSQL, while MySQL has CREATE AGGREGATE FUNCTION + String sql = "CREATE AGGREGATE function(int) (SFUNC = g, STYPE = int)"; + assertThat(assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL)) + .isInstanceOf(CreateAggregate.class); + assertThat(CCJSqlParserUtil.parse(sql)).isInstanceOf(UnsupportedStatement.class); + } + + @Test + void testCreateAggregateFields() throws Exception { + CreateAggregate aggregate = (CreateAggregate) CCJSqlParserUtil.parse( + "CREATE OR REPLACE AGGREGATE app.agg (a int, VARIADIC b text[] ORDER BY c float8) " + + "(SFUNC = app.f, STYPE = internal, SORTOP = <, INITCOND = '0', " + + "HYPOTHETICAL)", + POSTGRESQL); + assertThat(aggregate.isOrReplace()).isTrue(); + assertThat(aggregate.isOldStyle()).isFalse(); + RoutineReference signature = aggregate.getAggregate(); + assertThat(signature.getName()).isEqualTo("app.agg"); + assertThat(signature.isAllArguments()).isFalse(); + assertThat(signature.getArguments()).hasSize(2); + assertThat(signature.getArguments().get(0).getName()).isEqualTo("a"); + assertThat(signature.getArguments().get(1).getMode()) + .isEqualTo(RoutineReference.Argument.Mode.VARIADIC); + assertThat(signature.getArguments().get(1).getDataType()).hasToString("text[]"); + assertThat(signature.getOrderByArguments()).hasSize(1); + assertThat(aggregate.getDefinition()).hasSize(5); + assertThat(aggregate.getDefinitionElement("sfunc").getArgument().getDataType() + .getDataType()).isEqualTo("app.f"); + assertThat(aggregate.getDefinitionElement("SORTOP").getArgument().getKind()) + .isEqualTo(DefinitionArgument.Kind.OPERATOR); + assertThat(aggregate.getDefinitionElement("INITCOND").getArgument().getString().getValue()) + .isEqualTo("0"); + assertThat(aggregate.getDefinitionElement("HYPOTHETICAL").getArgument()).isNull(); + assertThat(aggregate.getDefinitionElement("FINALFUNC")).isNull(); + assertThat(aggregate.getASTNode()).isNotNull(); + + aggregate.setOrReplace(false); + signature.setOrderByArguments(null); + aggregate.getDefinition().subList(2, 5).clear(); + assertThat(aggregate).hasToString("CREATE AGGREGATE app.agg(a int, VARIADIC b text[]) " + + "(SFUNC = app.f, STYPE = internal)"); + + CreateAggregate all = (CreateAggregate) CCJSqlParserUtil.parse( + "CREATE AGGREGATE cnt (*) (SFUNC = f, STYPE = int8)", POSTGRESQL); + assertThat(all.getAggregate().isAllArguments()).isTrue(); + assertThat(all.isOldStyle()).isFalse(); + + CreateAggregate old = (CreateAggregate) CCJSqlParserUtil.parse( + "CREATE AGGREGATE app.agg (BASETYPE = int, SFUNC = f, STYPE = int)", POSTGRESQL); + assertThat(old.isOldStyle()).isTrue(); + assertThat(old.isOrReplace()).isFalse(); + assertThat(old.getAggregate().getName()).isEqualTo("app.agg"); + assertThat(old.getAggregate().getArguments()).isNull(); + assertThat(old.getDefinitionElement("basetype").getArgument().getDataType()) + .hasToString("int"); + assertThat(new CreateAggregate().isOldStyle()).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE AGGREGATE agg8 () (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (int)", + "CREATE AGGREGATE agg8 (int) ()", + "CREATE AGGREGATE agg8", + "CREATE AGGREGATE agg8 (int) (SFUNC = g, STYPE = )", + "CREATE AGGREGATE agg8 (*, int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (int ORDER BY) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (ORDER BY int ORDER BY int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (int DEFAULT 1) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (DISTINCT int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE IF NOT EXISTS agg8 (int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE agg8 (int) (SFUNC = g, STYPE = int) OWNER TO u", + "CREATE AGGREGATE agg8 (int) SFUNC = g, STYPE = int", + "CREATE AGGREGATE agg8 (int) (SFUNC = g, STYPE = int) (x = 1)", + "CREATE AGGREGATE FUNCTION agg8 (int) (SFUNC = g, STYPE = int)", + "CREATE AGGREGATE FUNCTION f RETURNS STRING SONAME 'x.so'", + // the old form takes identifiers with values only, and no keyword is an identifier + "CREATE AGGREGATE agg8 (SFUNC = g, STYPE = int, HYPOTHETICAL)", + "CREATE AGGREGATE agg8 (sfunc = g, parallel = safe, stype = int)", + "CREATE AGGREGATE agg8 (parallel = safe, sfunc = g, stype = int)", + "CREATE AGGREGATE agg8 (sfunc = g, type = int)", + "CREATE AGGREGATE agg8 (sfunc = g, int = int)", + "CREATE AGGREGATE agg8 (sfunc = g, between = int)", + "CREATE AGGREGATE agg8 (sfunc = g, left = int)", + "CREATE AGGREGATE agg8 (sfunc = g, select = int)"}) + void testRejectedCreateAggregate(String sql) { + assertRejected(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER AGGREGATE agg(int) RENAME TO agg2", + "ALTER AGGREGATE agg(VARIADIC a int ORDER BY b text) OWNER TO u", + "ALTER AGGREGATE agg(*) SET SCHEMA s", + "ALTER AGGREGATE s.agg(int, text) OWNER TO CURRENT_USER", + "ALTER AGGREGATE agg(ORDER BY int) RENAME TO \"Agg 2\"", + "ALTER AGGREGATE agg(a int, b int ORDER BY c int) SET SCHEMA t"}) + void testAlterAggregate(String sql) throws Exception { + assertStatement(sql, AlterAggregate.class); + } + + @Test + void testAlterAggregateFields() throws Exception { + AlterAggregate rename = (AlterAggregate) CCJSqlParserUtil.parse( + "ALTER AGGREGATE app.agg (int, text) RENAME TO agg2", POSTGRESQL); + assertThat(rename.getAggregate().getName()).isEqualTo("app.agg"); + assertThat(rename.getAggregate().getArguments()).hasSize(2); + assertThat(rename.getAction()).isEqualTo(AlterAggregate.Action.RENAME); + assertThat(rename.getNewName()).isEqualTo("agg2"); + rename.setAction(AlterAggregate.Action.OWNER); + rename.setNewName("u"); + assertThat(rename).hasToString("ALTER AGGREGATE app.agg(int, text) OWNER TO u"); + rename.setAction(AlterAggregate.Action.SET_SCHEMA); + assertThat(rename).hasToString("ALTER AGGREGATE app.agg(int, text) SET SCHEMA u"); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER AGGREGATE agg RENAME TO agg2", + "ALTER AGGREGATE agg () RENAME TO agg2", + "ALTER AGGREGATE agg (int)", + "ALTER AGGREGATE agg (int) RENAME agg2", + "ALTER AGGREGATE agg (int) RENAME TO s.agg2", + "ALTER AGGREGATE agg (int) SET (x = 1)", + "ALTER AGGREGATE agg (int) SET SCHEMA", + "ALTER AGGREGATE agg (int), agg2 (int) OWNER TO u", + "ALTER AGGREGATE IF EXISTS agg (int) RENAME TO agg2", + "ALTER AGGREGATE agg (int) OWNER TO u, v", + "ALTER AGGREGATE agg (int) DEPENDS ON EXTENSION e", + "ALTER AGGREGATE agg (int) RENAME TO agg2 CASCADE"}) + void testRejectedAlterAggregate(String sql) { + assertRejected(sql); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + /** PostgreSQL's aggregate signature is "*" or has an argument, wherever one is read. */ + @ParameterizedTest + @ValueSource(strings = { + "DROP AGGREGATE agg ()", + "DROP AGGREGATE agg (int), agg2 ()", + "COMMENT ON AGGREGATE agg () IS 'x'"}) + void testEmptyAggregateSignatureIsRejected(String sql) { + assertRejected(sql); + } + + @Test + void testOtherDialectsKeepTheirAggregates() throws Exception { + String mysql = "CREATE AGGREGATE FUNCTION f RETURNS STRING SONAME 'x.so'"; + assertThat(CCJSqlParserUtil.parse(mysql)).isInstanceOf(UnsupportedStatement.class); + assertThat(CCJSqlParserUtil.parse(mysql, parser -> parser.withDialect(Dialect.MYSQL))) + .isInstanceOf(UnsupportedStatement.class); + String sqlServer = "CREATE AGGREGATE dbo.Concatenate (@input nvarchar(200)) " + + "RETURNS nvarchar(max) EXTERNAL NAME StringUtilities.Concatenate"; + assertThat(CCJSqlParserUtil.parse(sqlServer)).isInstanceOf(UnsupportedStatement.class); + assertThat(CCJSqlParserUtil.parse(sqlServer, + parser -> parser.withDialect(Dialect.SQLSERVER))) + .isInstanceOf(UnsupportedStatement.class); + } + + @Test + void testVisitorsAndValidation() throws Exception { + String create = "CREATE AGGREGATE agg(int) (SFUNC = f, STYPE = int)"; + validateNoErrors(create, 1, FeaturesAllowed.CREATE); + validateNotAllowed(create, 1, 1, FeaturesAllowed.SELECT, Feature.createAggregate); + validate(create, 1, 1, MySqlVersion.V8_0); + String alter = "ALTER AGGREGATE agg(int) RENAME TO agg2"; + validateNoErrors(alter, 1, FeaturesAllowed.ALTER); + validateNotAllowed(alter, 1, 1, FeaturesAllowed.SELECT, Feature.alterAggregate); + validate(alter, 1, 1, MySqlVersion.V8_0); + + StatementVisitorAdapter visitor = new StatementVisitorAdapter() { + @Override + public String visit(CreateAggregate statement, S context) { + return context + ":" + statement.getAggregate().getName(); + } + + @Override + public String visit(AlterAggregate statement, S context) { + return context + ":" + statement.getNewName(); + } + }; + assertThat(CCJSqlParserUtil.parse(create, POSTGRESQL).accept(visitor, "c")) + .isEqualTo("c:agg"); + assertThat(CCJSqlParserUtil.parse(alter, POSTGRESQL).accept(visitor, "c")) + .isEqualTo("c:agg2"); + assertThat(CCJSqlParserUtil.parse(create, POSTGRESQL) + .accept(new StatementVisitorAdapter(), "c")).isNull(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlOperatorDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlOperatorDdlTest.java new file mode 100644 index 000000000..dc69835b2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlOperatorDdlTest.java @@ -0,0 +1,719 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.DefinitionArgument; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.AlterOperator; +import net.sf.jsqlparser.statement.alter.AlterOperatorClass; +import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; +import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.operator.CreateOperator; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; +import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; +import net.sf.jsqlparser.statement.create.operator.OperatorClassItem; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL CREATE and ALTER OPERATOR, OPERATOR CLASS and OPERATOR FAMILY, see + * https://www.postgresql.org/docs/18/sql-createoperator.html, + * https://www.postgresql.org/docs/18/sql-alteroperator.html, + * https://www.postgresql.org/docs/18/sql-createopclass.html, + * https://www.postgresql.org/docs/18/sql-alteropclass.html, + * https://www.postgresql.org/docs/18/sql-createopfamily.html and + * https://www.postgresql.org/docs/18/sql-alteropfamily.html. + */ +class PostgreSqlOperatorDdlTest extends ValidationTestAsserts { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static Statement assertStatement(String sql, Class type) + throws Exception { + assertThat(assertSqlCanBeParsedAndDeparsed(sql)).isInstanceOf(type); + return assertPostgreSqlStatement(sql, type); + } + + /** For an operator such as ##, which only the PostgreSQL dialect does not lex as a name. */ + private static Statement assertPostgreSqlStatement(String sql, + Class type) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertThat(statement).isInstanceOf(type).hasToString(sql); + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + statement.accept(deparser); + assertThat(deparser.getBuilder()).hasToString(sql); + assertThat(new TablesNamesFinder().getTables(statement)).isEmpty(); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + + Statements script = CCJSqlParserUtil.parseStatements( + sql + "; SELECT 1; " + sql + ";\n" + sql, POSTGRESQL); + assertThat(script).hasSize(4); + assertThat(script.get(0)).isInstanceOf(type).hasToString(sql); + assertThat(script.get(2)).isInstanceOf(type).hasToString(sql); + assertThat(script.get(3)).isInstanceOf(type).hasToString(sql); + return statement; + } + + private static void assertRejected(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR === (FUNCTION = f, LEFTARG = int, RIGHTARG = int)", + "CREATE OPERATOR s.<-> (PROCEDURE = s.f, LEFTARG = point, RIGHTARG = point, " + + "COMMUTATOR = <->, NEGATOR = OPERATOR(s.), RESTRICT = eqsel, " + + "JOIN = eqjoinsel, HASHES, MERGES)", + "CREATE OPERATOR public.!== (function = f, leftarg = int, rightarg = int, negator = ===)", + "CREATE OPERATOR d.public.=== (FUNCTION = f, LEFTARG = int, RIGHTARG = int)", + "CREATE OPERATOR \"s\".@@@ (FUNCTION = f, LEFTARG = int, RIGHTARG = int)", + "CREATE OPERATOR class.+ (FUNCTION = f)", + "CREATE OPERATOR family.+ (FUNCTION = f)", + "CREATE OPERATOR <#> (FUNCTION = f)", + "CREATE OPERATOR <#> (HASHES)", + "CREATE OPERATOR <#> (FUNCTION = f, LEFTARG = varchar (3), RIGHTARG = int[], " + + "\"Quoted\" = 'x', n = 1, m = -1.5, k = true, j = NONE, i = SETOF int)", + "CREATE OPERATOR <#> (select = f, table = int)"}) + void testCreateOperator(String sql) throws Exception { + assertStatement(sql, CreateOperator.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + /** PostgreSQL reads any run of operator characters as one operator. */ + @ParameterizedTest + @ValueSource(strings = { + "+", "-", "*", "/", "%", "^", "<", ">", "=", "~", "!", "@", "#", "&", "|", "?", + "<=", ">=", "<>", "!=", "==", "===", "<->", "<==>", "@@", "@@@", "@#@", "@#", "#@", + "<<|", "&<|", "|>>", "~=", "<^", ">^", "?#", "?-|", "?||", "##", "@-@", "!!", "|/", + "||/", "*<>", "~<~", "^@", "<<=", "-|-", "&&&", "~~*", "!~~*", "#-", "<%", "%>", + "<<%", "->>", "#>>", "?&", "%%", "|&>", "&<", "&>", ">>=", "<<->>", "=*", "*=", + "<=>", "@>", "<@", ">>", "<<", "|>", "||", "&&", "^^", "**", "~*", "^=", "-#", + "@?", "#>", "@-", "~+", "@+", "~-", "-+-|", "+*", "-="}) + void testOperatorNameIsOneNameWithItsExactText(String operator) throws Exception { + String create = "CREATE OPERATOR s." + operator + + " (FUNCTION = f, COMMUTATOR = " + operator + ", NEGATOR = OPERATOR(s." + + operator + "))"; + CreateOperator created = + (CreateOperator) assertPostgreSqlStatement(create, CreateOperator.class); + // without the dialect, the lexer reads # before # or @ as the start of a name + if (!operator.startsWith("##") && !operator.startsWith("#@")) { + assertThat(assertSqlCanBeParsedAndDeparsed(create)).isInstanceOf(CreateOperator.class); + } + assertThat(created.getOperator().getSchemaName()).isEqualTo("s"); + assertThat(created.getOperator().getName()).isEqualTo(operator); + assertThat(created.getDefinitionElement("commutator").getArgument().getOperator()) + .isEqualTo(operator); + assertThat(created.getDefinitionElement("NEGATOR").getArgument().getOperator()) + .isEqualTo("s." + operator); + + String alter = "ALTER OPERATOR " + operator + " (int, int) OWNER TO u"; + AlterOperator altered = + (AlterOperator) assertPostgreSqlStatement(alter, AlterOperator.class); + assertThat(altered.getOperator().getOperator().getName()).isEqualTo(operator); + + String family = "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 " + operator + + " (int, int), OPERATOR 2 " + operator; + AlterOperatorFamily added = (AlterOperatorFamily) assertPostgreSqlStatement(family, + AlterOperatorFamily.class); + assertThat(added.getItems().get(0).getOperator().getName()).isEqualTo(operator); + assertThat(added.getItems().get(1).getOperator().getName()).isEqualTo(operator); + + String drop = "DROP OPERATOR " + operator + " (int, int)"; + Drop dropped = (Drop) assertSqlCanBeParsedAndDeparsed(drop, false, POSTGRESQL); + assertThat(dropped.getOperators().get(0).getOperator().getName()).isEqualTo(operator); + } + + /** + * Texts PostgreSQL's lexer does not take as one operator: a name of several characters ends in + * + or - only if it has one of ~ ! @ # % ^ & | ` ?, and => is a token of its own. + */ + @ParameterizedTest + @ValueSource(strings = {"=>", "=-", "+-", "*-", "-+-", "<=+", "= =", "< =", "::", ":=", "..", + "myop", "@#x", "OPERATOR(===)", "\"===\"", "'==='", "(", ""}) + void testRejectedOperatorNames(String operator) { + assertRejected("CREATE OPERATOR " + operator + " (FUNCTION = f)"); + assertRejected("CREATE OPERATOR s." + operator + " (FUNCTION = f)"); + assertRejected("CREATE OPERATOR === (FUNCTION = f, COMMUTATOR = OPERATOR(" + operator + + "))"); + assertRejected("ALTER OPERATOR " + operator + " (int, int) OWNER TO u"); + assertRejected("ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 " + operator + + " (int, int)"); + assertRejected("DROP OPERATOR " + operator + " (int, int)"); + assertRejected("COMMENT ON OPERATOR " + operator + " (int, int) IS 'x'"); + } + + @Test + void testOperatorNameLength() throws Exception { + StringBuilder name = new StringBuilder(); + for (int i = 0; i < 63; i++) { + name.append('<'); + } + assertStatement("CREATE OPERATOR " + name + " (FUNCTION = f)", CreateOperator.class); + assertRejected("CREATE OPERATOR " + name + "< (FUNCTION = f)"); + } + + @Test + void testSharedOperatorReaders() throws Exception { + Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed( + "DROP OPERATOR d.public.<==> (int, int), @#@ (NONE, int)", false, POSTGRESQL); + assertThat(drop.getOperators().get(0).getOperator().getSchemaName()) + .isEqualTo("d.public"); + assertThat(drop.getOperators().get(0).getOperator().getName()).isEqualTo("<==>"); + assertThat(drop.getOperators().get(1).getOperator().getName()).isEqualTo("@#@"); + assertThat(assertSqlCanBeParsedAndDeparsed("COMMENT ON OPERATOR d.public.<==> (int, int)" + + " IS 'x'", false, POSTGRESQL)).isInstanceOf(Comment.class); + assertSqlCanBeParsedAndDeparsed("CREATE TYPE t (INPUT = f, x = <==>, y = OPERATOR(a.b.@#))", + false, POSTGRESQL); + // an Oracle operator keeps its name + assertThat(((Drop) CCJSqlParserUtil.parse("DROP OPERATOR a.b.c")).getOperators()) + .isEmpty(); + } + + @Test + void testCreateOperatorFields() throws Exception { + CreateOperator operator = (CreateOperator) CCJSqlParserUtil.parse( + "CREATE OPERATOR app.=== (FUNCTION = app.eq, LEFTARG = int, COMMUTATOR = ===, " + + "NEGATOR = OPERATOR(app.!==), HASHES)", + POSTGRESQL); + assertThat(operator.getOperator().getSchemaName()).isEqualTo("app"); + assertThat(operator.getOperator().getName()).isEqualTo("==="); + assertThat(operator.getOperator().getQualifiedName()).isEqualTo("app.==="); + assertThat(operator.getDefinition()).hasSize(5); + DefinitionArgument function = operator.getDefinitionElement("function").getArgument(); + assertThat(function.getKind()).isEqualTo(DefinitionArgument.Kind.TYPE); + assertThat(function.getDataType().getDataType()).isEqualTo("app.eq"); + DefinitionArgument negator = operator.getDefinitionElement("NEGATOR").getArgument(); + assertThat(negator.getKind()).isEqualTo(DefinitionArgument.Kind.OPERATOR); + assertThat(negator.isOperatorKeyword()).isTrue(); + assertThat(negator.getOperator()).isEqualTo("app.!=="); + assertThat(operator.getDefinitionElement("HASHES").getArgument()).isNull(); + assertThat(operator.getDefinitionElement("MERGES")).isNull(); + assertThat(operator.getASTNode()).isNotNull(); + + operator.getOperator().setName("<=>"); + operator.getDefinition().remove(4); + assertThat(operator).hasToString("CREATE OPERATOR app.<=> (FUNCTION = app.eq, " + + "LEFTARG = int, COMMUTATOR = ===, NEGATOR = OPERATOR(app.!==))"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR === ()", + "CREATE OPERATOR ===", + "CREATE OPERATOR === (FUNCTION = f,)", + "CREATE OPERATOR === (FUNCTION f)", + "CREATE OPERATOR === (FUNCTION = )", + "CREATE OPERATOR === (int, int) (FUNCTION = f)", + "CREATE OPERATOR === (FUNCTION = f) OWNER TO u", + "CREATE OR REPLACE OPERATOR === (FUNCTION = f, LEFTARG = int, RIGHTARG = int)", + "CREATE OPERATOR IF NOT EXISTS === (FUNCTION = f, LEFTARG = int, RIGHTARG = int)", + "CREATE OPERATOR === (FUNCTION = f, COMMUTATOR = +-)", + "CREATE OPERATOR === (FUNCTION = f, COMMUTATOR = =>)", + "CREATE OPERATOR eq BINDING (VARCHAR2, VARCHAR2) RETURN NUMBER USING eq_f"}) + void testRejectedCreateOperator(String sql) { + assertRejected(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR + (int, int) OWNER TO u", + "ALTER OPERATOR === (int, int) OWNER TO CURRENT_USER", + "ALTER OPERATOR === (int, int) OWNER TO SESSION_USER", + "ALTER OPERATOR === (int, int) OWNER TO CURRENT_ROLE", + "ALTER OPERATOR s.@@ (NONE, text) SET SCHEMA t", + "ALTER OPERATOR t.=== (int, int) SET SCHEMA public", + "ALTER OPERATOR === (int, int) SET (RESTRICT = eqsel, JOIN = NONE, HASHES)", + "ALTER OPERATOR === (int, int) SET (RESTRICT = NONE)", + "ALTER OPERATOR === (int, int) SET (COMMUTATOR = OPERATOR(public.===), NEGATOR = !==, " + + "MERGES = false, HASHES = true)", + "ALTER OPERATOR === (int, int) SET (x = 'a', y = 1, z = s.f2)", + "ALTER OPERATOR - (NONE, int) OWNER TO u", + "ALTER OPERATOR ! (bigint, NONE) OWNER TO u", + "ALTER OPERATOR d.public.=== (int, int) OWNER TO u", + "ALTER OPERATOR family.+ (varchar (3), int[]) OWNER TO u"}) + void testAlterOperator(String sql) throws Exception { + assertStatement(sql, AlterOperator.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + @Test + void testAlterOperatorFields() throws Exception { + AlterOperator owner = (AlterOperator) CCJSqlParserUtil.parse( + "ALTER OPERATOR app.@@ (NONE, text) OWNER TO u", POSTGRESQL); + assertThat(owner.getOperator().getOperator().getQualifiedName()).isEqualTo("app.@@"); + assertThat(owner.getOperator().getLeftType()).isNull(); + assertThat(owner.getOperator().getRightType()).hasToString("text"); + assertThat(owner.getOperator().getASTNode()).isNotNull(); + assertThat(owner.getAction()).isEqualTo(AlterOperator.Action.OWNER); + assertThat(owner.getNewName()).isEqualTo("u"); + assertThat(owner.getProperties()).isEmpty(); + + AlterOperator schema = (AlterOperator) CCJSqlParserUtil.parse( + "ALTER OPERATOR + (int, int) SET SCHEMA \"S\"", POSTGRESQL); + assertThat(schema.getAction()).isEqualTo(AlterOperator.Action.SET_SCHEMA); + assertThat(schema.getNewName()).isEqualTo("\"S\""); + + AlterOperator properties = (AlterOperator) CCJSqlParserUtil.parse( + "ALTER OPERATOR + (int, int) SET (RESTRICT = eqsel, JOIN = NONE, HASHES)", + POSTGRESQL); + assertThat(properties.getAction()).isEqualTo(AlterOperator.Action.SET_PROPERTIES); + assertThat(properties.getNewName()).isNull(); + assertThat(properties.getProperties()).hasSize(3); + assertThat(properties.getProperties().get(1).getArgument().getKind()) + .isEqualTo(DefinitionArgument.Kind.NONE); + properties.getProperties().remove(2); + properties.getOperator().setLeftType(null); + assertThat(properties).hasToString( + "ALTER OPERATOR + (NONE, int) SET (RESTRICT = eqsel, JOIN = NONE)"); + properties.setAction(null); + assertThatThrownBy(properties::toString).isInstanceOf(NullPointerException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR === (int, int) RENAME TO ====", + "ALTER OPERATOR === (int, int) RENAME TO x", + "ALTER OPERATOR === OWNER TO u", + "ALTER OPERATOR === (int) OWNER TO u", + "ALTER OPERATOR === (NONE, NONE) OWNER TO u", + "ALTER OPERATOR === (int, int, int) OWNER TO u", + "ALTER OPERATOR === (int, int) SET ()", + "ALTER OPERATOR === (int, int) SET (RESTRICT = )", + "ALTER OPERATOR === (int, int) SET RESTRICT = eqsel", + "ALTER OPERATOR === (int, int) OWNER u", + "ALTER OPERATOR === (int, int) SET SCHEMA", + "ALTER OPERATOR === (int, int)", + "ALTER OPERATOR IF EXISTS === (int, int) OWNER TO u", + "ALTER OPERATOR === (int, int), !== (int, int) OWNER TO u", + "ALTER OPERATOR myop (int, int) OWNER TO u", + "ALTER OPERATOR === (int, int) OWNER TO u CASCADE", + "ALTER OPERATOR eq COMPILE"}) + void testRejectedAlterOperator(String sql) { + assertRejected(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR CLASS oc DEFAULT FOR TYPE int USING btree AS OPERATOR 1 <, " + + "FUNCTION 1 f(int, int)", + "CREATE OPERATOR CLASS oc FOR TYPE int USING gist FAMILY fam AS OPERATOR 1 < (int, int) " + + "FOR ORDER BY float_ops, FUNCTION 1 (int, int) f(int, int), STORAGE int", + "CREATE OPERATOR CLASS s.oc2 FOR TYPE int4 USING btree FAMILY public.fam AS " + + "OPERATOR 1 < FOR SEARCH, OPERATOR 2 <=, OPERATOR 3 =, OPERATOR 4 >=, " + + "OPERATOR 5 >, FUNCTION 1 cmp(int, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE varchar (3) USING hash AS STORAGE int[]", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1 cmp", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1 s.cmp", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1 (int) cmp(int, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1 cmp()", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS " + + "FUNCTION 2 (int, text) s.f(IN a int, VARIADIC b text[])", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS " + + "OPERATOR 1 pg_catalog.< (int, int) FOR ORDER BY pg_catalog.float_ops", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 pg_catalog.<", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 - (NONE, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 ! (int, NONE)", + "CREATE OPERATOR CLASS oc3 FOR TYPE timestamp with time zone USING btree AS " + + "OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE double precision USING btree AS " + + "STORAGE double precision", + "CREATE OPERATOR CLASS \"Oc\" FOR TYPE s.\"T\" USING \"btree\" AS OPERATOR 1 <", + "CREATE OPERATOR CLASS class FOR TYPE int USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS family.oc FOR TYPE int USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 0 <"}) + void testCreateOperatorClass(String sql) throws Exception { + assertStatement(sql, CreateOperatorClass.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + @Test + void testCreateOperatorClassFields() throws Exception { + CreateOperatorClass operatorClass = (CreateOperatorClass) CCJSqlParserUtil.parse( + "CREATE OPERATOR CLASS app.oc DEFAULT FOR TYPE varchar(3) USING gist " + + "FAMILY app.fam AS OPERATOR 1 app.<< (NONE, int) FOR ORDER BY s.float_ops, " + + "OPERATOR 2 = FOR SEARCH, OPERATOR 3 <, " + + "FUNCTION 4 (int, text) app.f(int, text), FUNCTION 5 g, STORAGE int[]", + POSTGRESQL); + assertThat(operatorClass.getName()).isEqualTo("app.oc"); + assertThat(operatorClass.isDefaultClass()).isTrue(); + assertThat(operatorClass.getDataType()).hasToString("varchar (3)"); + assertThat(operatorClass.getDataType().getASTNode()).isNotNull(); + assertThat(operatorClass.getAccessMethod()).isEqualTo("gist"); + assertThat(operatorClass.getFamily()).isEqualTo("app.fam"); + assertThat(operatorClass.getItems()).hasSize(6); + + OperatorClassItem ordering = operatorClass.getItems().get(0); + assertThat(ordering.getKind()).isEqualTo(OperatorClassItem.Kind.OPERATOR); + assertThat(ordering.getNumber()).isEqualTo(1); + assertThat(ordering.getOperator().getSchemaName()).isEqualTo("app"); + assertThat(ordering.getOperator().getName()).isEqualTo("<<"); + assertThat(ordering.getOperandTypes()).hasSize(2); + assertThat(ordering.getOperandTypes().get(0)).isNull(); + assertThat(ordering.getOperandTypes().get(1)).hasToString("int"); + assertThat(ordering.getPurpose()).isEqualTo(OperatorClassItem.Purpose.ORDER_BY); + assertThat(ordering.getSortFamily()).isEqualTo("s.float_ops"); + assertThat(ordering.getFunction()).isNull(); + + OperatorClassItem search = operatorClass.getItems().get(1); + assertThat(search.getOperandTypes()).isNull(); + assertThat(search.getPurpose()).isEqualTo(OperatorClassItem.Purpose.SEARCH); + assertThat(search.getSortFamily()).isNull(); + assertThat(operatorClass.getItems().get(2).getPurpose()).isNull(); + + OperatorClassItem function = operatorClass.getItems().get(3); + assertThat(function.getKind()).isEqualTo(OperatorClassItem.Kind.FUNCTION); + assertThat(function.getNumber()).isEqualTo(4); + assertThat(function.getOperandTypes()).hasSize(2); + assertThat(function.getFunction().getName()).isEqualTo("app.f"); + assertThat(function.getFunction().getArguments()).hasSize(2); + assertThat(function.getOperator()).isNull(); + assertThat(operatorClass.getItems().get(4).getFunction().getArguments()).isNull(); + assertThat(operatorClass.getItems().get(4).getOperandTypes()).isNull(); + + OperatorClassItem storage = operatorClass.getItems().get(5); + assertThat(storage.getKind()).isEqualTo(OperatorClassItem.Kind.STORAGE); + assertThat(storage.getNumber()).isNull(); + assertThat(storage.getStorageType()).hasToString("int[]"); + + operatorClass.setDefaultClass(false); + operatorClass.setFamily(null); + operatorClass.getItems().subList(1, 6).clear(); + ordering.setPurpose(null); + OperatorClassItem added = new OperatorClassItem(OperatorClassItem.Kind.STORAGE); + added.setStorageType(storage.getStorageType()); + operatorClass.getItems().add(added); + assertThat(operatorClass).hasToString("CREATE OPERATOR CLASS app.oc FOR TYPE varchar (3) " + + "USING gist AS OPERATOR 1 app.<< (NONE, int), STORAGE int[]"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS", + "CREATE OPERATOR CLASS oc3 FOR TYPE int AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR int USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int DEFAULT USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING s.btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1.5 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR -1 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 99999999999 <", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < (int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < (NONE, NONE)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < FOR ORDER float_ops", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < FOR", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < RECHECK", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 OPERATOR(<)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 +-", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION cmp(int, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS FUNCTION 1 () cmp(int, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS " + + "FUNCTION 1 (NONE, int) cmp(int, int)", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS " + + "FUNCTION 1 cmp(int, int) FOR SEARCH", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS STORAGE", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS STORAGE int,", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 < OPERATOR 2 <=", + "CREATE OPERATOR CLASS oc3 FOR TYPE int USING btree FAMILY AS OPERATOR 1 <", + "CREATE OR REPLACE OPERATOR CLASS oc3 FOR TYPE int USING btree AS OPERATOR 1 <", + "CREATE OPERATOR CLASS IF NOT EXISTS oc3 FOR TYPE int USING btree AS OPERATOR 1 <"}) + void testRejectedCreateOperatorClass(String sql) { + assertRejected(sql); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR CLASS oc USING btree RENAME TO oc2", + "ALTER OPERATOR CLASS s.oc USING btree OWNER TO u", + "ALTER OPERATOR CLASS oc USING btree OWNER TO CURRENT_USER", + "ALTER OPERATOR CLASS oc USING btree SET SCHEMA s", + "ALTER OPERATOR CLASS \"Oc\" USING \"btree\" RENAME TO \"Oc2\"", + "ALTER OPERATOR CLASS class USING btree RENAME TO family"}) + void testAlterOperatorClass(String sql) throws Exception { + assertStatement(sql, AlterOperatorClass.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + @Test + void testAlterOperatorClassFields() throws Exception { + AlterOperatorClass rename = (AlterOperatorClass) CCJSqlParserUtil.parse( + "ALTER OPERATOR CLASS app.oc USING btree RENAME TO oc2", POSTGRESQL); + assertThat(rename.getName()).isEqualTo("app.oc"); + assertThat(rename.getAccessMethod()).isEqualTo("btree"); + assertThat(rename.getAction()).isEqualTo(AlterOperatorClass.Action.RENAME); + assertThat(rename.getNewName()).isEqualTo("oc2"); + rename.setAction(AlterOperatorClass.Action.OWNER); + rename.setNewName("u"); + assertThat(rename).hasToString("ALTER OPERATOR CLASS app.oc USING btree OWNER TO u"); + rename.setAction(AlterOperatorClass.Action.SET_SCHEMA); + rename.setName("oc"); + rename.setAccessMethod("hash"); + assertThat(rename).hasToString("ALTER OPERATOR CLASS oc USING hash SET SCHEMA u"); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR CLASS oc RENAME TO oc2", + "ALTER OPERATOR CLASS oc USING btree", + "ALTER OPERATOR CLASS oc USING btree RENAME oc2", + "ALTER OPERATOR CLASS oc USING btree RENAME TO s.oc2", + "ALTER OPERATOR CLASS oc USING btree SET (x = 1)", + "ALTER OPERATOR CLASS oc USING btree ADD OPERATOR 1 <", + "ALTER OPERATOR CLASS IF EXISTS oc USING btree RENAME TO oc2", + "ALTER OPERATOR CLASS oc USING btree SET SCHEMA s.t", + "ALTER OPERATOR CLASS oc USING btree RENAME TO oc2 CASCADE"}) + void testRejectedAlterOperatorClass(String sql) { + assertRejected(sql); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR FAMILY f USING btree", + "CREATE OPERATOR FAMILY s.f USING hash", + "CREATE OPERATOR FAMILY \"F 1\" USING \"btree\"", + "CREATE OPERATOR FAMILY family USING btree"}) + void testCreateOperatorFamily(String sql) throws Exception { + assertStatement(sql, CreateOperatorFamily.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OPERATOR FAMILY f", + "CREATE OPERATOR FAMILY f USING", + "CREATE OPERATOR FAMILY f USING s.btree", + "CREATE OPERATOR FAMILY f USING btree AS OPERATOR 1 <", + "CREATE OPERATOR FAMILY IF NOT EXISTS f USING btree", + "CREATE OR REPLACE OPERATOR FAMILY f USING btree", + "CREATE OPERATOR FAMILY f USING btree, hash", + "CREATE OPERATOR FAMILY f FOR TYPE int USING btree"}) + void testRejectedCreateOperatorFamily(String sql) { + assertRejected(sql); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 < (int, int), " + + "FUNCTION 1 (int, int) g(int, int)", + "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 < (int4, int2) FOR SEARCH, " + + "OPERATOR 2 <= (int4, int2) FOR ORDER BY s.fam2", + "ALTER OPERATOR FAMILY f USING btree ADD FUNCTION 1 cmp(int, int)", + "ALTER OPERATOR FAMILY f USING btree ADD FUNCTION 1 (int4) cmp(int, int), " + + "FUNCTION 2 (int4, int2) s.cmp2(internal)", + "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 <", + "ALTER OPERATOR FAMILY f USING btree ADD STORAGE int", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 (int, int), FUNCTION 1 (int, int)", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 (int4)", + "ALTER OPERATOR FAMILY f USING btree DROP FUNCTION 2 (int4, int2, int8)", + "ALTER OPERATOR FAMILY s.f USING hash DROP FUNCTION 1 (varchar (3), int[])", + "ALTER OPERATOR FAMILY f USING btree OWNER TO u", + "ALTER OPERATOR FAMILY f USING btree OWNER TO SESSION_USER", + "ALTER OPERATOR FAMILY f USING btree RENAME TO f2", + "ALTER OPERATOR FAMILY f USING btree SET SCHEMA s", + "ALTER OPERATOR FAMILY family USING btree RENAME TO class"}) + void testAlterOperatorFamily(String sql) throws Exception { + assertStatement(sql, AlterOperatorFamily.class); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + } + + @Test + void testOperatorFamilyFields() throws Exception { + CreateOperatorFamily family = (CreateOperatorFamily) CCJSqlParserUtil.parse( + "CREATE OPERATOR FAMILY app.fam USING btree", POSTGRESQL); + assertThat(family.getName()).isEqualTo("app.fam"); + assertThat(family.getAccessMethod()).isEqualTo("btree"); + family.setName("fam"); + family.setAccessMethod("hash"); + assertThat(family).hasToString("CREATE OPERATOR FAMILY fam USING hash"); + + AlterOperatorFamily add = (AlterOperatorFamily) CCJSqlParserUtil.parse( + "ALTER OPERATOR FAMILY app.fam USING btree ADD OPERATOR 1 < (int4, int2), " + + "FUNCTION 1 (int4, int2) btint42cmp(int4, int2)", + POSTGRESQL); + assertThat(add.getName()).isEqualTo("app.fam"); + assertThat(add.getAccessMethod()).isEqualTo("btree"); + assertThat(add.getAction()).isEqualTo(AlterOperatorFamily.Action.ADD); + assertThat(add.getNewName()).isNull(); + assertThat(add.getItems()).hasSize(2); + assertThat(add.getItems().get(0).getOperator().getName()).isEqualTo("<"); + assertThat(add.getItems().get(1).getFunction().getName()).isEqualTo("btint42cmp"); + + AlterOperatorFamily drop = (AlterOperatorFamily) CCJSqlParserUtil.parse( + "ALTER OPERATOR FAMILY fam USING btree DROP OPERATOR 1 (int4, int2), " + + "FUNCTION 2 (int4)", + POSTGRESQL); + assertThat(drop.getAction()).isEqualTo(AlterOperatorFamily.Action.DROP); + OperatorClassItem operator = drop.getItems().get(0); + assertThat(operator.getKind()).isEqualTo(OperatorClassItem.Kind.OPERATOR); + assertThat(operator.getNumber()).isEqualTo(1); + assertThat(operator.getOperator()).isNull(); + assertThat(operator.getOperandTypes()).hasSize(2); + OperatorClassItem function = drop.getItems().get(1); + assertThat(function.getKind()).isEqualTo(OperatorClassItem.Kind.FUNCTION); + assertThat(function.getFunction()).isNull(); + assertThat(function.getOperandTypes()).hasSize(1); + drop.getItems().remove(0); + function.setNumber(3); + assertThat(drop) + .hasToString("ALTER OPERATOR FAMILY fam USING btree DROP FUNCTION 3 (int4)"); + + AlterOperatorFamily owner = (AlterOperatorFamily) CCJSqlParserUtil.parse( + "ALTER OPERATOR FAMILY fam USING btree OWNER TO u", POSTGRESQL); + assertThat(owner.getAction()).isEqualTo(AlterOperatorFamily.Action.OWNER); + assertThat(owner.getNewName()).isEqualTo("u"); + assertThat(owner.getItems()).isEmpty(); + owner.setAction(AlterOperatorFamily.Action.RENAME); + assertThat(owner).hasToString("ALTER OPERATOR FAMILY fam USING btree RENAME TO u"); + owner.setAction(AlterOperatorFamily.Action.SET_SCHEMA); + assertThat(owner).hasToString("ALTER OPERATOR FAMILY fam USING btree SET SCHEMA u"); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER OPERATOR FAMILY f USING btree ADD", + "ALTER OPERATOR FAMILY f USING btree DROP", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 < (int, int)", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 ()", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 (NONE, int)", + "ALTER OPERATOR FAMILY f USING btree DROP FUNCTION 1 cmp(int, int)", + "ALTER OPERATOR FAMILY f USING btree DROP FUNCTION (int, int)", + "ALTER OPERATOR FAMILY f USING btree DROP STORAGE int", + "ALTER OPERATOR FAMILY f USING btree DROP OPERATOR 1 (int, int) CASCADE", + "ALTER OPERATOR FAMILY f ADD OPERATOR 1 < (int, int)", + "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 < (int, int) " + + "DROP OPERATOR 2 (int, int)", + "ALTER OPERATOR FAMILY f USING btree ADD OPERATOR 1 < (int, int), " + + "DROP OPERATOR 2 (int, int)", + "ALTER OPERATOR FAMILY f USING btree SET (x = 1)", + "ALTER OPERATOR FAMILY f USING btree RENAME TO", + "ALTER OPERATOR FAMILY IF EXISTS f USING btree RENAME TO f2", + "ALTER OPERATOR FAMILY f USING btree"}) + void testRejectedAlterOperatorFamily(String sql) { + assertRejected(sql); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } + + @Test + void testOtherDialectsKeepOracleOperators() throws Exception { + for (String sql : new String[] { + "CREATE OPERATOR eq_op BINDING (VARCHAR2, VARCHAR2) RETURN NUMBER USING eq_f", + "CREATE OR REPLACE OPERATOR s.eq_op BINDING (VARCHAR2) RETURN NUMBER USING eq_f", + "CREATE OPERATOR class BINDING (VARCHAR2) RETURN NUMBER USING eq_f", + "ALTER OPERATOR eq_op COMPILE", + "ALTER OPERATOR class COMPILE", + "ALTER OPERATOR eq_op ADD BINDING (NUMBER) RETURN NUMBER USING f", + "ALTER OPERATOR family ADD BINDING (NUMBER) RETURN NUMBER USING f"}) { + assertThat(CCJSqlParserUtil.parse(sql)).isInstanceOf(UnsupportedStatement.class); + assertThat(CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.ORACLE))) + .isInstanceOf(UnsupportedStatement.class); + } + } + + @Test + void testVisitorsAndValidation() throws Exception { + String[][] statements = { + {"CREATE OPERATOR === (FUNCTION = f)", "createOperator"}, + {"ALTER OPERATOR === (int, int) OWNER TO u", "alterOperator"}, + {"CREATE OPERATOR CLASS oc FOR TYPE int USING btree AS OPERATOR 1 <", + "createOperatorClass"}, + {"ALTER OPERATOR CLASS oc USING btree RENAME TO oc2", "alterOperatorClass"}, + {"CREATE OPERATOR FAMILY f USING btree", "createOperatorFamily"}, + {"ALTER OPERATOR FAMILY f USING btree RENAME TO f2", "alterOperatorFamily"}}; + for (String[] entry : statements) { + String sql = entry[0]; + Feature feature = Feature.valueOf(entry[1]); + validateNoErrors(sql, 1, + feature.name().startsWith("create") ? FeaturesAllowed.CREATE + : FeaturesAllowed.ALTER); + validateNotAllowed(sql, 1, 1, FeaturesAllowed.SELECT, feature); + validate(sql, 1, 1, MySqlVersion.V8_0); + } + + StatementVisitorAdapter visitor = new StatementVisitorAdapter() { + @Override + public String visit(CreateOperator statement, S context) { + return context + ":" + statement.getOperator().getName(); + } + + @Override + public String visit(AlterOperator statement, S context) { + return context + ":" + statement.getAction(); + } + + @Override + public String visit(CreateOperatorClass statement, S context) { + return context + ":" + statement.getName(); + } + + @Override + public String visit(AlterOperatorClass statement, S context) { + return context + ":" + statement.getNewName(); + } + + @Override + public String visit(CreateOperatorFamily statement, S context) { + return context + ":" + statement.getAccessMethod(); + } + + @Override + public String visit(AlterOperatorFamily statement, S context) { + return context + ":" + statement.getAction(); + } + }; + String[] expected = {"c:===", "c:OWNER", "c:oc", "c:oc2", "c:btree", "c:RENAME"}; + for (int i = 0; i < statements.length; i++) { + Statement statement = CCJSqlParserUtil.parse(statements[i][0], POSTGRESQL); + assertThat(statement.accept(visitor, "c")).isEqualTo(expected[i]); + assertThat(statement.accept(new StatementVisitorAdapter(), "c")).isNull(); + } + } +}