diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 755d965fc0..eb02b6db01 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2530,6 +2530,85 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Decides whether an ALTER TABLE ADD table constraint needs the table-constraint production + * shared with CREATE TABLE in dialects that otherwise keep the generic ALTER representation. A + * nested class keeps its branches out of CCJSqlParser.class, whose constant pool is close to + * what JaCoCo can still instrument. + */ + private static final class AlterConstraintLookahead { + private AlterConstraintLookahead() {} + + /** + * Whether the action starting at {@code first} is an unnamed CHECK, a FOREIGN KEY, + * EXCLUDE, or uses a constraint attribute (INITIALLY IMMEDIATE|DEFERRED, NOT VALID, or + * ENFORCED outside CHECK) before the end of the action. + */ + static boolean needsSharedTableConstraint(Token first, CCJSqlParserTokenManager source) { + Token second = next(first, source); + Token body = first.kind == K_CONSTRAINT ? next(second, source) : first; + if (isSharedOnlyConstraint(first, second, body, source)) { + return true; + } + return (first.kind == K_CONSTRAINT || first.kind == K_PRIMARY + || first.kind == K_UNIQUE) + && hasConstraintAttribute(first, body.kind == K_CHECK, source); + } + + /** An unnamed CHECK, a FOREIGN KEY or an EXCLUDE constraint. */ + private static boolean isSharedOnlyConstraint(Token first, Token second, Token body, + CCJSqlParserTokenManager source) { + return first.kind == K_CHECK && second.kind == OPENING_BRACKET + || first.kind == K_FOREIGN || isExcludeConstraint(body, source); + } + + /** Whether a constraint attribute follows at the top level before the end of the action. */ + private static boolean hasConstraintAttribute(Token first, boolean check, + CCJSqlParserTokenManager source) { + int depth = 0; + for (Token token = first; token.kind != EOF; token = next(token, source)) { + if (token.kind == OPENING_BRACKET) { + depth++; + } else if (token.kind == CLOSING_BRACKET) { + if (depth-- == 0) { + return false; + } + } else if (depth == 0) { + if (token.kind == ST_SEMICOLON || token.kind == K_COMMA) { + return false; + } + if (isConstraintAttribute(token, next(token, source), check)) { + return true; + } + } + } + return false; + } + + private static boolean isConstraintAttribute(Token token, Token next, boolean check) { + if ("INITIALLY".equalsIgnoreCase(token.image)) { + return "IMMEDIATE".equalsIgnoreCase(next.image) + || "DEFERRED".equalsIgnoreCase(next.image); + } + return token.kind == K_NOT && "VALID".equalsIgnoreCase(next.image) + || token.kind == K_ENFORCED && !check; + } + + /** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */ + private static boolean isExcludeConstraint(Token token, CCJSqlParserTokenManager source) { + int following = next(token, source).kind; + return token.kind == K_EXCLUDE && (following == K_USING + || following == OPENING_BRACKET || following == K_WHERE); + } + + private static Token next(Token token, CCJSqlParserTokenManager source) { + if (token.next == null) { + token.next = source.getNextToken(); + } + return token.next; + } + } + /** * True when an IS after a condition opens a postfix test * (IS [NOT] TRUE/FALSE/UNKNOWN/NULL/JSON) rather than IS DISTINCT FROM or @@ -3050,6 +3129,8 @@ public class CCJSqlParser extends AbstractJSqlParser { && "EXPRESSION".equalsIgnoreCase(getToken(columnActionOffset + 1).image)) { return true; } + // VALIDATE CONSTRAINT starts no other ALTER TABLE action in any dialect. + if (first.kind == K_VALIDATE && getToken(2).kind == K_CONSTRAINT) { return true; } if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; } if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT") || isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER") @@ -21032,13 +21113,14 @@ AlterExpression AlterExpressionAddAlterModify(): constraint=PostgreSqlConstraintUsingIndex() { alterExp.setConstraint(constraint); } | 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 || getToken(1).kind == K_NOT - && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) }) + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) + || AlterConstraintLookahead.needsSharedTableConstraint(getToken(1), token_source)) }) element=TableConstraint(false) { setAlterTableElement(alterExp, element); } | LOOKAHEAD({ isTableIndexAhead() }) element=TableIndexSpec(false) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java new file mode 100644 index 0000000000..ce41db3d31 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java @@ -0,0 +1,128 @@ +/*- + * #%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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * ALTER TABLE ADD accepts the table constraint forms of CREATE TABLE in every dialect, not only + * with the MySQL and PostgreSQL dialects. + */ +class AlterAddTableConstraintTest { + private static final List DIALECTS = + Arrays.asList(null, Dialect.ANSI_SQL, Dialect.ORACLE, Dialect.SQLSERVER, Dialect.H2); + + @ParameterizedTest + @ValueSource(strings = { + "CHECK (id > 0)", + "CHECK (id > 0) NOT VALID", + "CONSTRAINT ck CHECK (id > 0) NOT VALID", + "FOREIGN KEY (id) REFERENCES parent (id) DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) NOT VALID", + "CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED", + "CONSTRAINT uq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE", + "CONSTRAINT ex EXCLUDE USING gist (id WITH =)"}) + void createAndAlterExposeTheSameConstraintInEveryDialect(String definition) + throws JSQLParserException { + for (Dialect dialect : DIALECTS) { + Consumer configuration = parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + CreateTable create = (CreateTable) CCJSqlParserUtil + .parse("CREATE TABLE t (id INT, " + definition + ")", configuration); + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT", true, + configuration); + NamedConstraint expected = create.getTableConstraints().get(0); + NamedConstraint actual = alter.getAlterExpressions().get(0).getConstraint(); + assertEquals(expected.getClass(), actual.getClass(), String.valueOf(dialect)); + assertEquals(expected.toString(), actual.toString(), String.valueOf(dialect)); + assertEquals(2, alter.getAlterExpressions().size()); + } + } + + @Test + void attributesAndReferencesAreStructuredWithoutADialect() throws JSQLParserException { + String sql = "ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) " + + "DEFERRABLE INITIALLY DEFERRED"; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + ForeignKeyConstraint foreignKey = assertInstanceOf(ForeignKeyConstraint.class, + alter.getAlterExpressions().get(0).getConstraint()); + ConstraintAttributes attributes = foreignKey.getConstraintAttributes(); + assertEquals(Boolean.TRUE, attributes.getDeferrable()); + assertEquals(ConstraintAttributes.Initially.DEFERRED, attributes.getInitially()); + assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("t", "parent"); + + CheckConstraint check = assertInstanceOf(CheckConstraint.class, + ((Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CHECK (id > 0) NOT VALID")) + .getAlterExpressions().get(0).getConstraint()); + assertNull(check.getName()); + assertTrue(check.getConstraintAttributes().isNotValid()); + } + + @Test + void formsThatAlreadyParsedKeepTheGenericRepresentation() throws JSQLParserException { + AlterExpression deferrable = ((Alter) CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE")) + .getAlterExpressions().get(0); + assertThat(deferrable.getConstraints()).hasSize(1); + assertInstanceOf(DeferrableConstraint.class, deferrable.getConstraints().get(0)); + + AlterExpression unique = ((Alter) CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (id)")) + .getAlterExpressions().get(0); + assertEquals("UNIQUE KEY", unique.getConstraintType()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id", + "ALTER TABLE t ADD CONSTRAINT uk UNIQUE USING INDEX t_idx"}) + void postgreSqlOnlyFormsStillRequireThePostgreSqlDialect(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertDoesNotThrow( + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + @Test + void enforcedPrimaryKeyIsRejected() { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil + .parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) ENFORCED")); + assertThat(exception).hasStackTraceContaining( + "ENFORCED is supported only for CHECK and foreign key constraints"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java index bc3f6ecb66..e7d10cddef 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java @@ -229,6 +229,23 @@ void bulkTablespaceMovesShareOneModel(String objectType) throws JSQLParserExcept assertEquals(2, CCJSqlParserUtil.parseStatements(statement + "; SELECT 1").size()); } + @Test + void validateConstraintIsStructuredWithoutThePostgreSqlDialect() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {null, Dialect.ANSI_SQL, Dialect.MYSQL}) { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD CHECK (c > 0) NOT VALID, VALIDATE CONSTRAINT ck", true, + p -> { + if (dialect != null) { + p.withDialect(dialect); + } + }); + RelationAlterAction validate = assertInstanceOf(RelationAlterAction.class, + alter.getAlterExpressions().get(1)); + assertEquals(RelationAlterAction.Kind.VALIDATE_CONSTRAINT, validate.getKind()); + assertEquals("ck", validate.getConstraintName()); + } + } + private static Statement parse(String sql) throws JSQLParserException { return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); }