Skip to content

Commit 63d65ed

Browse files
authored
feat: share PostgreSQL foreign key enforcement attributes (#2695)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent a1b82fc commit 63d65ed

5 files changed

Lines changed: 242 additions & 29 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ public class CheckConstraint extends NamedConstraint {
2222

2323
private Expression expression;
2424

25-
private Boolean enforced;
26-
2725
private boolean noInherit;
2826

2927
public CheckConstraint() {
@@ -61,11 +59,19 @@ public CheckConstraint withNoInherit(boolean noInherit) {
6159
}
6260

6361
public Boolean getEnforced() {
64-
return enforced;
62+
return getConstraintAttributes() == null ? null : getConstraintAttributes().getEnforced();
6563
}
6664

6765
public void setEnforced(Boolean enforced) {
68-
this.enforced = enforced;
66+
ConstraintAttributes attributes = getConstraintAttributes();
67+
if (attributes == null) {
68+
if (enforced == null) {
69+
return;
70+
}
71+
attributes = new ConstraintAttributes();
72+
setConstraintAttributes(attributes);
73+
}
74+
attributes.setEnforced(enforced);
6975
}
7076

7177
@Override
@@ -81,9 +87,6 @@ public void appendTo(StringBuilder b, Consumer<Expression> expressionPrinter) {
8187
if (noInherit) {
8288
b.append(" NO INHERIT");
8389
}
84-
if (enforced != null) {
85-
b.append(enforced ? " ENFORCED" : " NOT ENFORCED");
86-
}
8790
appendConstraintSuffixTo(b);
8891
appendConstraintAttributesTo(b);
8992
}

‎src/main/java/net/sf/jsqlparser/statement/create/table/ConstraintAttributes.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum Initially {
2020
private Boolean deferrable;
2121
private Initially initially;
2222
private boolean notValid;
23+
private Boolean enforced;
2324

2425
public Boolean getDeferrable() {
2526
return deferrable;
@@ -45,13 +46,25 @@ public void setNotValid(boolean notValid) {
4546
this.notValid = notValid;
4647
}
4748

49+
/** Null preserves an omitted ENFORCED clause. */
50+
public Boolean getEnforced() {
51+
return enforced;
52+
}
53+
54+
public void setEnforced(Boolean enforced) {
55+
this.enforced = enforced;
56+
}
57+
4858
public void appendTo(StringBuilder sql) {
4959
if (deferrable != null) {
5060
sql.append(deferrable ? " DEFERRABLE" : " NOT DEFERRABLE");
5161
}
5262
if (initially != null) {
5363
sql.append(" INITIALLY ").append(initially);
5464
}
65+
if (enforced != null) {
66+
sql.append(enforced ? " ENFORCED" : " NOT ENFORCED");
67+
}
5568
if (notValid) {
5669
sql.append(" NOT VALID");
5770
}

‎src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,20 @@ public enum MatchType {
3333
private Table table;
3434
private List<String> referencedColumnNames;
3535
private MatchType matchType;
36+
private ConstraintAttributes constraintAttributes;
3637
private final Set<ReferentialAction> referentialActions = new LinkedHashSet<>(2);
3738

39+
/**
40+
* Attributes of a column REFERENCES clause; table constraints own their attributes on Index.
41+
*/
42+
public ConstraintAttributes getConstraintAttributes() {
43+
return constraintAttributes;
44+
}
45+
46+
public void setConstraintAttributes(ConstraintAttributes constraintAttributes) {
47+
this.constraintAttributes = constraintAttributes;
48+
}
49+
3850
public Table getTable() {
3951
return table;
4052
}
@@ -130,6 +142,9 @@ public String toString() {
130142
builder.append(" MATCH ").append(matchType);
131143
}
132144
referentialActions.forEach(builder::append);
145+
if (constraintAttributes != null) {
146+
constraintAttributes.appendTo(builder);
147+
}
133148
return builder.toString();
134149
}
135150
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,14 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15401540
|| "\"XML\"".equalsIgnoreCase(name);
15411541
}
15421542

1543+
private boolean isPostgreSqlConstraintAttributeAhead() {
1544+
int kind = getToken(1).kind;
1545+
int next = getToken(2).kind;
1546+
return kind == K_DEFERRABLE || kind == K_ENFORCED || isKeywordAhead("INITIALLY")
1547+
|| kind == K_NOT && (next == K_DEFERRABLE || next == K_ENFORCED
1548+
|| "VALID".equalsIgnoreCase(getToken(2).image));
1549+
}
1550+
15431551
private boolean isMySqlStatisticsOptionAhead() {
15441552
String name = getToken(1).image;
15451553
return "STATS_AUTO_RECALC".equalsIgnoreCase(name)
@@ -14167,7 +14175,7 @@ ColumnOption ColumnDefinitionOption(): {
1416714175
tk=<S_IDENTIFIER> <K_DEFAULT> <K_VALUE>
1416814176
{ option = ColumnOption.serialDefaultValue(); }
1416914177
|
14170-
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec()
14178+
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec(true)
1417114179
{ option = ColumnOption.reference(reference); }
1417214180
|
1417314181
LOOKAHEAD(<K_DROP> <K_DEFAULT>) <K_DROP> <K_DEFAULT>
@@ -14816,25 +14824,61 @@ void PostgreSqlConstraintOptions(Index index):
1481614824
}
1481714825

1481814826
void PostgreSqlConstraintAttributes(Index index):
14827+
{ ConstraintAttributes attributes; }
14828+
{
14829+
attributes=PostgreSqlConstraintAttributeList(index.getConstraintAttributes())
14830+
{
14831+
requireDdlSyntax(attributes == null || attributes.getEnforced() == null
14832+
|| index instanceof CheckConstraint || index instanceof ForeignKeyIndex,
14833+
"ENFORCED is supported only for CHECK and foreign key constraints");
14834+
index.setConstraintAttributes(attributes);
14835+
}
14836+
}
14837+
14838+
/** Shared attribute parsing for table constraints and column REFERENCES clauses. */
14839+
ConstraintAttributes PostgreSqlConstraintAttributeList(ConstraintAttributes attributes):
1481914840
{
14820-
ConstraintAttributes attributes = new ConstraintAttributes();
14821-
boolean present = false;
14822-
boolean deferrable = true;
14841+
boolean present = attributes != null;
14842+
boolean negative;
14843+
Boolean enforced;
1482314844
Token token;
1482414845
}
1482514846
{
14826-
[ LOOKAHEAD(2) [ <K_NOT> { deferrable = false; } ] <K_DEFERRABLE> {
14827-
attributes.setDeferrable(deferrable); present = true;
14828-
} ]
14829-
[ LOOKAHEAD({ isKeywordAhead("INITIALLY") }) token=<S_IDENTIFIER> token=<S_IDENTIFIER> {
14830-
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
14831-
"Expected IMMEDIATE or DEFERRED");
14832-
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
14833-
present = true;
14834-
} ]
14835-
[ LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
14836-
<K_NOT> token=<S_IDENTIFIER> { attributes.setNotValid(true); present = true; } ]
14837-
{ if (present) { index.setConstraintAttributes(attributes); } }
14847+
{ if (attributes == null) { attributes = new ConstraintAttributes(); } }
14848+
( LOOKAHEAD({ isPostgreSqlConstraintAttributeAhead() }) (
14849+
LOOKAHEAD(2) { negative = false; }
14850+
[ <K_NOT> { negative = true; } ] <K_DEFERRABLE> {
14851+
requireDdlSyntax(attributes.getDeferrable() == null, "Duplicate DEFERRABLE clause");
14852+
attributes.setDeferrable(!negative); present = true;
14853+
}
14854+
|
14855+
LOOKAHEAD({ isKeywordAhead("INITIALLY") }) <S_IDENTIFIER> token=<S_IDENTIFIER> {
14856+
requireDdlSyntax(attributes.getInitially() == null, "Duplicate INITIALLY clause");
14857+
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
14858+
"Expected IMMEDIATE or DEFERRED");
14859+
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
14860+
present = true;
14861+
}
14862+
|
14863+
LOOKAHEAD(2) enforced=ConstraintEnforcement() {
14864+
requireDdlSyntax(attributes.getEnforced() == null, "Duplicate ENFORCED clause");
14865+
attributes.setEnforced(enforced); present = true;
14866+
}
14867+
|
14868+
LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
14869+
<K_NOT> <S_IDENTIFIER> {
14870+
requireDdlSyntax(!attributes.isNotValid(), "Duplicate NOT VALID clause");
14871+
attributes.setNotValid(true); present = true;
14872+
}
14873+
) )*
14874+
{ return present ? attributes : null; }
14875+
}
14876+
14877+
Boolean ConstraintEnforcement():
14878+
{ boolean enforced = true; }
14879+
{
14880+
[ <K_NOT> { enforced = false; } ] <K_ENFORCED>
14881+
{ return enforced; }
1483814882
}
1483914883

1484014884
/**
@@ -15826,7 +15870,7 @@ void ReferentialActionSpec(ForeignKeyReference reference):
1582615870
}
1582715871
}
1582815872

15829-
ForeignKeyReference ForeignKeyReferenceSpec():
15873+
ForeignKeyReference ForeignKeyReferenceSpec(boolean columnContext):
1583015874
{
1583115875
ForeignKeyReference reference = new ForeignKeyReference();
1583215876
ForeignKeyReference.MatchType matchType;
@@ -15853,6 +15897,12 @@ ForeignKeyReference ForeignKeyReferenceSpec():
1585315897
]
1585415898
ReferentialActions(reference)
1585515899
{
15900+
if (columnContext) {
15901+
ConstraintAttributes attributes = PostgreSqlConstraintAttributeList(null);
15902+
requireDdlSyntax(attributes == null || !attributes.isNotValid(),
15903+
"NOT VALID requires a table constraint");
15904+
reference.setConstraintAttributes(attributes);
15905+
}
1585615906
return reference;
1585715907
}
1585815908
}
@@ -15873,10 +15923,7 @@ CheckConstraint CheckConstraintSpec(String constraintName):
1587315923
[ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
1587415924
&& getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
1587515925
<K_NO> TypeDdlKeyword("INHERIT") { noInherit = true; } ]
15876-
[ LOOKAHEAD(2)
15877-
[ <K_NOT> { enforced = false; } ]
15878-
<K_ENFORCED> { if (enforced == null) { enforced = true; } }
15879-
]
15926+
[ LOOKAHEAD(2) enforced=ConstraintEnforcement() ]
1588015927
{
1588115928
checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp)
1588215929
.withEnforced(enforced).withNoInherit(noInherit);
@@ -15906,7 +15953,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName):
1590615953
if (constraintName != null) { fkIndex.setName(constraintName); }
1590715954
fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames);
1590815955
}
15909-
reference=ForeignKeyReferenceSpec() { fkIndex.setReference(reference); }
15956+
reference=ForeignKeyReferenceSpec(false) { fkIndex.setReference(reference); }
1591015957
{
1591115958
return fkIndex;
1591215959
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.List;
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
17+
import net.sf.jsqlparser.statement.Statement;
18+
import net.sf.jsqlparser.statement.alter.Alter;
19+
import net.sf.jsqlparser.statement.create.table.*;
20+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.ValueSource;
24+
25+
class PostgreSqlConstraintEnforcementTest {
26+
@ParameterizedTest
27+
@ValueSource(strings = {"ENFORCED", "NOT ENFORCED",
28+
"DEFERRABLE INITIALLY DEFERRED NOT ENFORCED",
29+
"NOT ENFORCED DEFERRABLE INITIALLY DEFERRED",
30+
"INITIALLY IMMEDIATE NOT DEFERRABLE ENFORCED"})
31+
void sharesForeignKeyAttributesAcrossCreateAndAlter(String attributes)
32+
throws JSQLParserException {
33+
for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) {
34+
Statement statement =
35+
parse(prefix + "CONSTRAINT fk FOREIGN KEY (id) REFERENCES public.p (id) "
36+
+ "MATCH SIMPLE ON DELETE CASCADE " + attributes
37+
+ (prefix.startsWith("CREATE") ? ")" : ", ADD COLUMN extra INT"));
38+
ForeignKeyIndex fk = (ForeignKeyIndex) (statement instanceof CreateTable
39+
? ((CreateTable) statement).getIndexes().get(0)
40+
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
41+
assertEquals(!attributes.contains("NOT ENFORCED"),
42+
fk.getConstraintAttributes().getEnforced());
43+
roundTrip(statement);
44+
fk.getConstraintAttributes().setEnforced(true);
45+
roundTrip(statement);
46+
assertFalse(statement.toString().contains("NOT ENFORCED"));
47+
fk.getConstraintAttributes().setEnforced(null);
48+
roundTrip(statement);
49+
assertFalse(statement.toString().contains("ENFORCED"));
50+
}
51+
}
52+
53+
@ParameterizedTest
54+
@ValueSource(strings = {"ENFORCED", "NOT ENFORCED DEFERRABLE INITIALLY DEFERRED"})
55+
void columnReferencesRetainTheirOwnAttributesAndFollowingOptions(String attributes)
56+
throws JSQLParserException {
57+
for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) {
58+
Statement statement = parse(prefix + "id INT REFERENCES public.p (id) " + attributes
59+
+ " NOT NULL"
60+
+ (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT"));
61+
ColumnDefinition column = statement instanceof CreateTable
62+
? ((CreateTable) statement).getColumnDefinitions().get(0)
63+
: ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0);
64+
ForeignKeyReference reference =
65+
column.getColumnOptions().get(0).getForeignKeyReference();
66+
assertNotNull(reference.getConstraintAttributes());
67+
assertEquals(!attributes.contains("NOT ENFORCED"),
68+
reference.getConstraintAttributes().getEnforced());
69+
assertEquals(ColumnOption.Kind.NULLABILITY, column.getColumnOptions().get(1).getKind());
70+
roundTrip(statement);
71+
reference.getConstraintAttributes().setEnforced(null);
72+
roundTrip(statement);
73+
}
74+
}
75+
76+
@Test
77+
void checkLegacyAccessorsUseTheSharedAttributeState() throws JSQLParserException {
78+
for (String prefix : List.of("CREATE TABLE t (id INT, ", "ALTER TABLE t ADD ")) {
79+
Statement statement = parse(prefix + "CHECK (id > 0) NOT ENFORCED"
80+
+ (prefix.startsWith("CREATE") ? ")" : " NOT VALID"));
81+
CheckConstraint check = (CheckConstraint) (statement instanceof CreateTable
82+
? ((CreateTable) statement).getIndexes().get(0)
83+
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
84+
assertEquals(false, check.getConstraintAttributes().getEnforced());
85+
check.getConstraintAttributes().setEnforced(true);
86+
assertEquals(true, check.getEnforced());
87+
check.setEnforced(false);
88+
assertEquals(false, check.getConstraintAttributes().getEnforced());
89+
roundTrip(statement);
90+
check.setEnforced(null);
91+
assertNull(check.getEnforced());
92+
roundTrip(statement);
93+
}
94+
assertNull(new CheckConstraint().withEnforced(null).getConstraintAttributes());
95+
}
96+
97+
@Test
98+
void preservesAlterEnforcementAndStatementBoundaries() throws JSQLParserException {
99+
for (String flag : List.of("ENFORCED", "NOT ENFORCED")) {
100+
Alter alter = (Alter) parse("ALTER TABLE t ALTER CONSTRAINT fk " + flag);
101+
assertEquals(!flag.startsWith("NOT"), alter.getAlterExpressions().get(0).isEnforced());
102+
roundTrip(alter);
103+
}
104+
assertEquals(2, CCJSqlParserUtil.parseStatements(
105+
"CREATE TABLE t(id INT REFERENCES p NOT ENFORCED); SELECT 1").size());
106+
}
107+
108+
@ParameterizedTest
109+
@ValueSource(strings = {"FOREIGN KEY(id) REFERENCES p ENFORCED NOT ENFORCED",
110+
"FOREIGN KEY(id) REFERENCES p DEFERRABLE NOT DEFERRABLE",
111+
"FOREIGN KEY(id) REFERENCES p ENFORCED INITIALLY wrong",
112+
"PRIMARY KEY(id) NOT ENFORCED", "CHECK(id > 0) ENFORCED ENFORCED"})
113+
void rejectsMalformedAttributeTails(String constraint) {
114+
assertThrows(JSQLParserException.class,
115+
() -> parse("CREATE TABLE t(id INT, " + constraint + ")"));
116+
}
117+
118+
@ParameterizedTest
119+
@ValueSource(strings = {"CREATE TABLE t(id INT REFERENCES p ENFORCED NOT VALID)",
120+
"ALTER TABLE t ADD COLUMN id INT REFERENCES p NOT VALID"})
121+
void rejectsNotValidOnColumnReferences(String sql) {
122+
assertThrows(JSQLParserException.class, () -> parse(sql));
123+
}
124+
125+
private static Statement parse(String sql) throws JSQLParserException {
126+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
127+
}
128+
129+
private static void roundTrip(Statement statement) throws JSQLParserException {
130+
StringBuilder out = new StringBuilder();
131+
statement.accept(new StatementDeParser(out));
132+
assertEquals(statement.toString(), out.toString());
133+
assertEquals(out.toString(), parse(out.toString()).toString());
134+
}
135+
}

0 commit comments

Comments
 (0)