From d9730d6c8f9457ed2c57a9fce85f50ddc15c417e Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 5 Nov 2026 12:52:53 +0900 Subject: [PATCH] fix: parse PostgreSQL DROP CAST, DROP OPERATOR and DROP TRANSFORM DROP CAST (source AS target), DROP OPERATOR op (left, right) and DROP TRANSFORM FOR type LANGUAGE lang failed in every dialect. pg_dump --clean writes them for every cast, operator and transform, so such a dump could not be parsed as a script. Drop.ObjectType gains CAST, OPERATOR and TRANSFORM, and their targets are typed: getCast() is a CastSignature, getOperators() holds an OperatorSignature per operator, with the operator reference COMMENT ON OPERATOR uses and null as the NONE marker of a missing operand, and getTransform() is a TransformSignature. toString() and the deparser render them through the same appendTo(), and they carry AST nodes spanning their source. Oracle's DROP OPERATOR name keeps a plain name. With the PostgreSQL dialect DROP OPERATOR and DROP TRANSFORM require their signatures, and a signature is followed only by CASCADE or RESTRICT. Signed-off-by: minleejae --- .../jsqlparser/statement/CastSignature.java | 69 ++++ .../statement/OperatorSignature.java | 95 ++++++ .../statement/TransformSignature.java | 69 ++++ .../create/table/ExclusionOperator.java | 2 +- .../sf/jsqlparser/statement/drop/Drop.java | 113 ++++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 99 ++++++ src/site/sphinx/usage.rst | 19 +- .../parser/ASTNodeCoverageTest.java | 5 +- .../drop/DropSignatureTargetTest.java | 315 ++++++++++++++++++ .../validator/DropValidatorTest.java | 9 + 10 files changed, 780 insertions(+), 15 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/CastSignature.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/OperatorSignature.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/TransformSignature.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/drop/DropSignatureTargetTest.java 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 0000000000..2aeb0bb286 --- /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 0000000000..75957e3cce --- /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 0000000000..2ceca11a5e --- /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 182b1e124b..f2393760f4 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 2c29b8dc8a..86ad0c03fc 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 operators) { + clearTargets(); + if (operators != null) { + this.operators.addAll(operators); + } + } + + public Drop withOperators(Collection 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 76fc4f7041..c61c7418ca 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 f56cb9ea95..ecf447ba8b 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 0ac7a4e84a..25dd26d38d 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 0000000000..47008ab925 --- /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 9b6ed8a302..f7cc632f31 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";