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 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 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";