Skip to content

Commit e0f5f5f

Browse files
committed
feat: support PostgreSQL referential action column lists
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0f58f7f commit e0f5f5f

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
@@ -15731,21 +15731,32 @@ ReferentialAction.Action Action():
1573115731
* Shared between CREATE TABLE FK and ALTER TABLE FK definitions.
1573215732
*/
1573315733
void ReferentialActions(ForeignKeyReference reference):
15734+
{}
1573415735
{
15735-
Token tk;
15736-
ReferentialAction.Action action = null;
15736+
[ LOOKAHEAD(2) ReferentialActionSpec(reference) ]
15737+
[ LOOKAHEAD(2) ReferentialActionSpec(reference) ]
1573715738
}
15739+
15740+
void ReferentialActionSpec(ForeignKeyReference reference):
1573815741
{
15739-
[ LOOKAHEAD(2) (
15740-
<K_ON>
15741-
( tk=<K_DELETE> | tk=<K_UPDATE> ) action = Action()
15742-
{ reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); }
15743-
)]
15744-
[ LOOKAHEAD(2) (
15745-
<K_ON>
15746-
( tk=<K_DELETE> | tk=<K_UPDATE> ) action = Action()
15747-
{ reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); }
15748-
)]
15742+
Token token;
15743+
ReferentialAction.Type type;
15744+
ReferentialAction.Action action;
15745+
List<String> columnNames = null;
15746+
}
15747+
{
15748+
<K_ON> ( token=<K_DELETE> | token=<K_UPDATE> )
15749+
{ type = ReferentialAction.Type.from(token.image); }
15750+
action=Action()
15751+
[ LOOKAHEAD({ type == ReferentialAction.Type.DELETE
15752+
&& (action == ReferentialAction.Action.SET_NULL
15753+
|| action == ReferentialAction.Action.SET_DEFAULT)
15754+
&& "(".equals(getToken(1).image) })
15755+
columnNames=ColumnsNamesList() ]
15756+
{
15757+
reference.setReferentialAction(type, action);
15758+
reference.getReferentialAction(type).setColumnNames(columnNames);
15759+
}
1574915760
}
1575015761

1575115762
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)