diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 58c277a88..0f62330c2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -504,6 +504,11 @@ public void setColumnOldName(String columnOldName) { this.columnOldName = columnOldName; } + /** + * Returns the constraint name, including any SQL identifier quotes. For named + * {@link AlterOperation#DROP_FOREIGN_KEY} and {@link AlterOperation#DROP_CHECK} actions, this + * is the deletion target; {@link #getPkColumns()} is not used. + */ public String getConstraintName() { return this.constraintName; } @@ -884,6 +889,7 @@ protected boolean isDropSpecialOperation() { case DROP_PRIMARY_KEY: case DROP_UNIQUE: case DROP_FOREIGN_KEY: + case DROP_CHECK: return true; case DROP: return columnName == null && pkColumns != null && !pkColumns.isEmpty(); @@ -1041,8 +1047,15 @@ protected void toStringDropSpecial(StringBuilder b) { b.append("DROP UNIQUE (").append(PlainSelect.getStringList(pkColumns)).append(')'); break; case DROP_FOREIGN_KEY: - b.append("DROP FOREIGN KEY (").append(PlainSelect.getStringList(pkColumns)) - .append(')'); + b.append("DROP FOREIGN KEY "); + if (constraintName != null) { + b.append(constraintName); + } else { + b.append('(').append(PlainSelect.getStringList(pkColumns)).append(')'); + } + break; + case DROP_CHECK: + b.append("DROP CHECK ").append(constraintName); break; default: // Oracle Multi Column Drop diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java index 37e3b8587..d8a56fbd7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java @@ -19,18 +19,11 @@ public class AlterExpressionDrop extends AlterExpression { @Override protected void appendBody(StringBuilder b) { + if (isDropSpecialOperation()) { + toStringDropSpecial(b); + return; + } switch (getOperation()) { - case DROP_PRIMARY_KEY: - b.append("DROP PRIMARY KEY "); - break; - case DROP_UNIQUE: - b.append("DROP UNIQUE (") - .append(PlainSelect.getStringList(getPkColumns())).append(')'); - break; - case DROP_FOREIGN_KEY: - b.append("DROP FOREIGN KEY (") - .append(PlainSelect.getStringList(getPkColumns())).append(')'); - break; case DROP_PARTITION: b.append("DROP PARTITION ") .append(PlainSelect.getStringList(getPartitions())); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index 52bdc23ce..bca23bbe4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1411612de..edc608fb7 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -17053,6 +17053,12 @@ AlterExpression AlterExpressionDrop(): { { alterExp.setOperation(AlterOperation.DROP); } ( + LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_CHECK + || getToken(1).kind == K_FOREIGN && getToken(2).kind == K_KEY + && getToken(3).kind != OPENING_BRACKET) }) + MySqlDropNamedConstraint(alterExp) + | ( { alterExp = new AlterExpressionPartition(); @@ -17118,6 +17124,15 @@ AlterExpression AlterExpressionDrop(): { return alterExp; } } +/** A MySQL deletion target is a constraint name, not a list of key columns. */ +void MySqlDropNamedConstraint(AlterExpression alterExp): +{ String name; } +{ + ( { alterExp.setOperation(AlterOperation.DROP_FOREIGN_KEY); } + | { alterExp.setOperation(AlterOperation.DROP_CHECK); } ) + name=RelObjectName() { alterExp.setConstraintName(name); } +} + /** * Parses partition maintenance operations within AlterExpression. * Handles: TRUNCATE/ANALYZE/CHECK/OPTIMIZE/REBUILD/REPAIR PARTITION, diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6e4c96feb..09a373fad 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -252,6 +252,29 @@ explicit index owners, without reporting catalog-only object names as tables. The statement deparser delegates real tables to its configured select deparser. MySQL ALGORITHM/LOCK tokens retain their order and optional equals signs. +MySQL named constraint drops +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With ``Dialect.MYSQL``, ``DROP FOREIGN KEY name`` and ``DROP CHECK name`` +expose the target through ``AlterExpression.getConstraintName()``. Inspect +``getOperation()`` for ``DROP_FOREIGN_KEY`` or ``DROP_CHECK``. Identifier +quotes are preserved, and changing the name changes both SQL renderers. +These named targets do not populate the legacy ``getPkColumns()`` list. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child DROP FOREIGN KEY fk_parent", + parser -> parser.withDialect(Dialect.MYSQL)); + AlterExpression drop = alter.getAlterExpressions().get(0); + drop.setConstraintName("fk_customer"); + // ALTER TABLE child DROP FOREIGN KEY fk_customer + String sql = alter.toString(); + + // The same API also supports constructing an action without parsing SQL. + AlterExpression checkDrop = new AlterExpression() + .withOperation(AlterOperation.DROP_CHECK).withConstraintName("positive_id"); + Inspect PostgreSQL schema statements ------------------------------------ diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java new file mode 100644 index 000000000..8e9d5f7a7 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlDropConstraintTest.java @@ -0,0 +1,91 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +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; + +class MySqlDropConstraintTest { + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY fk1", "DROP FOREIGN KEY `fk name`", + "DROP CHECK c1", "DROP CHECK `check name`", + "DROP FOREIGN KEY fk1, DROP CHECK c1", + "DROP FOREIGN KEY fk1, ALGORITHM = INPLACE, LOCK = NONE"}) + void roundTripsNamedDrops(String actions) throws JSQLParserException { + Alter alter = parse("ALTER TABLE t " + actions); + assertEquals("ALTER TABLE t " + actions, alter.toString()); + assertRoundTrip(alter); + } + + @Test + void namesAndKindsAreEditableWithoutColumnLists() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t DROP FOREIGN KEY fk1, DROP CHECK c1"); + AlterExpression foreign = alter.getAlterExpressions().get(0); + AlterExpression check = alter.getAlterExpressions().get(1); + assertEquals(AlterOperation.DROP_FOREIGN_KEY, foreign.getOperation()); + assertEquals("fk1", foreign.getConstraintName()); + assertNull(foreign.getPkColumns()); + assertEquals(AlterOperation.DROP_CHECK, check.getOperation()); + assertEquals("c1", check.getConstraintName()); + check.setConstraintName("`new check`"); + foreign.setConstraintName("fk2"); + assertEquals("ALTER TABLE t DROP FOREIGN KEY fk2, DROP CHECK `new check`", + alter.toString()); + assertRoundTrip(alter); + } + + @Test + void constructedActionsUseTheSameRendererAsParsedActions() throws JSQLParserException { + for (AlterOperation operation : List.of(AlterOperation.DROP_FOREIGN_KEY, + AlterOperation.DROP_CHECK)) { + Alter alter = new Alter(); + alter.setTable(new Table("t")); + alter.addAlterExpression(new AlterExpression().withOperation(operation) + .withConstraintName("`target constraint`")); + Alter parsed = parse(alter.toString()); + assertEquals(operation, parsed.getAlterExpressions().get(0).getOperation()); + assertEquals("`target constraint`", + parsed.getAlterExpressions().get(0).getConstraintName()); + assertRoundTrip(alter); + // Editing the operation on either concrete class must have the same effect. + parsed.getAlterExpressions().get(0).setOperation(AlterOperation.DROP_CHECK); + assertEquals("ALTER TABLE t DROP CHECK `target constraint`", parsed.toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN KEY (id)", "DROP UNIQUE (a, b)", + "DROP PRIMARY KEY", "DROP CONSTRAINT IF EXISTS ck CASCADE", "DROP (a, b)", + "DROP COLUMN c", "DROP INDEX idx"}) + void preservesDefaultDropForms(String action) throws JSQLParserException { + String sql = "ALTER TABLE t " + action; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + assertEquals(sql, alter.toString().trim()); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(alter.toString(), parse(sql.toString()).toString()); + } +}