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(); + } + } +}