Skip to content

Commit 17be330

Browse files
authored
feat: support PostgreSQL referential action column lists (#2691)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 8a11fb0 commit 17be330

3 files changed

Lines changed: 135 additions & 19 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/ReferentialAction.java‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
import java.io.Serializable;
1313
import java.util.Locale;
14+
import java.util.List;
15+
import java.util.Objects;
16+
import net.sf.jsqlparser.statement.select.PlainSelect;
1417

1518
public class ReferentialAction implements Serializable {
1619

1720
private Type type;
1821
private Action action;
22+
private List<String> columnNames;
1923

2024
public ReferentialAction() {
2125
// default constructor
@@ -52,6 +56,20 @@ public ReferentialAction withAction(Action action) {
5256
return this;
5357
}
5458

59+
/** Columns affected by PostgreSQL ON DELETE SET NULL or SET DEFAULT; null means all columns. */
60+
public List<String> getColumnNames() {
61+
return columnNames;
62+
}
63+
64+
public void setColumnNames(List<String> columnNames) {
65+
this.columnNames = columnNames;
66+
}
67+
68+
public ReferentialAction withColumnNames(List<String> columnNames) {
69+
setColumnNames(columnNames);
70+
return this;
71+
}
72+
5573
@Override
5674
public int hashCode() {
5775
final int prime = 31;
@@ -64,7 +82,9 @@ public int hashCode() {
6482
@Override
6583
public String toString() {
6684
return " ON " + getType().name() + " " +
67-
getAction().getAction();
85+
getAction().getAction()
86+
+ (columnNames == null ? ""
87+
: " " + PlainSelect.getStringList(columnNames, true, true));
6888
}
6989

7090
@Override
@@ -79,12 +99,8 @@ public boolean equals(Object obj) {
7999
return false;
80100
}
81101
ReferentialAction other = (ReferentialAction) obj;
82-
// if (action != other.action) {
83-
// return false;
84-
// }
85-
// if (type != other.type) {
86-
// return false;
87-
return action == other.action && type == other.type;
102+
return action == other.action && type == other.type
103+
&& Objects.equals(columnNames, other.columnNames);
88104
}
89105

90106
public enum Type {

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

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15761,21 +15761,32 @@ ReferentialAction.Action Action():
1576115761
* Shared between CREATE TABLE FK and ALTER TABLE FK definitions.
1576215762
*/
1576315763
void ReferentialActions(ForeignKeyReference reference):
15764+
{}
1576415765
{
15765-
Token tk;
15766-
ReferentialAction.Action action = null;
15766+
[ LOOKAHEAD(2) ReferentialActionSpec(reference) ]
15767+
[ LOOKAHEAD(2) ReferentialActionSpec(reference) ]
1576715768
}
15769+
15770+
void ReferentialActionSpec(ForeignKeyReference reference):
1576815771
{
15769-
[ LOOKAHEAD(2) (
15770-
<K_ON>
15771-
( tk=<K_DELETE> | tk=<K_UPDATE> ) action = Action()
15772-
{ reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); }
15773-
)]
15774-
[ LOOKAHEAD(2) (
15775-
<K_ON>
15776-
( tk=<K_DELETE> | tk=<K_UPDATE> ) action = Action()
15777-
{ reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); }
15778-
)]
15772+
Token token;
15773+
ReferentialAction.Type type;
15774+
ReferentialAction.Action action;
15775+
List<String> columnNames = null;
15776+
}
15777+
{
15778+
<K_ON> ( token=<K_DELETE> | token=<K_UPDATE> )
15779+
{ type = ReferentialAction.Type.from(token.image); }
15780+
action=Action()
15781+
[ LOOKAHEAD({ type == ReferentialAction.Type.DELETE
15782+
&& (action == ReferentialAction.Action.SET_NULL
15783+
|| action == ReferentialAction.Action.SET_DEFAULT)
15784+
&& "(".equals(getToken(1).image) })
15785+
columnNames=ColumnsNamesList() ]
15786+
{
15787+
reference.setReferentialAction(type, action);
15788+
reference.getReferentialAction(type).setColumnNames(columnNames);
15789+
}
1577915790
}
1578015791

1578115792
ForeignKeyReference ForeignKeyReferenceSpec():
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2023 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.statement.Statement;
17+
import net.sf.jsqlparser.statement.ReferentialAction;
18+
import net.sf.jsqlparser.statement.alter.Alter;
19+
import net.sf.jsqlparser.statement.create.table.CreateTable;
20+
import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex;
21+
import net.sf.jsqlparser.statement.create.table.ForeignKeyReference;
22+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class PostgreSqlForeignKeyActionColumnsTest {
28+
@ParameterizedTest
29+
@ValueSource(strings = {"NULL", "DEFAULT"})
30+
void sharesActionsAcrossCreateAndAlter(String value) throws JSQLParserException {
31+
for (String columns : new String[] {"author_id", "tenant_id, author_id", "\"Author Id\""}) {
32+
for (boolean updateFirst : new boolean[] {false, true}) {
33+
String delete = "ON DELETE SET " + value + " (" + columns + ")";
34+
String actions = updateFirst ? "ON UPDATE CASCADE " + delete
35+
: delete + " ON UPDATE CASCADE";
36+
for (String prefix : new String[] {
37+
"CREATE TABLE posts (tenant_id INT, author_id INT, ",
38+
"ALTER TABLE posts ADD "}) {
39+
String sql = prefix + "CONSTRAINT fk FOREIGN KEY (tenant_id, author_id) "
40+
+ "REFERENCES users (tenant_id, id) MATCH SIMPLE " + actions
41+
+ (prefix.startsWith("CREATE") ? ")" : "");
42+
Statement statement = CCJSqlParserUtil.parse(sql);
43+
ForeignKeyIndex index = (ForeignKeyIndex) (statement instanceof CreateTable
44+
? ((CreateTable) statement).getIndexes().get(0)
45+
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
46+
ReferentialAction action =
47+
index.getReferentialAction(ReferentialAction.Type.DELETE);
48+
assertEquals(List.of(columns.split(", ")), action.getColumnNames());
49+
assertNull(index.getReferentialAction(ReferentialAction.Type.UPDATE)
50+
.getColumnNames());
51+
roundTrip(statement);
52+
action.setColumnNames(List.of("author_id"));
53+
roundTrip(statement);
54+
assertTrue(statement.toString().contains("SET " + value + " (author_id)"));
55+
action.setColumnNames(null);
56+
roundTrip(statement);
57+
}
58+
}
59+
}
60+
}
61+
62+
@Test
63+
void supportsColumnReferencesAndStatementBoundaries() throws JSQLParserException {
64+
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
65+
"CREATE TABLE posts (author_id INT REFERENCES users ON DELETE SET NULL (author_id) NOT NULL)");
66+
ForeignKeyReference reference = table.getColumnDefinitions().get(0)
67+
.getColumnOptions().get(0).getForeignKeyReference();
68+
assertEquals(List.of("author_id"),
69+
reference.getReferentialAction(ReferentialAction.Type.DELETE).getColumnNames());
70+
roundTrip(table);
71+
assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1").size());
72+
}
73+
74+
@ParameterizedTest
75+
@ValueSource(strings = {"ON UPDATE SET NULL (a)", "ON UPDATE SET DEFAULT (a)",
76+
"ON DELETE CASCADE (a)", "ON DELETE SET NULL ()", "ON DELETE SET NULL (a,)",
77+
"ON DELETE SET NULL (a + 1)"})
78+
void rejectsInvalidActionColumns(String action) {
79+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
80+
"CREATE TABLE t (a INT, FOREIGN KEY (a) REFERENCES p (a) " + action + ")"));
81+
}
82+
83+
private static void roundTrip(Statement statement) throws JSQLParserException {
84+
StringBuilder out = new StringBuilder();
85+
statement.accept(new StatementDeParser(out));
86+
assertEquals(statement.toString(), out.toString());
87+
assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString());
88+
}
89+
}

0 commit comments

Comments
 (0)