diff --git a/src/main/java/net/sf/jsqlparser/statement/CastSignature.java b/src/main/java/net/sf/jsqlparser/statement/CastSignature.java
new file mode 100644
index 000000000..2aeb0bb28
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/CastSignature.java
@@ -0,0 +1,69 @@
+/*-
+ * #%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;
+
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+
+/**
+ * A PostgreSQL cast, named by the types it converts between: {@code (source_type AS target_type)},
+ * as in {@code DROP CAST (text AS integer)}.
+ */
+public class CastSignature extends ASTNodeAccessImpl {
+
+ private ColDataType sourceType;
+ private ColDataType targetType;
+
+ public CastSignature() {
+ // the types are set by the parser or the setters
+ }
+
+ public CastSignature(ColDataType sourceType, ColDataType targetType) {
+ this.sourceType = sourceType;
+ this.targetType = targetType;
+ }
+
+ public ColDataType getSourceType() {
+ return sourceType;
+ }
+
+ public void setSourceType(ColDataType sourceType) {
+ this.sourceType = sourceType;
+ }
+
+ public CastSignature withSourceType(ColDataType sourceType) {
+ setSourceType(sourceType);
+ return this;
+ }
+
+ public ColDataType getTargetType() {
+ return targetType;
+ }
+
+ public void setTargetType(ColDataType targetType) {
+ this.targetType = targetType;
+ }
+
+ public CastSignature withTargetType(ColDataType targetType) {
+ setTargetType(targetType);
+ return this;
+ }
+
+ @Override
+ public StringBuilder appendTo(StringBuilder builder) {
+ return builder.append('(').append(sourceType).append(" AS ").append(targetType)
+ .append(')');
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/OperatorSignature.java b/src/main/java/net/sf/jsqlparser/statement/OperatorSignature.java
new file mode 100644
index 000000000..75957e3cc
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/OperatorSignature.java
@@ -0,0 +1,95 @@
+/*-
+ * #%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;
+
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+import net.sf.jsqlparser.statement.create.table.ExclusionOperator;
+
+/**
+ * A PostgreSQL operator, named by its possibly schema-qualified symbol and its operand types:
+ * {@code [schema.]op ({left_type | NONE}, {right_type | NONE})}, as in
+ * {@code DROP OPERATOR ~ (NONE, bigint)}. A null operand type is the NONE marker of a missing
+ * operand, and is rendered as {@code NONE}.
+ */
+public class OperatorSignature extends ASTNodeAccessImpl {
+
+ private ExclusionOperator operator;
+ private ColDataType leftType;
+ private ColDataType rightType;
+
+ public OperatorSignature() {
+ // the operator and its operand types are set by the parser or the setters
+ }
+
+ public OperatorSignature(ExclusionOperator operator, ColDataType leftType,
+ ColDataType rightType) {
+ this.operator = operator;
+ this.leftType = leftType;
+ this.rightType = rightType;
+ }
+
+ /** The operator symbol and its schema, read by the reference COMMENT ON OPERATOR uses. */
+ public ExclusionOperator getOperator() {
+ return operator;
+ }
+
+ public void setOperator(ExclusionOperator operator) {
+ this.operator = operator;
+ }
+
+ public OperatorSignature withOperator(ExclusionOperator operator) {
+ setOperator(operator);
+ return this;
+ }
+
+ /** The left operand type, or null for NONE, the missing left operand of a prefix operator. */
+ public ColDataType getLeftType() {
+ return leftType;
+ }
+
+ public void setLeftType(ColDataType leftType) {
+ this.leftType = leftType;
+ }
+
+ public OperatorSignature withLeftType(ColDataType leftType) {
+ setLeftType(leftType);
+ return this;
+ }
+
+ /** The right operand type, or null for NONE. */
+ public ColDataType getRightType() {
+ return rightType;
+ }
+
+ public void setRightType(ColDataType rightType) {
+ this.rightType = rightType;
+ }
+
+ public OperatorSignature withRightType(ColDataType rightType) {
+ setRightType(rightType);
+ return this;
+ }
+
+ @Override
+ public StringBuilder appendTo(StringBuilder builder) {
+ return builder.append(operator).append(" (").append(operand(leftType)).append(", ")
+ .append(operand(rightType)).append(')');
+ }
+
+ private static Object operand(ColDataType type) {
+ return type == null ? "NONE" : type;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/TransformSignature.java b/src/main/java/net/sf/jsqlparser/statement/TransformSignature.java
new file mode 100644
index 000000000..2ceca11a5
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/TransformSignature.java
@@ -0,0 +1,69 @@
+/*-
+ * #%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;
+
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+
+/**
+ * A PostgreSQL transform, named by the data type and the procedural language it connects:
+ * {@code FOR type_name LANGUAGE lang_name}, as in
+ * {@code DROP TRANSFORM FOR hstore LANGUAGE plpython3u}.
+ */
+public class TransformSignature extends ASTNodeAccessImpl {
+
+ private ColDataType dataType;
+ private String language;
+
+ public TransformSignature() {
+ // the type and language are set by the parser or the setters
+ }
+
+ public TransformSignature(ColDataType dataType, String language) {
+ this.dataType = dataType;
+ this.language = language;
+ }
+
+ public ColDataType getDataType() {
+ return dataType;
+ }
+
+ public void setDataType(ColDataType dataType) {
+ this.dataType = dataType;
+ }
+
+ public TransformSignature withDataType(ColDataType dataType) {
+ setDataType(dataType);
+ return this;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public TransformSignature withLanguage(String language) {
+ setLanguage(language);
+ return this;
+ }
+
+ @Override
+ public StringBuilder appendTo(StringBuilder builder) {
+ return builder.append("FOR ").append(dataType).append(" LANGUAGE ").append(language);
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
index 182b1e124..f2393760f 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
@@ -13,7 +13,7 @@
/**
* An exclusion operator, optionally qualified and enclosed in PostgreSQL OPERATOR(...). COMMENT ON
- * OPERATOR uses the same reference without the OPERATOR keyword.
+ * OPERATOR and DROP OPERATOR use the same reference without the OPERATOR keyword.
*/
public class ExclusionOperator implements Serializable {
private String schemaName;
diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
index 2c29b8dc8..86ad0c03f 100644
--- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
+++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
@@ -22,9 +22,12 @@
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.CascadeBehavior;
+import net.sf.jsqlparser.statement.CastSignature;
+import net.sf.jsqlparser.statement.OperatorSignature;
import net.sf.jsqlparser.statement.RoutineReference;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitor;
+import net.sf.jsqlparser.statement.TransformSignature;
import net.sf.jsqlparser.statement.create.index.AlgorithmOrLockOption;
import net.sf.jsqlparser.statement.create.user.UserAccount;
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -32,7 +35,7 @@
public class Drop extends ASTNodeAccessImpl implements Statement {
public enum ObjectType {
- DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, OTHER;
+ DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, CAST, OPERATOR, TRANSFORM, OTHER;
/** Targets are {@link RoutineReference}s, each with an optional argument signature. */
public boolean hasRoutineTargets() {
@@ -60,6 +63,9 @@ public boolean hasAccessMethod() {
private final List
names = new ArrayList<>();
private final List routines = new ArrayList<>();
private final List accounts = new ArrayList<>();
+ private final List operators = new ArrayList<>();
+ private CastSignature cast;
+ private TransformSignature transform;
private List parameters;
private Table table;
private int tablePosition;
@@ -121,8 +127,9 @@ public void setName(Table name) {
}
/**
- * Returns the dropped object names. Routine and account targets return a read-only snapshot of
- * their names; edit {@link #getRoutines()} or {@link #getAccounts()} to change them.
+ * Returns the dropped object names. Routine, account and operator targets return a read-only
+ * snapshot of their names; edit {@link #getRoutines()}, {@link #getAccounts()} or
+ * {@link #getOperators()} to change them. A cast or transform has no name.
*/
public List getNames() {
if (!routines.isEmpty()) {
@@ -136,6 +143,13 @@ public List getNames() {
.map(account -> new Table(account.toString(), false))
.collect(Collectors.toList()));
}
+ if (!operators.isEmpty()) {
+ return Collections.unmodifiableList(operators.stream()
+ .map(operator -> operator.getOperator() == null ? new Table()
+ : new Table(operator.getOperator().getSchemaName(),
+ operator.getOperator().getName()))
+ .collect(Collectors.toList()));
+ }
return Collections.unmodifiableList(names);
}
@@ -204,27 +218,97 @@ public Drop addAccounts(UserAccount... accounts) {
return this;
}
+ /**
+ * PostgreSQL DROP OPERATOR targets, each an operator with its operand types. Oracle's
+ * {@code DROP OPERATOR name} keeps a plain name instead.
+ */
+ public List getOperators() {
+ return Collections.unmodifiableList(operators);
+ }
+
+ /** Replaces the targets with operators, discarding all other targets. */
+ public void setOperators(Collection extends OperatorSignature> operators) {
+ clearTargets();
+ if (operators != null) {
+ this.operators.addAll(operators);
+ }
+ }
+
+ public Drop withOperators(Collection extends OperatorSignature> operators) {
+ setOperators(operators);
+ return this;
+ }
+
+ /** Appends operator targets. Other targets are discarded first. */
+ public Drop addOperators(OperatorSignature... operators) {
+ if (operators.length > 0 && this.operators.isEmpty()) {
+ clearTargets();
+ }
+ Collections.addAll(this.operators, operators);
+ return this;
+ }
+
+ /** The target of PostgreSQL DROP CAST, or null for other object types. */
+ public CastSignature getCast() {
+ return cast;
+ }
+
+ /** Replaces the targets with a cast, discarding all other targets. */
+ public void setCast(CastSignature cast) {
+ clearTargets();
+ this.cast = cast;
+ }
+
+ public Drop withCast(CastSignature cast) {
+ setCast(cast);
+ return this;
+ }
+
+ /** The target of PostgreSQL DROP TRANSFORM, or null for other object types. */
+ public TransformSignature getTransform() {
+ return transform;
+ }
+
+ /** Replaces the targets with a transform, discarding all other targets. */
+ public void setTransform(TransformSignature transform) {
+ clearTargets();
+ this.transform = transform;
+ }
+
+ public Drop withTransform(TransformSignature transform) {
+ setTransform(transform);
+ return this;
+ }
+
private void clearTargets() {
names.clear();
routines.clear();
accounts.clear();
+ operators.clear();
+ cast = null;
+ transform = null;
typeToParameters = new HashMap<>();
}
+ private boolean hasTypedTargets() {
+ return !routines.isEmpty() || !accounts.isEmpty() || !operators.isEmpty() || cast != null
+ || transform != null;
+ }
+
/**
- * Converts routine or account targets to the legacy representation before a legacy edit: the
- * names, and the last routine's argument declarations as the single legacy signature.
+ * Converts typed targets to the legacy representation before a legacy edit: the names, and the
+ * last routine's argument declarations as the single legacy signature. A cast or transform
+ * leaves no name.
*/
private void useNameTargets() {
- if (routines.isEmpty() && accounts.isEmpty()) {
+ if (!hasTypedTargets()) {
return;
}
- List legacyNames = getNames();
+ List legacyNames = new ArrayList<>(getNames());
Map> legacyParameters = new HashMap<>();
getTypeToParameters().forEach((key, value) -> legacyParameters.put(key,
value == null ? null : new ArrayList<>(value)));
- routines.clear();
- accounts.clear();
+ clearTargets();
names.addAll(legacyNames);
typeToParameters = legacyParameters;
}
@@ -572,6 +656,17 @@ private void appendTargets(StringBuilder builder, Consumer tablePrinter)
} else if (!accounts.isEmpty()) {
builder.append(accounts.stream().map(Object::toString)
.collect(Collectors.joining(", ")));
+ } else if (!operators.isEmpty()) {
+ for (int i = 0; i < operators.size(); i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ operators.get(i).appendTo(builder);
+ }
+ } else if (cast != null) {
+ cast.appendTo(builder);
+ } else if (transform != null) {
+ transform.appendTo(builder);
} else {
appendNames(builder, tablePrinter);
if ("FUNCTION".equals(type)) {
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 76fc4f704..c61c7418c 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -3622,6 +3622,27 @@ public class CCJSqlParser extends AbstractJSqlParser {
&& (first.kind == K_WITH && "(".equals(second.image) && third.kind == K_FORCE
|| "(".equals(first.image) && second.kind == K_FORCE);
}
+
+ /**
+ * A PostgreSQL operator, possibly after a schema and a dot, is spelled with the characters
+ * of PostgreSQL's operator tokens, unlike the name of an Oracle operator.
+ */
+ static boolean isOperatorAhead(Token first, Token second, Token third) {
+ return isOperatorSymbol(first) || ".".equals(second.image) && isOperatorSymbol(third);
+ }
+
+ // an identifier such as #tmp may start with an operator character too
+ private static boolean isOperatorSymbol(Token token) {
+ return token.kind != S_IDENTIFIER && token.kind != S_QUOTED_IDENTIFIER
+ && token.image != null && !token.image.isEmpty()
+ && "+-*/<>=~!@#%^&|?".indexOf(token.image.charAt(0)) >= 0;
+ }
+
+ /** The targets are a cast, a transform or operators, named by their types. */
+ static boolean hasSignatureTargets(Drop drop) {
+ return drop.getCast() != null || drop.getTransform() != null
+ || !drop.getOperators().isEmpty();
+ }
}
/** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */
@@ -20741,6 +20762,9 @@ void DropTargets(Drop drop):
boolean aggregate = drop.getObjectType() == Drop.ObjectType.AGGREGATE;
RoutineReference routine;
UserAccount account;
+ CastSignature cast;
+ TransformSignature transform;
+ OperatorSignature operator;
Table name;
}
{
@@ -20758,11 +20782,79 @@ void DropTargets(Drop drop):
}
}
|
+ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.CAST })
+ cast=CastSignature() { drop.setCast(cast); }
+ |
+ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.TRANSFORM && getToken(1).kind == K_FOR })
+ transform=TransformSignature() { drop.setTransform(transform); }
+ |
+ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.OPERATOR
+ && DropLookahead.isOperatorAhead(getToken(1), getToken(2), getToken(3)) })
+ operator=OperatorSignature() { drop.addOperators(operator); }
+ ( "," operator=OperatorSignature() { drop.addOperators(operator); } )*
+ |
name=DropObjectName() { drop.addNames(name); }
( "," name=DropObjectName() { drop.addNames(name); } )*
+ {
+ // PostgreSQL names an operator by its symbol and operand types and a transform by its
+ // type and language; Oracle's DROP OPERATOR name keeps the name
+ requireDdlSyntax(!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
+ || drop.getObjectType() != Drop.ObjectType.OPERATOR
+ && drop.getObjectType() != Drop.ObjectType.TRANSFORM,
+ "DROP " + drop.getType() + " requires a signature in PostgreSQL");
+ }
)
}
+/** PostgreSQL ( source_type AS target_type ), naming a cast. */
+CastSignature CastSignature():
+{
+ Token first = getToken(1);
+ CastSignature result = new CastSignature();
+ ColDataType type;
+}
+{
+ "(" type=ColDataType() { result.setSourceType(type); }
+ type=ColDataType() { result.setTargetType(type); } ")"
+ { return linked(result, first); }
+}
+
+/** PostgreSQL FOR type_name LANGUAGE lang_name, naming a transform. */
+TransformSignature TransformSignature():
+{
+ Token first = getToken(1);
+ TransformSignature result = new TransformSignature();
+ ColDataType type;
+ String language;
+}
+{
+ type=ColDataType() { result.setDataType(type); }
+ ContextualKeyword("LANGUAGE") language=RelObjectName() { result.setLanguage(language); }
+ { return linked(result, first); }
+}
+
+/**
+ * PostgreSQL [ schema. ] operator ( { left_type | NONE }, { right_type | NONE } ), naming an
+ * operator. NONE marks a missing operand, but one operand type is required.
+ */
+OperatorSignature OperatorSignature():
+{
+ Token first = getToken(1);
+ OperatorSignature result = new OperatorSignature();
+ ExclusionOperator operator;
+ ColDataType type;
+}
+{
+ operator=PostgreSqlOperatorReference() { result.setOperator(operator); }
+ "(" type=ExtensionOperatorType() { result.setLeftType(type); }
+ "," type=ExtensionOperatorType() { result.setRightType(type); } ")"
+ {
+ requireTypeDdlSyntax(result.getLeftType() != null || result.getRightType() != null,
+ "An operator signature needs an operand type");
+ return linked(result, first);
+ }
+}
+
Drop Drop():
{
Drop drop = new Drop();
@@ -20849,6 +20941,9 @@ Drop Drop():
|
tk=
|
+ // PostgreSQL DROP CAST ( source_type AS target_type )
+ tk=
+ |
// PostgreSQL DROP EVENT TRIGGER and MySQL DROP EVENT (TRIGGER cannot name a MySQL event)
tk= [ LOOKAHEAD(1) { eventTrigger = true; } ]
|
@@ -20918,6 +21013,10 @@ Drop Drop():
)*
[ { behavior = CascadeBehavior.CASCADE; } | { behavior = CascadeBehavior.RESTRICT; } ]
{
+ // a cast, transform or operator signature is followed by CASCADE or RESTRICT only
+ requireDdlSyntax(!DropLookahead.hasSignatureTargets(drop)
+ || dropArgs.isEmpty() && indexTable == null && indexOptions.isEmpty(),
+ "Unexpected clause after DROP " + drop.getType());
if (dropArgs.size() > 0) {
drop.setParameters(dropArgs);
}
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index f56cb9ea9..ecf447ba8 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -274,10 +274,20 @@ typed properties:
* ``getAccessMethod()`` holds the index access method of PostgreSQL
``DROP OPERATOR CLASS|FAMILY name USING method``, which takes one name.
``isProcedural()`` records the ``PROCEDURAL LANGUAGE`` spelling.
+* PostgreSQL names casts, operators and transforms by types. ``getCast()`` is the
+ ``CastSignature`` of ``DROP CAST (source_type AS target_type)``.
+ ``getOperators()`` holds an ``OperatorSignature`` per target of
+ ``DROP OPERATOR [schema.]op ({left_type | NONE}, {right_type | NONE}) [, ...]``;
+ its operator is the ``ExclusionOperator`` reference that COMMENT ON OPERATOR
+ uses, and a null operand type stands for NONE. ``getTransform()`` is the
+ ``TransformSignature`` of ``DROP TRANSFORM FOR type_name LANGUAGE lang_name``.
+ Oracle's ``DROP OPERATOR name`` keeps a plain name, which the PostgreSQL
+ dialect rejects.
TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized
-object types, and so are PostgreSQL's ACCESS METHOD, LANGUAGE, OPERATOR CLASS,
-OPERATOR FAMILY and TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER and TEMPLATE.
+object types, and so are PostgreSQL's ACCESS METHOD, CAST, LANGUAGE, OPERATOR,
+OPERATOR CLASS, OPERATOR FAMILY, TRANSFORM and TEXT SEARCH CONFIGURATION,
+DICTIONARY, PARSER and TEMPLATE.
``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is
parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model.
@@ -292,8 +302,9 @@ parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model.
The legacy accessors remain and are derived from these properties:
-* ``getName()`` and ``getNames()`` return read-only snapshots of routine and
- account names. Edit ``getRoutines()`` or ``getAccounts()`` to change them.
+* ``getName()`` and ``getNames()`` return read-only snapshots of routine,
+ account and operator names. Edit ``getRoutines()``, ``getAccounts()`` or
+ ``getOperators()`` to change them. A cast or transform has no name.
* ``getTypeToParameters()`` and ``getParamsByType()`` return a read-only map with
the argument declarations of the last routine, where the legacy
single-signature form kept them. The map now also covers PROCEDURE, ROUTINE
diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
index 0ac7a4e84..25dd26d38 100644
--- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
+++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
@@ -146,7 +146,10 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException {
"CLUSTER (VERBOSE) t USING i",
"CLUSTER VERBOSE i ON t",
"ANALYSE (\"verbose\", PARALLEL -1) t",
- "CHECKPOINT"}) {
+ "CHECKPOINT",
+ "DROP CAST IF EXISTS (int[] AS public.t) CASCADE",
+ "DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint)",
+ "DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u"}) {
new Walk(sql, sql, problems).check(CCJSqlParserUtil.parseStatements(sql));
}
String block = "BEGIN x := 1; NULL; foo(1); END;";
diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropSignatureTargetTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropSignatureTargetTest.java
new file mode 100644
index 000000000..47008ab92
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropSignatureTargetTest.java
@@ -0,0 +1,315 @@
+/*-
+ * #%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.drop;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.CascadeBehavior;
+import net.sf.jsqlparser.statement.CastSignature;
+import net.sf.jsqlparser.statement.OperatorSignature;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.TransformSignature;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+import net.sf.jsqlparser.statement.create.table.ExclusionOperator;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** PostgreSQL DROP CAST, DROP OPERATOR and DROP TRANSFORM name their targets by types. */
+class DropSignatureTargetTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"DROP CAST (text AS integer)",
+ "DROP CAST IF EXISTS (bigint AS int4) CASCADE",
+ "DROP CAST (int[] AS public.mytype) RESTRICT",
+ "DROP CAST (double precision AS timestamp with time zone)",
+ "DROP OPERATOR === (integer, integer)",
+ "DROP OPERATOR IF EXISTS ~ (NONE, bigint) CASCADE",
+ "DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint)",
+ "DROP OPERATOR @@ (text, text)", "DROP OPERATOR <-> (point, point) RESTRICT",
+ "DROP OPERATOR ! (bigint, NONE)", "DROP OPERATOR \"my schema\".+ (int, int)",
+ "DROP OPERATOR - (NONE, integer), || (text, text), ? (jsonb, text)",
+ "DROP OPERATOR <<= (inet, inet), -|- (int4range, int4range), ?| (jsonb, text[])",
+ "DROP OPERATOR * (int, int), / (int, int), = (int, int), >= (int, int)",
+ "DROP OPERATOR @-@ (NONE, lseg), public.#- (jsonb, text[]), <#> (point, point)",
+ "DROP TRANSFORM FOR hstore LANGUAGE plpython3u",
+ "DROP TRANSFORM IF EXISTS FOR public.hstore LANGUAGE plpython3u RESTRICT",
+ "DROP TRANSFORM FOR integer[] LANGUAGE plperl CASCADE",
+ "DROP TRANSFORM FOR character varying LANGUAGE \"plpython3u\""})
+ void signatureTargetsRoundTrip(String sql) throws JSQLParserException {
+ for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) {
+ assertEquals(sql, drop.toString());
+ assertNotEquals(Drop.ObjectType.OTHER, drop.getObjectType());
+ // no tokens are left over: CASCADE or RESTRICT is the only legacy parameter
+ assertEquals(drop.getBehavior() == null ? null : List.of(drop.getBehavior().name()),
+ drop.getParameters());
+ assertTrue(new TablesNamesFinder().getTables(drop).isEmpty());
+ }
+ assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
+ assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1",
+ parser -> parser.withDialect(Dialect.POSTGRESQL)).size());
+ }
+
+ @Test
+ void castFields() throws JSQLParserException {
+ Drop drop = parse("DROP CAST IF EXISTS (bigint AS int4) CASCADE");
+ assertEquals(Drop.ObjectType.CAST, drop.getObjectType());
+ assertEquals("CAST", drop.getType());
+ assertTrue(drop.isIfExists());
+ assertEquals(CascadeBehavior.CASCADE, drop.getBehavior());
+ CastSignature cast = drop.getCast();
+ assertEquals("bigint", cast.getSourceType().getDataType());
+ assertEquals("int4", cast.getTargetType().getDataType());
+ assertTrue(drop.getNames().isEmpty());
+ assertNull(drop.getName());
+ assertTrue(drop.getOperators().isEmpty());
+ assertNull(drop.getTransform());
+
+ cast.setTargetType(new ColDataType("integer"));
+ assertEquals("DROP CAST IF EXISTS (bigint AS integer) CASCADE", drop.toString());
+ assertRendersAlike(drop);
+
+ assertEquals("DROP CAST (text AS integer)",
+ new Drop().withObjectType(Drop.ObjectType.CAST)
+ .withCast(new CastSignature(new ColDataType("text"),
+ new ColDataType("integer")))
+ .toString());
+ }
+
+ @Test
+ void operatorFields() throws JSQLParserException {
+ Drop drop =
+ parse("DROP OPERATOR IF EXISTS public.=== (integer, integer), ~ (NONE, bigint)");
+ assertEquals(Drop.ObjectType.OPERATOR, drop.getObjectType());
+ assertEquals(2, drop.getOperators().size());
+
+ OperatorSignature first = drop.getOperators().get(0);
+ assertEquals("public", first.getOperator().getSchemaName());
+ assertEquals("===", first.getOperator().getName());
+ assertFalse(first.getOperator().isUseOperatorKeyword());
+ assertEquals("integer", first.getLeftType().getDataType());
+ assertEquals("integer", first.getRightType().getDataType());
+
+ OperatorSignature prefix = drop.getOperators().get(1);
+ assertNull(prefix.getOperator().getSchemaName());
+ assertEquals("~", prefix.getOperator().getName());
+ assertNull(prefix.getLeftType(), "NONE is a null operand type");
+ assertEquals("bigint", prefix.getRightType().getDataType());
+
+ assertEquals(List.of("public.===", "~"), names(drop));
+ assertThrows(UnsupportedOperationException.class,
+ () -> drop.getNames().add(new Table("x")));
+ assertThrows(UnsupportedOperationException.class,
+ () -> drop.getOperators().add(new OperatorSignature()));
+ assertNull(drop.getCast());
+
+ prefix.setLeftType(new ColDataType("bigint"));
+ first.setRightType(null);
+ assertEquals("DROP OPERATOR IF EXISTS public.=== (integer, NONE), ~ (bigint, bigint)",
+ drop.toString());
+ assertRendersAlike(drop);
+
+ // ## is an operator with the PostgreSQL dialect; without one it is an identifier, as in
+ // SQL Server's ##global_temp
+ Drop hash = parse("DROP OPERATOR ## (lseg, lseg)");
+ assertEquals("##", hash.getOperators().get(0).getOperator().getName());
+ assertEquals("DROP OPERATOR ## (lseg, lseg)", hash.toString());
+
+ assertEquals("DROP OPERATOR @@ (NONE, text) CASCADE", new Drop()
+ .withObjectType(Drop.ObjectType.OPERATOR)
+ .addOperators(new OperatorSignature(new ExclusionOperator().withName("@@"), null,
+ new ColDataType("text")))
+ .withBehavior(CascadeBehavior.CASCADE).toString());
+ }
+
+ @Test
+ void transformFields() throws JSQLParserException {
+ Drop drop = parse(
+ "DROP TRANSFORM IF EXISTS FOR public.hstore LANGUAGE plpython3u RESTRICT");
+ assertEquals(Drop.ObjectType.TRANSFORM, drop.getObjectType());
+ TransformSignature transform = drop.getTransform();
+ assertEquals("public.hstore", transform.getDataType().getDataType());
+ assertEquals("plpython3u", transform.getLanguage());
+ assertEquals(CascadeBehavior.RESTRICT, drop.getBehavior());
+ assertTrue(drop.getNames().isEmpty());
+
+ transform.setLanguage("plperl");
+ assertEquals("DROP TRANSFORM IF EXISTS FOR public.hstore LANGUAGE plperl RESTRICT",
+ drop.toString());
+ assertRendersAlike(drop);
+
+ assertEquals("DROP TRANSFORM FOR hstore LANGUAGE plperl",
+ new Drop().withObjectType(Drop.ObjectType.TRANSFORM)
+ .withTransform(new TransformSignature(new ColDataType("hstore"), "plperl"))
+ .toString());
+ }
+
+ @Test
+ void signaturesSpanTheirSource() throws JSQLParserException {
+ String sql = "DROP CAST IF EXISTS (bigint AS int4) CASCADE";
+ assertSpans("(bigint AS int4)", sql, parse(sql).getCast());
+
+ sql = "DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint) RESTRICT";
+ Drop operators = parse(sql);
+ assertSpans("public.=== (integer, integer)", sql, operators.getOperators().get(0));
+ assertSpans("~ (NONE, bigint)", sql, operators.getOperators().get(1));
+ assertSpans("bigint", sql, operators.getOperators().get(1).getRightType());
+
+ sql = "DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u";
+ assertSpans("FOR public.hstore LANGUAGE plpython3u", sql, parse(sql).getTransform());
+ assertSpans(sql, sql, parse(sql));
+ }
+
+ @Test
+ void targetKindsReplaceEachOther() throws JSQLParserException {
+ Drop drop = parse("DROP OPERATOR === (integer, integer)");
+ drop.withObjectType(Drop.ObjectType.CAST)
+ .setCast(new CastSignature(new ColDataType("text"), new ColDataType("int")));
+ assertTrue(drop.getOperators().isEmpty());
+ assertEquals("DROP CAST (text AS int)", drop.toString());
+
+ drop.withObjectType(Drop.ObjectType.TRANSFORM)
+ .setTransform(new TransformSignature(new ColDataType("hstore"), "plperl"));
+ assertNull(drop.getCast());
+ assertEquals("DROP TRANSFORM FOR hstore LANGUAGE plperl", drop.toString());
+
+ drop.withObjectType(Drop.ObjectType.OPERATOR).setOperators(List.of(
+ new OperatorSignature(new ExclusionOperator().withName("+"), null,
+ new ColDataType("int"))));
+ assertNull(drop.getTransform());
+ assertEquals("DROP OPERATOR + (NONE, int)", drop.toString());
+
+ // the legacy name setters switch to plain names
+ drop.setName(new Table("op"));
+ assertTrue(drop.getOperators().isEmpty());
+ assertEquals("DROP OPERATOR op", drop.toString());
+
+ Drop cast = parse("DROP CAST (text AS integer) CASCADE");
+ cast.addNames(new Table("c"));
+ assertNull(cast.getCast());
+ assertEquals("DROP CAST c CASCADE", cast.toString());
+ }
+
+ @Test
+ void operatorNamesStayNamesOutsidePostgreSql() throws JSQLParserException {
+ Drop oracle = (Drop) CCJSqlParserUtil.parse("DROP OPERATOR app.eq_op",
+ parser -> parser.withDialect(Dialect.ORACLE));
+ assertEquals(Drop.ObjectType.OPERATOR, oracle.getObjectType());
+ assertEquals(List.of("app.eq_op"), names(oracle));
+ assertTrue(oracle.getOperators().isEmpty());
+ assertEquals("DROP OPERATOR app.eq_op", oracle.toString());
+
+ for (String sql : List.of("DROP OPERATOR IF EXISTS eq_op CASCADE", "DROP TRANSFORM t")) {
+ assertSqlCanBeParsedAndDeparsed(sql);
+ assertThrows(JSQLParserException.class,
+ () -> parse(sql), sql + " needs a signature in PostgreSQL");
+ }
+
+ // identifiers that start with an operator character or a quote are names too
+ assertEquals(List.of("`eq_op`"),
+ names((Drop) assertSqlCanBeParsedAndDeparsed("DROP OPERATOR `eq_op`")));
+ Drop temporary = (Drop) CCJSqlParserUtil.parse("DROP OPERATOR #tmp",
+ parser -> parser.withDialect(Dialect.SQLSERVER));
+ assertEquals(List.of("#tmp"), names(temporary));
+ assertTrue(temporary.getOperators().isEmpty());
+ }
+
+ @Test
+ void schemasNamedLikeTypeWordsQualifyOperators() throws JSQLParserException {
+ Drop drop = parse("DROP OPERATOR class.+ (int, int), family.- (NONE, int)");
+ assertEquals(Drop.ObjectType.OPERATOR, drop.getObjectType());
+ assertEquals(List.of("class.+", "family.-"), names(drop));
+ assertEquals(Drop.ObjectType.OPERATOR_CLASS,
+ parse("DROP OPERATOR CLASS c USING btree").getObjectType());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"DROP CAST text AS integer", "DROP CAST (text)",
+ "DROP CAST (text AS integer), (int AS text)", "DROP CAST (text AS integer) foo",
+ "DROP CAST IF EXISTS", "DROP CAST (text AS integer) LOCK = NONE",
+ "DROP CAST (text AS integer) CASCADE RESTRICT", "DROP OPERATOR === (integer)",
+ "DROP OPERATOR === (NONE, NONE)", "DROP OPERATOR ===", "DROP OPERATOR === ()",
+ "DROP OPERATOR === (int, int, int)", "DROP OPERATOR foo (int, int)",
+ "DROP OPERATOR foo", "DROP OPERATOR === (int, int) foo",
+ "DROP OPERATOR = = (int, int)", "DROP OPERATOR === (int, int) ON t",
+ "DROP OPERATOR === (int, int),", "DROP OPERATOR === (int, int) USING btree",
+ "DROP OPERATOR IF EXISTS", "DROP TRANSFORM FOR hstore",
+ "DROP TRANSFORM hstore LANGUAGE plpython3u", "DROP TRANSFORM FOR hstore LANGUAGE",
+ "DROP TRANSFORM FOR hstore LANGUAGE a, b",
+ "DROP TRANSFORM FOR hstore LANGUAGE plpython3u foo", "DROP TRANSFORM foo"})
+ void rejectsMalformedSignatures(String sql) {
+ assertThrows(JSQLParserException.class, () -> parse(sql), sql);
+ }
+
+ @Test
+ void pgDumpCleanScriptParses() throws JSQLParserException {
+ String script = "SET statement_timeout = 0;\n"
+ + "SELECT pg_catalog.set_config('search_path', '', false);\n"
+ + "DROP TRANSFORM IF EXISTS FOR public.hstore LANGUAGE plpython3u;\n"
+ + "DROP CAST IF EXISTS (text AS public.mood);\n"
+ + "DROP OPERATOR IF EXISTS public.~ (NONE, bigint);\n"
+ + "DROP OPERATOR IF EXISTS public.=== (integer, integer);\n"
+ + "DROP OPERATOR CLASS IF EXISTS public.myops USING btree;\n"
+ + "DROP FUNCTION IF EXISTS public.mood_from_text(text);\n"
+ + "DROP TYPE IF EXISTS public.mood;\n"
+ + "DROP EXTENSION IF EXISTS hstore;\n"
+ + "DROP SCHEMA IF EXISTS public;\n";
+ Statements statements = CCJSqlParserUtil.parseStatements(script,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ assertEquals(11, statements.size());
+ assertEquals(List.of(Drop.ObjectType.TRANSFORM, Drop.ObjectType.CAST,
+ Drop.ObjectType.OPERATOR, Drop.ObjectType.OPERATOR),
+ statements.subList(2, 6).stream().map(statement -> ((Drop) statement)
+ .getObjectType()).collect(Collectors.toList()));
+ assertEquals("DROP OPERATOR IF EXISTS public.=== (integer, integer)",
+ statements.get(5).toString());
+ }
+
+ private static void assertSpans(String expected, String sql, ASTNodeAccess part) {
+ Node node = part.getASTNode();
+ assertEquals(expected, sql.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1));
+ }
+
+ private static List names(Drop drop) {
+ return drop.getNames().stream().map(Table::getFullyQualifiedName)
+ .collect(Collectors.toList());
+ }
+
+ private static void assertRendersAlike(Drop drop) {
+ StringBuilder builder = new StringBuilder();
+ drop.accept(new StatementDeParser(builder), null);
+ assertEquals(drop.toString(), builder.toString());
+ }
+
+ private static Drop parse(String sql) throws JSQLParserException {
+ return (Drop) CCJSqlParserUtil.parse(sql,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ }
+
+ private static List roundTrip(String sql, Dialect dialect) throws JSQLParserException {
+ Drop drop = (Drop) assertSqlCanBeParsedAndDeparsed(sql, true,
+ parser -> parser.withDialect(dialect));
+ assertRendersAlike(drop);
+ return List.of(drop, (Drop) CCJSqlParserUtil.parse(drop.toString(),
+ parser -> parser.withDialect(dialect)));
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/DropValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/DropValidatorTest.java
index 9b6ed8a30..f7cc632f3 100644
--- a/src/test/java/net/sf/jsqlparser/util/validation/validator/DropValidatorTest.java
+++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/DropValidatorTest.java
@@ -77,6 +77,15 @@ public void testValidationDropRoutinesAndAccounts() throws JSQLParserException {
DatabaseType.MARIADB);
}
+ @Test
+ public void testValidationDropSignatureTargets() throws JSQLParserException {
+ String sql = "DROP CAST IF EXISTS (text AS integer) CASCADE;"
+ + " DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint);"
+ + " DROP TRANSFORM FOR hstore LANGUAGE plpython3u";
+ validateNoErrors(sql, 3, DatabaseType.POSTGRESQL);
+ validateNotAllowed(sql, 3, 3, FeaturesAllowed.DML, Feature.drop);
+ }
+
@Test
public void testValidationReassignOwned() throws JSQLParserException {
String sql = "REASSIGN OWNED BY old_role TO new_role";