diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java new file mode 100644 index 000000000..d7f17d651 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java @@ -0,0 +1,57 @@ +/*- + * #%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 java.util.Objects; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; + +/** Changes an existing constraint using the attributes shared with CREATE and ADD. */ +public class AlterConstraintAttributes extends AlterExpression { + private ConstraintAttributes attributes = new ConstraintAttributes(); + + public AlterConstraintAttributes() { + setOperation(AlterOperation.ALTER); + setConstraintType("CONSTRAINT"); + } + + public ConstraintAttributes getAttributes() { + return attributes; + } + + public void setAttributes(ConstraintAttributes attributes) { + this.attributes = Objects.requireNonNull(attributes, "attributes"); + } + + @Override + public String getConstraintName() { + return getConstraintSymbol(); + } + + @Override + public void setConstraintName(String name) { + setConstraintSymbol(name); + } + + @Override + public boolean isEnforced() { + return Boolean.TRUE.equals(attributes.getEnforced()); + } + + @Override + public void setEnforced(boolean enforced) { + attributes.setEnforced(enforced); + } + + @Override + protected void appendBody(StringBuilder builder) { + builder.append("ALTER CONSTRAINT ").append(getConstraintSymbol()); + attributes.appendTo(builder); + } +} diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 4b505dab0..dbc7ebb95 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1543,11 +1543,16 @@ public class CCJSqlParser extends AbstractJSqlParser { } private boolean isPostgreSqlConstraintAttributeAhead() { - int kind = getToken(1).kind; - int next = getToken(2).kind; - return kind == K_DEFERRABLE || kind == K_ENFORCED || isKeywordAhead("INITIALLY") + return isPostgreSqlConstraintAttributeAhead(1); + } + + private boolean isPostgreSqlConstraintAttributeAhead(int offset) { + int kind = getToken(offset).kind; + int next = getToken(offset + 1).kind; + return kind == K_DEFERRABLE || kind == K_ENFORCED + || "INITIALLY".equalsIgnoreCase(getToken(offset).image) || kind == K_NOT && (next == K_DEFERRABLE || next == K_ENFORCED - || "VALID".equalsIgnoreCase(getToken(2).image)); + || "VALID".equalsIgnoreCase(getToken(offset + 1).image)); } private boolean isMySqlStatisticsOptionAhead() { @@ -18222,6 +18227,24 @@ AlterExpressionPartition AlterExpressionPostgreSqlPartitionOp(): * Dispatcher production for all ALTER TABLE expression types. * Delegates to focused sub-productions for each operation category. */ +AlterConstraintAttributes AlterConstraintAttributes(): +{ + AlterConstraintAttributes action = new AlterConstraintAttributes(); + String name; + ConstraintAttributes attributes; +} +{ + name=RelObjectName() + attributes=PostgreSqlConstraintAttributeList(null) + { + requireDdlSyntax(attributes != null && !attributes.isNotValid(), + "ALTER CONSTRAINT requires attributes and does not accept NOT VALID"); + action.setConstraintSymbol(name); + action.setAttributes(attributes); + return action; + } +} + AlterExpression AlterExpression(): { AlterExpression alterExp = null; @@ -18243,6 +18266,9 @@ AlterExpression AlterExpression(): | LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") }) alterExp=AlterSharedTableOption() + | + LOOKAHEAD({ getToken(1).kind == K_ALTER && getToken(2).kind == K_CONSTRAINT + && isPostgreSqlConstraintAttributeAhead(4) }) alterExp=AlterConstraintAttributes() | LOOKAHEAD({ isPostgreSqlRelationActionAhead() }) alterExp=PostgreSqlRelationAction() { requireDdlSyntax(((RelationAlterAction) alterExp).getColumnNumber() == null, @@ -18423,6 +18449,7 @@ AlterExpression AlterExpression(): i++; } + requireDdlSyntax(i > 0, "Expected an ALTER TABLE action"); alterExp.setOptionalSpecifier( optionalSpecifier.toString() ); } ) diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java new file mode 100644 index 000000000..01acb2523 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlConstraintAttributesChangeTest.java @@ -0,0 +1,97 @@ +/*- + * #%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.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +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; + +class PostgreSqlConstraintAttributesChangeTest { + @ParameterizedTest + @ValueSource(strings = {"DEFERRABLE", "NOT DEFERRABLE", "INITIALLY DEFERRED", + "INITIALLY IMMEDIATE", "DEFERRABLE INITIALLY DEFERRED", + "INITIALLY DEFERRED DEFERRABLE", "ENFORCED", "NOT ENFORCED", + "DEFERRABLE INITIALLY DEFERRED NOT ENFORCED", + "NOT ENFORCED INITIALLY IMMEDIATE NOT DEFERRABLE"}) + void sharesCreateAddAndAlterAttributes(String attributes) throws JSQLParserException { + String sql = "ALTER TABLE t ALTER CONSTRAINT fk " + attributes + ", ADD COLUMN z INT"; + Alter table = (Alter) parse(sql); + AlterConstraintAttributes change = assertInstanceOf(AlterConstraintAttributes.class, + table.getAlterExpressions().get(0)); + assertEquals(AlterOperation.ALTER, change.getOperation()); + assertEquals("CONSTRAINT", change.getConstraintType()); + assertEquals("fk", change.getConstraintSymbol()); + assertEquals("fk", change.getConstraintName()); + String declaration = "CONSTRAINT fk FOREIGN KEY(id) REFERENCES p(id) " + attributes; + CreateTable create = (CreateTable) parse("CREATE TABLE t(id INT, " + + declaration + ")"); + Alter add = (Alter) parse("ALTER TABLE t ADD " + declaration); + assertEquals(create.getIndexes().get(0).getConstraintAttributes().toString(), + change.getAttributes().toString()); + assertEquals( + add.getAlterExpressions().get(0).getIndex().getConstraintAttributes().toString(), + change.getAttributes().toString()); + assertEquals(2, table.getAlterExpressions().size()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + + @Test + void legacyAndStructuredMutationsStayConsistent() throws JSQLParserException { + Alter table = (Alter) parse("ALTER TABLE t ALTER CONSTRAINT fk ENFORCED"); + AlterConstraintAttributes action = + (AlterConstraintAttributes) table.getAlterExpressions().get(0); + action.setConstraintName("\"new fk\""); + assertEquals("\"new fk\"", action.getConstraintSymbol()); + action.setEnforced(false); + assertEquals(Boolean.FALSE, action.getAttributes().getEnforced()); + action.getAttributes().setDeferrable(true); + action.getAttributes().setInitially(ConstraintAttributes.Initially.DEFERRED); + assertEquals( + "ALTER TABLE t ALTER CONSTRAINT \"new fk\" DEFERRABLE INITIALLY DEFERRED NOT ENFORCED", + table.toString()); + action.getAttributes().setEnforced(true); + assertTrue(action.isEnforced()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(table)); + roundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"NOT VALID", "DEFERRABLE NOT VALID", "INITIALLY UNKNOWN", + "DEFERRABLE NOT DEFERRABLE", "ENFORCED NOT ENFORCED", + "INITIALLY DEFERRED INITIALLY IMMEDIATE", "DEFERRABLE,"}) + void rejectsWrongOrDuplicateAlterAttributes(String attributes) { + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLE t ALTER CONSTRAINT fk " + attributes)); + } + + private static net.sf.jsqlparser.statement.Statement parse(String sql) + throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect( + net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)); + } + + private static void roundTrip(Alter table) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + table.accept(new StatementDeParser(output)); + assertEquals(table.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +}