Skip to content

Commit 338c6bb

Browse files
committed
feat: share PostgreSQL foreign key enforcement attributes
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0f58f7f commit 338c6bb

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
@@ -1535,6 +1535,14 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15351535
|| "\"XML\"".equalsIgnoreCase(name);
15361536
}
15371537

1538+
private boolean isPostgreSqlConstraintAttributeAhead() {
1539+
int kind = getToken(1).kind;
1540+
int next = getToken(2).kind;
1541+
return kind == K_DEFERRABLE || kind == K_ENFORCED || isKeywordAhead("INITIALLY")
1542+
|| kind == K_NOT && (next == K_DEFERRABLE || next == K_ENFORCED
1543+
|| "VALID".equalsIgnoreCase(getToken(2).image));
1544+
}
1545+
15381546
private boolean isMySqlStatisticsOptionAhead() {
15391547
String name = getToken(1).image;
15401548
return "STATS_AUTO_RECALC".equalsIgnoreCase(name)
@@ -14157,7 +14165,7 @@ ColumnOption ColumnDefinitionOption(): {
1415714165
tk=<S_IDENTIFIER> <K_DEFAULT> <K_VALUE>
1415814166
{ option = ColumnOption.serialDefaultValue(); }
1415914167
|
14160-
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec()
14168+
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec(true)
1416114169
{ option = ColumnOption.reference(reference); }
1416214170
|
1416314171
LOOKAHEAD(<K_DROP> <K_DEFAULT>) <K_DROP> <K_DEFAULT>
@@ -14794,25 +14802,61 @@ void PostgreSqlConstraintOptions(Index index):
1479414802
}
1479514803

1479614804
void PostgreSqlConstraintAttributes(Index index):
14805+
{ ConstraintAttributes attributes; }
14806+
{
14807+
attributes=PostgreSqlConstraintAttributeList(index.getConstraintAttributes())
14808+
{
14809+
requireDdlSyntax(attributes == null || attributes.getEnforced() == null
14810+
|| index instanceof CheckConstraint || index instanceof ForeignKeyIndex,
14811+
"ENFORCED is supported only for CHECK and foreign key constraints");
14812+
index.setConstraintAttributes(attributes);
14813+
}
14814+
}
14815+
14816+
/** Shared attribute parsing for table constraints and column REFERENCES clauses. */
14817+
ConstraintAttributes PostgreSqlConstraintAttributeList(ConstraintAttributes attributes):
1479714818
{
14798-
ConstraintAttributes attributes = new ConstraintAttributes();
14799-
boolean present = false;
14800-
boolean deferrable = true;
14819+
boolean present = attributes != null;
14820+
boolean negative;
14821+
Boolean enforced;
1480114822
Token token;
1480214823
}
1480314824
{
14804-
[ LOOKAHEAD(2) [ <K_NOT> { deferrable = false; } ] <K_DEFERRABLE> {
14805-
attributes.setDeferrable(deferrable); present = true;
14806-
} ]
14807-
[ LOOKAHEAD({ isKeywordAhead("INITIALLY") }) token=<S_IDENTIFIER> token=<S_IDENTIFIER> {
14808-
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
14809-
"Expected IMMEDIATE or DEFERRED");
14810-
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
14811-
present = true;
14812-
} ]
14813-
[ LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
14814-
<K_NOT> token=<S_IDENTIFIER> { attributes.setNotValid(true); present = true; } ]
14815-
{ if (present) { index.setConstraintAttributes(attributes); } }
14825+
{ if (attributes == null) { attributes = new ConstraintAttributes(); } }
14826+
( LOOKAHEAD({ isPostgreSqlConstraintAttributeAhead() }) (
14827+
LOOKAHEAD(2) { negative = false; }
14828+
[ <K_NOT> { negative = true; } ] <K_DEFERRABLE> {
14829+
requireDdlSyntax(attributes.getDeferrable() == null, "Duplicate DEFERRABLE clause");
14830+
attributes.setDeferrable(!negative); present = true;
14831+
}
14832+
|
14833+
LOOKAHEAD({ isKeywordAhead("INITIALLY") }) <S_IDENTIFIER> token=<S_IDENTIFIER> {
14834+
requireDdlSyntax(attributes.getInitially() == null, "Duplicate INITIALLY clause");
14835+
requireDdlSyntax("IMMEDIATE".equalsIgnoreCase(token.image) || "DEFERRED".equalsIgnoreCase(token.image),
14836+
"Expected IMMEDIATE or DEFERRED");
14837+
attributes.setInitially(ConstraintAttributes.Initially.valueOf(token.image.toUpperCase(Locale.ROOT)));
14838+
present = true;
14839+
}
14840+
|
14841+
LOOKAHEAD(2) enforced=ConstraintEnforcement() {
14842+
requireDdlSyntax(attributes.getEnforced() == null, "Duplicate ENFORCED clause");
14843+
attributes.setEnforced(enforced); present = true;
14844+
}
14845+
|
14846+
LOOKAHEAD({ getToken(1).kind == K_NOT && "VALID".equalsIgnoreCase(getToken(2).image) })
14847+
<K_NOT> <S_IDENTIFIER> {
14848+
requireDdlSyntax(!attributes.isNotValid(), "Duplicate NOT VALID clause");
14849+
attributes.setNotValid(true); present = true;
14850+
}
14851+
) )*
14852+
{ return present ? attributes : null; }
14853+
}
14854+
14855+
Boolean ConstraintEnforcement():
14856+
{ boolean enforced = true; }
14857+
{
14858+
[ <K_NOT> { enforced = false; } ] <K_ENFORCED>
14859+
{ return enforced; }
1481614860
}
1481714861

1481814862
/**
@@ -15748,7 +15792,7 @@ void ReferentialActions(ForeignKeyReference reference):
1574815792
)]
1574915793
}
1575015794

15751-
ForeignKeyReference ForeignKeyReferenceSpec():
15795+
ForeignKeyReference ForeignKeyReferenceSpec(boolean columnContext):
1575215796
{
1575315797
ForeignKeyReference reference = new ForeignKeyReference();
1575415798
ForeignKeyReference.MatchType matchType;
@@ -15775,6 +15819,12 @@ ForeignKeyReference ForeignKeyReferenceSpec():
1577515819
]
1577615820
ReferentialActions(reference)
1577715821
{
15822+
if (columnContext) {
15823+
ConstraintAttributes attributes = PostgreSqlConstraintAttributeList(null);
15824+
requireDdlSyntax(attributes == null || !attributes.isNotValid(),
15825+
"NOT VALID requires a table constraint");
15826+
reference.setConstraintAttributes(attributes);
15827+
}
1577815828
return reference;
1577915829
}
1578015830
}
@@ -15795,10 +15845,7 @@ CheckConstraint CheckConstraintSpec(String constraintName):
1579515845
[ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
1579615846
&& getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
1579715847
<K_NO> TypeDdlKeyword("INHERIT") { noInherit = true; } ]
15798-
[ LOOKAHEAD(2)
15799-
[ <K_NOT> { enforced = false; } ]
15800-
<K_ENFORCED> { if (enforced == null) { enforced = true; } }
15801-
]
15848+
[ LOOKAHEAD(2) enforced=ConstraintEnforcement() ]
1580215849
{
1580315850
checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp)
1580415851
.withEnforced(enforced).withNoInherit(noInherit);
@@ -15828,7 +15875,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName):
1582815875
if (constraintName != null) { fkIndex.setName(constraintName); }
1582915876
fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames);
1583015877
}
15831-
reference=ForeignKeyReferenceSpec() { fkIndex.setReference(reference); }
15878+
reference=ForeignKeyReferenceSpec(false) { fkIndex.setReference(reference); }
1583215879
{
1583315880
return fkIndex;
1583415881
}
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)