From f3c2358619222d179da89da87f9cb9a65077974d Mon Sep 17 00:00:00 2001 From: minleejae Date: Sun, 20 Sep 2026 20:54:02 +0900 Subject: [PATCH] fix: reuse table constraints for MySQL ALTER TABLE Signed-off-by: minleejae --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 12 +- .../alter/MySqlAlterConstraintTest.java | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5e6d9c46e..3d486d847 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1791,7 +1791,9 @@ public class CCJSqlParser extends AbstractJSqlParser { ? ((NamedConstraint) index).getIndexName() : index.getName()); alterExp.setUk(index.getType().toUpperCase(Locale.ROOT).contains("KEY")); alterExp.setUkTypeSpecified(index.getIndexKeyword() != null); - for (String option : new ArrayList(index.getIndexSpec())) { + // Named constraints render their own options; only legacy indexes need projection. + for (String option : index instanceof NamedConstraint ? Collections.emptyList() + : new ArrayList(index.getIndexSpec())) { if (option.toUpperCase(Locale.ROOT).startsWith("USING ")) { alterExp.addParameters("USING"); alterExp.addParameters(option.substring("USING ".length())); @@ -14594,7 +14596,8 @@ Index TableIndexSpec(boolean createContext): columns=IndexColumnsWithParamsList() TableIndexOptions(createContext, indexOptions) { - if (createContext) { + // MySQL ALTER preserves both the constraint symbol and the index name. + if (createContext || Dialect.MYSQL.name().equals(getAsString(Feature.dialect))) { index = new NamedConstraint() .withIndexName(indexName) .withType(typeToken.image @@ -15584,7 +15587,7 @@ CheckConstraint CheckConstraintSpec(String constraintName): CheckConstraint checkConstraint; } { - ( LOOKAHEAD(2) "(" exp = Expression() ")" )* + "(" exp = Expression() ")" [ LOOKAHEAD(2) [ { enforced = false; } ] { if (enforced == null) { enforced = true; } } @@ -17071,7 +17074,8 @@ AlterExpression AlterExpressionAddAlterModify(): } | LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD - && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + || Dialect.MYSQL.name().equals(getAsString(Feature.dialect))) && (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY || getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN || getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE) }) diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java new file mode 100644 index 000000000..88997b343 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterConstraintTest.java @@ -0,0 +1,119 @@ +/*- + * #%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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +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 MySqlAlterConstraintTest { + @ParameterizedTest + @ValueSource(strings = { + "CONSTRAINT uq UNIQUE KEY idx (id)", + "CONSTRAINT uq UNIQUE INDEX idx USING BTREE (id DESC) COMMENT 'unique id'", + "CONSTRAINT `unique id` UNIQUE KEY `index id` (id)", + "CONSTRAINT UNIQUE KEY idx (id)", + "CONSTRAINT uq UNIQUE (id)", + "UNIQUE KEY idx (id)", + "CONSTRAINT pk PRIMARY KEY (id)", + "CONSTRAINT PRIMARY KEY (id)", + "CHECK (id > 0)", + "CONSTRAINT CHECK (id > 0) NOT ENFORCED", + "CONSTRAINT positive CHECK (id > 0) ENFORCED", + "CONSTRAINT fk FOREIGN KEY parent_idx (id) REFERENCES parent (id) ON DELETE CASCADE" + }) + void createAndAlterExposeTheSameConstraint(String definition) throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT, " + definition + ")", p -> p.withDialect(Dialect.MYSQL)); + Alter alter = parse("ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT"); + Index actual = alter.getAlterExpressions().get(0).getIndex(); + assertEquals(create.getIndexes().get(0).getClass(), actual.getClass()); + assertEquals(create.getIndexes().get(0).toString(), actual.toString()); + assertEquals(2, alter.getAlterExpressions().size()); + assertRoundTrip(alter); + } + + @Test + void constraintAndIndexNamesCanBeEditedIndependently() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE KEY idx (id)"); + NamedConstraint unique = assertInstanceOf(NamedConstraint.class, + alter.getAlterExpressions().get(0).getIndex()); + assertEquals("uq", unique.getName()); + assertEquals("idx", unique.getIndexName()); + assertEquals(Index.Kind.UNIQUE, unique.getKind()); + unique.setName("new_constraint"); + unique.setIndexName("new_index"); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_constraint UNIQUE KEY new_index (id)", + alter.toString()); + assertRoundTrip(alter); + } + + @Test + void unnamedCheckRetainsKeywordEnforcementAndExpressionVisitor() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT CHECK (id > 0) NOT ENFORCED"); + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + alter.getAlterExpressions().get(0).getIndex()); + assertTrue(check.isUseConstraintKeyword()); + assertEquals(Boolean.FALSE, check.getEnforced()); + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(10); + } + }; + alter.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + assertEquals("ALTER TABLE t ADD CONSTRAINT CHECK (id > 10) NOT ENFORCED", + buffer.toString()); + assertRoundTrip(parse(buffer.toString())); + } + + @Test + void sharedIndexStillProvidesLegacyPrimaryKeyColumns() throws JSQLParserException { + AlterExpression primary = parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id)") + .getAlterExpressions().get(0); + assertEquals(List.of("id"), primary.getPkColumns()); + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK", "CHECK ()", "CONSTRAINT CHECK (id > 0) (id < 10)", + "CONSTRAINT uq UNIQUE KEY idx ()"}) + void rejectsIncompleteOrRepeatedConstraintBodies(String definition) { + assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t ADD " + definition)); + } + + 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 buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), parse(buffer.toString()).toString()); + } +}