Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ public class CheckConstraint extends NamedConstraint {

private Expression expression;

private Boolean enforced;

private boolean noInherit;

public CheckConstraint() {
Expand Down Expand Up @@ -61,11 +59,19 @@ public CheckConstraint withNoInherit(boolean noInherit) {
}

public Boolean getEnforced() {
return enforced;
return getConstraintAttributes() == null ? null : getConstraintAttributes().getEnforced();
}

public void setEnforced(Boolean enforced) {
this.enforced = enforced;
ConstraintAttributes attributes = getConstraintAttributes();
if (attributes == null) {
if (enforced == null) {
return;
}
attributes = new ConstraintAttributes();
setConstraintAttributes(attributes);
}
attributes.setEnforced(enforced);
}

@Override
Expand All @@ -81,9 +87,6 @@ public void appendTo(StringBuilder b, Consumer<Expression> expressionPrinter) {
if (noInherit) {
b.append(" NO INHERIT");
}
if (enforced != null) {
b.append(enforced ? " ENFORCED" : " NOT ENFORCED");
}
appendConstraintSuffixTo(b);
appendConstraintAttributesTo(b);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public enum Initially {
private Boolean deferrable;
private Initially initially;
private boolean notValid;
private Boolean enforced;

public Boolean getDeferrable() {
return deferrable;
Expand All @@ -45,13 +46,25 @@ public void setNotValid(boolean notValid) {
this.notValid = notValid;
}

/** Null preserves an omitted ENFORCED clause. */
public Boolean getEnforced() {
return enforced;
}

public void setEnforced(Boolean enforced) {
this.enforced = enforced;
}

public void appendTo(StringBuilder sql) {
if (deferrable != null) {
sql.append(deferrable ? " DEFERRABLE" : " NOT DEFERRABLE");
}
if (initially != null) {
sql.append(" INITIALLY ").append(initially);
}
if (enforced != null) {
sql.append(enforced ? " ENFORCED" : " NOT ENFORCED");
}
if (notValid) {
sql.append(" NOT VALID");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,20 @@ public enum MatchType {
private Table table;
private List<String> referencedColumnNames;
private MatchType matchType;
private ConstraintAttributes constraintAttributes;
private final Set<ReferentialAction> referentialActions = new LinkedHashSet<>(2);

/**
* Attributes of a column REFERENCES clause; table constraints own their attributes on Index.
*/
public ConstraintAttributes getConstraintAttributes() {
return constraintAttributes;
}

public void setConstraintAttributes(ConstraintAttributes constraintAttributes) {
this.constraintAttributes = constraintAttributes;
}

public Table getTable() {
return table;
}
Expand Down Expand Up @@ -130,6 +142,9 @@ public String toString() {
builder.append(" MATCH ").append(matchType);
}
referentialActions.forEach(builder::append);
if (constraintAttributes != null) {
constraintAttributes.appendTo(builder);
}
return builder.toString();
}
}
91 changes: 69 additions & 22 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,14 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
|| "\"XML\"".equalsIgnoreCase(name);
}

private boolean isPostgreSqlConstraintAttributeAhead() {
int kind = getToken(1).kind;
int next = getToken(2).kind;
return kind == K_DEFERRABLE || kind == K_ENFORCED || isKeywordAhead("INITIALLY")
|| kind == K_NOT && (next == K_DEFERRABLE || next == K_ENFORCED
|| "VALID".equalsIgnoreCase(getToken(2).image));
}

private boolean isMySqlStatisticsOptionAhead() {
String name = getToken(1).image;
return "STATS_AUTO_RECALC".equalsIgnoreCase(name)
Expand Down Expand Up @@ -14157,7 +14165,7 @@ ColumnOption ColumnDefinitionOption(): {
tk=<S_IDENTIFIER> <K_DEFAULT> <K_VALUE>
{ option = ColumnOption.serialDefaultValue(); }
|
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec()
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec(true)
{ option = ColumnOption.reference(reference); }
|
LOOKAHEAD(<K_DROP> <K_DEFAULT>) <K_DROP> <K_DEFAULT>
Expand Down Expand Up @@ -14794,25 +14802,61 @@ void PostgreSqlConstraintOptions(Index index):
}

void PostgreSqlConstraintAttributes(Index index):
{ ConstraintAttributes attributes; }
{
attributes=PostgreSqlConstraintAttributeList(index.getConstraintAttributes())
{
requireDdlSyntax(attributes == null || attributes.getEnforced() == null
|| index instanceof CheckConstraint || index instanceof ForeignKeyIndex,
"ENFORCED is supported only for CHECK and foreign key constraints");
index.setConstraintAttributes(attributes);
}
}

/** Shared attribute parsing for table constraints and column REFERENCES clauses. */
ConstraintAttributes PostgreSqlConstraintAttributeList(ConstraintAttributes attributes):
{
ConstraintAttributes attributes = new ConstraintAttributes();
boolean present = false;
boolean deferrable = true;
boolean present = attributes != null;
boolean negative;
Boolean enforced;
Token token;
}
{
[ LOOKAHEAD(2) [ <K_NOT> { deferrable = false; } ] <K_DEFERRABLE> {
attributes.setDeferrable(deferrable); present = true;
} ]
[ LOOKAHEAD({ isKeywordAhead("INITIALLY") }) token=<S_IDENTIFIER> token=<S_IDENTIFIER> {
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
"Expected IMMEDIATE or DEFERRED");
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
present = true;
} ]
[ LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
<K_NOT> token=<S_IDENTIFIER> { attributes.setNotValid(true); present = true; } ]
{ if (present) { index.setConstraintAttributes(attributes); } }
{ if (attributes == null) { attributes = new ConstraintAttributes(); } }
( LOOKAHEAD({ isPostgreSqlConstraintAttributeAhead() }) (
LOOKAHEAD(2) { negative = false; }
[ <K_NOT> { negative = true; } ] <K_DEFERRABLE> {
requireDdlSyntax(attributes.getDeferrable() == null, "Duplicate DEFERRABLE clause");
attributes.setDeferrable(!negative); present = true;
}
|
LOOKAHEAD({ isKeywordAhead("INITIALLY") }) <S_IDENTIFIER> token=<S_IDENTIFIER> {
requireDdlSyntax(attributes.getInitially() == null, "Duplicate INITIALLY clause");
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
"Expected IMMEDIATE or DEFERRED");
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
present = true;
}
|
LOOKAHEAD(2) enforced=ConstraintEnforcement() {
requireDdlSyntax(attributes.getEnforced() == null, "Duplicate ENFORCED clause");
attributes.setEnforced(enforced); present = true;
}
|
LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
<K_NOT> <S_IDENTIFIER> {
requireDdlSyntax(!attributes.isNotValid(), "Duplicate NOT VALID clause");
attributes.setNotValid(true); present = true;
}
) )*
{ return present ? attributes : null; }
}

Boolean ConstraintEnforcement():
{ boolean enforced = true; }
{
[ <K_NOT> { enforced = false; } ] <K_ENFORCED>
{ return enforced; }
}

/**
Expand Down Expand Up @@ -15748,7 +15792,7 @@ void ReferentialActions(ForeignKeyReference reference):
)]
}

ForeignKeyReference ForeignKeyReferenceSpec():
ForeignKeyReference ForeignKeyReferenceSpec(boolean columnContext):
{
ForeignKeyReference reference = new ForeignKeyReference();
ForeignKeyReference.MatchType matchType;
Expand All @@ -15775,6 +15819,12 @@ ForeignKeyReference ForeignKeyReferenceSpec():
]
ReferentialActions(reference)
{
if (columnContext) {
ConstraintAttributes attributes = PostgreSqlConstraintAttributeList(null);
requireDdlSyntax(attributes == null || !attributes.isNotValid(),
"NOT VALID requires a table constraint");
reference.setConstraintAttributes(attributes);
}
return reference;
}
}
Expand All @@ -15795,10 +15845,7 @@ CheckConstraint CheckConstraintSpec(String constraintName):
[ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
&& getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
<K_NO> TypeDdlKeyword("INHERIT") { noInherit = true; } ]
[ LOOKAHEAD(2)
[ <K_NOT> { enforced = false; } ]
<K_ENFORCED> { if (enforced == null) { enforced = true; } }
]
[ LOOKAHEAD(2) enforced=ConstraintEnforcement() ]
{
checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp)
.withEnforced(enforced).withNoInherit(noInherit);
Expand Down Expand Up @@ -15828,7 +15875,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName):
if (constraintName != null) { fkIndex.setName(constraintName); }
fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames);
}
reference=ForeignKeyReferenceSpec() { fkIndex.setReference(reference); }
reference=ForeignKeyReferenceSpec(false) { fkIndex.setReference(reference); }
{
return fkIndex;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*-
* #%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 java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
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.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PostgreSqlConstraintEnforcementTest {
@ParameterizedTest
@ValueSource(strings = {"ENFORCED", "NOT ENFORCED",
"DEFERRABLE INITIALLY DEFERRED NOT ENFORCED",
"NOT ENFORCED DEFERRABLE INITIALLY DEFERRED",
"INITIALLY IMMEDIATE NOT DEFERRABLE ENFORCED"})
void sharesForeignKeyAttributesAcrossCreateAndAlter(String attributes)
throws JSQLParserException {
for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) {
Statement statement =
parse(prefix + "CONSTRAINT fk FOREIGN KEY (id) REFERENCES public.p (id) "
+ "MATCH SIMPLE ON DELETE CASCADE " + attributes
+ (prefix.startsWith("CREATE") ? ")" : ", ADD COLUMN extra INT"));
ForeignKeyIndex fk = (ForeignKeyIndex) (statement instanceof CreateTable
? ((CreateTable) statement).getIndexes().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
assertEquals(!attributes.contains("NOT ENFORCED"),
fk.getConstraintAttributes().getEnforced());
roundTrip(statement);
fk.getConstraintAttributes().setEnforced(true);
roundTrip(statement);
assertFalse(statement.toString().contains("NOT ENFORCED"));
fk.getConstraintAttributes().setEnforced(null);
roundTrip(statement);
assertFalse(statement.toString().contains("ENFORCED"));
}
}

@ParameterizedTest
@ValueSource(strings = {"ENFORCED", "NOT ENFORCED DEFERRABLE INITIALLY DEFERRED"})
void columnReferencesRetainTheirOwnAttributesAndFollowingOptions(String attributes)
throws JSQLParserException {
for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) {
Statement statement = parse(prefix + "id INT REFERENCES public.p (id) " + attributes
+ " NOT NULL"
+ (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT"));
ColumnDefinition column = statement instanceof CreateTable
? ((CreateTable) statement).getColumnDefinitions().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0);
ForeignKeyReference reference =
column.getColumnOptions().get(0).getForeignKeyReference();
assertNotNull(reference.getConstraintAttributes());
assertEquals(!attributes.contains("NOT ENFORCED"),
reference.getConstraintAttributes().getEnforced());
assertEquals(ColumnOption.Kind.NULLABILITY, column.getColumnOptions().get(1).getKind());
roundTrip(statement);
reference.getConstraintAttributes().setEnforced(null);
roundTrip(statement);
}
}

@Test
void checkLegacyAccessorsUseTheSharedAttributeState() throws JSQLParserException {
for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) {
Statement statement = parse(prefix + "CHECK (id > 0) NOT ENFORCED"
+ (prefix.startsWith("CREATE") ? ")" : " NOT VALID"));
CheckConstraint check = (CheckConstraint) (statement instanceof CreateTable
? ((CreateTable) statement).getIndexes().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
assertEquals(false, check.getConstraintAttributes().getEnforced());
check.getConstraintAttributes().setEnforced(true);
assertEquals(true, check.getEnforced());
check.setEnforced(false);
assertEquals(false, check.getConstraintAttributes().getEnforced());
roundTrip(statement);
check.setEnforced(null);
assertNull(check.getEnforced());
roundTrip(statement);
}
assertNull(new CheckConstraint().withEnforced(null).getConstraintAttributes());
}

@Test
void preservesAlterEnforcementAndStatementBoundaries() throws JSQLParserException {
for (String flag : List.of("ENFORCED", "NOT ENFORCED")) {
Alter alter = (Alter) parse("ALTER TABLE t ALTER CONSTRAINT fk " + flag);
assertEquals(!flag.startsWith("NOT"), alter.getAlterExpressions().get(0).isEnforced());
roundTrip(alter);
}
assertEquals(2, CCJSqlParserUtil.parseStatements(
"CREATE TABLE t(id INT REFERENCES p NOT ENFORCED); SELECT 1").size());
}

@ParameterizedTest
@ValueSource(strings = {"FOREIGN KEY(id) REFERENCES p ENFORCED NOT ENFORCED",
"FOREIGN KEY(id) REFERENCES p DEFERRABLE NOT DEFERRABLE",
"FOREIGN KEY(id) REFERENCES p ENFORCED INITIALLY wrong",
"PRIMARY KEY(id) NOT ENFORCED", "CHECK(id > 0) ENFORCED ENFORCED"})
void rejectsMalformedAttributeTails(String constraint) {
assertThrows(JSQLParserException.class,
() -> parse("CREATE TABLE t(id INT, " + constraint + ")"));
}

@ParameterizedTest
@ValueSource(strings = {"CREATE TABLE t(id INT REFERENCES p ENFORCED NOT VALID)",
"ALTER TABLE t ADD COLUMN id INT REFERENCES p NOT VALID"})
void rejectsNotValidOnColumnReferences(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql));
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}

private static void roundTrip(Statement statement) throws JSQLParserException {
StringBuilder out = new StringBuilder();
statement.accept(new StatementDeParser(out));
assertEquals(statement.toString(), out.toString());
assertEquals(out.toString(), parse(out.toString()).toString());
}
}
Loading