diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java index 3b4bc996b..13a7c6886 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java @@ -30,6 +30,17 @@ public enum MatchType { } private Table table; + private String constraintName; + + /** Optional name of a column REFERENCES constraint. Table constraints own their name. */ + public String getConstraintName() { + return constraintName; + } + + public void setConstraintName(String constraintName) { + this.constraintName = constraintName; + } + private List referencedColumnNames; private MatchType matchType; private boolean usingPeriod; @@ -148,7 +159,11 @@ public ForeignKeyReference addReferencedColumnNames( @Override public String toString() { - StringBuilder builder = new StringBuilder("REFERENCES ").append(table); + StringBuilder builder = new StringBuilder(); + if (constraintName != null) { + builder.append("CONSTRAINT ").append(constraintName).append(' '); + } + builder.append("REFERENCES ").append(table); if (referencedColumnNames != null) { builder.append('('); for (int i = 0; i < referencedColumnNames.size(); i++) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..48b24df70 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14172,24 +14172,23 @@ ColumnOption ColumnDefinitionOption(): { LOOKAHEAD( ) defaultExpression=Expression() { option = ColumnOption.onUpdate(defaultExpression); } | - LOOKAHEAD( ) - { option = ColumnOption.constraint(new NamedConstraint().withType("PRIMARY KEY")); } + LOOKAHEAD([ RelObjectName() ] ( | )) + constraint=ColumnKeyConstraint() { option = ColumnOption.constraint(constraint); } | LOOKAHEAD([ RelObjectName() ] ) [ constraintName=RelObjectName() ] constraint=CheckConstraintSpec(constraintName) { option = ColumnOption.constraint(constraint); } | - LOOKAHEAD() constraint=ColumnUniqueConstraint() - { option = ColumnOption.constraint(constraint); } - | LOOKAHEAD({ isKeywordAhead("SERIAL") && getToken(2).kind == K_DEFAULT && getToken(3).kind == K_VALUE }) tk= { option = ColumnOption.serialDefaultValue(); } | - LOOKAHEAD() reference=ForeignKeyReferenceSpec(true) - { option = ColumnOption.reference(reference); } + LOOKAHEAD([ RelObjectName() ] ) + [ constraintName=RelObjectName() ] + reference=ForeignKeyReferenceSpec(true) + { reference.setConstraintName(constraintName); option = ColumnOption.reference(reference); } | LOOKAHEAD( ) { option = ColumnOption.raw("DROP", "DEFAULT"); } @@ -14230,17 +14229,30 @@ GeneratedColumnDefinition GeneratedColumnDefinition(): } } -NamedConstraint ColumnUniqueConstraint(): +/** Keeps a column key's name and attributes on the same node in CREATE and ALTER. */ +NamedConstraint ColumnKeyConstraint(): { - NamedConstraint constraint = new NamedConstraint().withType("UNIQUE"); + NamedConstraint constraint = new NamedConstraint(); + String name; Boolean nullsDistinct = null; } { - - [ [ { nullsDistinct = false; } ] { - constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct); - } ] - { return constraint; } + [ name=RelObjectName() { constraint.setName(name); } ] + ( + { constraint.setType("PRIMARY KEY"); } + | + { constraint.setType("UNIQUE"); } + [ [ { nullsDistinct = false; } ] { + constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct); + } ] + ) + PostgreSqlConstraintAttributes(constraint) + { + requireDdlSyntax(constraint.getConstraintAttributes() == null + || !constraint.getConstraintAttributes().isNotValid(), + "NOT VALID requires a table constraint"); + return constraint; + } } IdentityDefinition IdentityDefinition(): @@ -14841,6 +14853,15 @@ void PostgreSqlConstraintAttributes(Index index): { ConstraintAttributes attributes; } { attributes=PostgreSqlConstraintAttributeList(index.getConstraintAttributes()) + ( LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { + requireDdlSyntax(index instanceof CheckConstraint && !((CheckConstraint) index).isNoInherit(), + "NO INHERIT requires a CHECK constraint and cannot be repeated"); + ((CheckConstraint) index).setNoInherit(true); + } + attributes=PostgreSqlConstraintAttributeList(attributes) + )* { requireDdlSyntax(attributes == null || attributes.getEnforced() == null || index instanceof CheckConstraint || index instanceof ForeignKeyIndex, diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java new file mode 100644 index 000000000..fda9365cd --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnKeyMutationTest.java @@ -0,0 +1,90 @@ +/*- + * #%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.create; + +import static org.junit.jupiter.api.Assertions.*; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlColumnKeyMutationTest { + @ParameterizedTest + @ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"}) + void keyNameAndAttributesAreOwnedByOneNode(String key) throws JSQLParserException { + for (boolean alter : new boolean[] {false, true}) { + String prefix = alter ? "ALTER TABLE t ADD COLUMN " : "CREATE TABLE t ("; + String suffix = alter ? "" : ")"; + Statement statement = parse(prefix + "id INT CONSTRAINT old_name " + key + + " DEFERRABLE INITIALLY DEFERRED NOT NULL" + suffix); + ColumnDefinition column = alter + ? ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0) + : ((CreateTable) statement).getColumnDefinitions().get(0); + assertEquals(2, column.getColumnOptions().size()); + Index constraint = column.getColumnOptions().get(0).getConstraint(); + assertEquals("old_name", constraint.getName()); + assertTrue(constraint.getConstraintAttributes().getDeferrable()); + constraint.setName("new_name"); + constraint.getConstraintAttributes().setDeferrable(false); + constraint.getConstraintAttributes() + .setInitially(ConstraintAttributes.Initially.IMMEDIATE); + assertSql(statement, prefix + "id INT CONSTRAINT new_name " + key + + " NOT DEFERRABLE INITIALLY IMMEDIATE NOT NULL" + suffix); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT CONSTRAINT old_fk REFERENCES parent(id))", + "ALTER TABLE t ADD COLUMN id INT CONSTRAINT old_fk REFERENCES parent(id)"}) + void referenceNameCanBeReplacedAndRemoved(String sql) throws JSQLParserException { + Statement statement = parse(sql); + ForeignKeyReference reference = statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0).getForeignKeyReference() + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0) + .getForeignKeyReference(); + assertEquals("old_fk", reference.getConstraintName()); + reference.setConstraintName("new_fk"); + assertSql(statement, sql.replace("old_fk", "new_fk")); + reference.setConstraintName(null); + assertSql(statement, sql.replace("CONSTRAINT old_fk ", "")); + } + + @ParameterizedTest + @ValueSource(strings = {"NOT ENFORCED NO INHERIT", "NO INHERIT NOT ENFORCED", + "NOT VALID NO INHERIT NOT ENFORCED", "NOT ENFORCED NOT VALID NO INHERIT"}) + void tableCheckAllowsInheritanceAmongAttributes(String attributes) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CHECK (id > 0) " + attributes); + CheckConstraint check = (CheckConstraint) alter.getAlterExpressions().get(0).getIndex(); + assertTrue(check.isNoInherit()); + assertFalse(check.getEnforced()); + assertEquals(attributes.contains("NOT VALID"), + check.getConstraintAttributes().isNotValid()); + assertSql(alter, "ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT ENFORCED" + + (attributes.contains("NOT VALID") ? " NOT VALID" : "")); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertSql(Statement statement, String expected) throws JSQLParserException { + assertEquals(expected, statement.toString()); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + assertEquals(expected, parse(expected).toString()); + } +}