diff --git a/src/main/java/net/sf/jsqlparser/statement/ReferentialAction.java b/src/main/java/net/sf/jsqlparser/statement/ReferentialAction.java index 6e441ee1c..80fba45a9 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ReferentialAction.java +++ b/src/main/java/net/sf/jsqlparser/statement/ReferentialAction.java @@ -11,11 +11,15 @@ import java.io.Serializable; import java.util.Locale; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.statement.select.PlainSelect; public class ReferentialAction implements Serializable { private Type type; private Action action; + private List columnNames; public ReferentialAction() { // default constructor @@ -52,6 +56,20 @@ public ReferentialAction withAction(Action action) { return this; } + /** Columns affected by PostgreSQL ON DELETE SET NULL or SET DEFAULT; null means all columns. */ + public List getColumnNames() { + return columnNames; + } + + public void setColumnNames(List columnNames) { + this.columnNames = columnNames; + } + + public ReferentialAction withColumnNames(List columnNames) { + setColumnNames(columnNames); + return this; + } + @Override public int hashCode() { final int prime = 31; @@ -64,7 +82,9 @@ public int hashCode() { @Override public String toString() { return " ON " + getType().name() + " " + - getAction().getAction(); + getAction().getAction() + + (columnNames == null ? "" + : " " + PlainSelect.getStringList(columnNames, true, true)); } @Override @@ -79,12 +99,8 @@ public boolean equals(Object obj) { return false; } ReferentialAction other = (ReferentialAction) obj; - // if (action != other.action) { - // return false; - // } - // if (type != other.type) { - // return false; - return action == other.action && type == other.type; + return action == other.action && type == other.type + && Objects.equals(columnNames, other.columnNames); } public enum Type { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0df99f424..b828cb063 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -15731,21 +15731,32 @@ ReferentialAction.Action Action(): * Shared between CREATE TABLE FK and ALTER TABLE FK definitions. */ void ReferentialActions(ForeignKeyReference reference): +{} { - Token tk; - ReferentialAction.Action action = null; + [ LOOKAHEAD(2) ReferentialActionSpec(reference) ] + [ LOOKAHEAD(2) ReferentialActionSpec(reference) ] } + +void ReferentialActionSpec(ForeignKeyReference reference): { - [ LOOKAHEAD(2) ( - - ( tk= | tk= ) action = Action() - { reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } - )] - [ LOOKAHEAD(2) ( - - ( tk= | tk= ) action = Action() - { reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } - )] + Token token; + ReferentialAction.Type type; + ReferentialAction.Action action; + List columnNames = null; +} +{ + ( token= | token= ) + { type = ReferentialAction.Type.from(token.image); } + action=Action() + [ LOOKAHEAD({ type == ReferentialAction.Type.DELETE + && (action == ReferentialAction.Action.SET_NULL + || action == ReferentialAction.Action.SET_DEFAULT) + && "(".equals(getToken(1).image) }) + columnNames=ColumnsNamesList() ] + { + reference.setReferentialAction(type, action); + reference.getReferentialAction(type).setColumnNames(columnNames); + } } ForeignKeyReference ForeignKeyReferenceSpec(): diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java new file mode 100644 index 000000000..7f4724785 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignKeyActionColumnsTest.java @@ -0,0 +1,89 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2023 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.statement.Statement; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +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 PostgreSqlForeignKeyActionColumnsTest { + @ParameterizedTest + @ValueSource(strings = {"NULL", "DEFAULT"}) + void sharesActionsAcrossCreateAndAlter(String value) throws JSQLParserException { + for (String columns : new String[] {"author_id", "tenant_id, author_id", "\"Author Id\""}) { + for (boolean updateFirst : new boolean[] {false, true}) { + String delete = "ON DELETE SET " + value + " (" + columns + ")"; + String actions = updateFirst ? "ON UPDATE CASCADE " + delete + : delete + " ON UPDATE CASCADE"; + for (String prefix : new String[] { + "CREATE TABLE posts (tenant_id INT, author_id INT, ", + "ALTER TABLE posts ADD "}) { + String sql = prefix + "CONSTRAINT fk FOREIGN KEY (tenant_id, author_id) " + + "REFERENCES users (tenant_id, id) MATCH SIMPLE " + actions + + (prefix.startsWith("CREATE") ? ")" : ""); + Statement statement = CCJSqlParserUtil.parse(sql); + ForeignKeyIndex index = (ForeignKeyIndex) (statement instanceof CreateTable + ? ((CreateTable) statement).getIndexes().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getIndex()); + ReferentialAction action = + index.getReferentialAction(ReferentialAction.Type.DELETE); + assertEquals(List.of(columns.split(", ")), action.getColumnNames()); + assertNull(index.getReferentialAction(ReferentialAction.Type.UPDATE) + .getColumnNames()); + roundTrip(statement); + action.setColumnNames(List.of("author_id")); + roundTrip(statement); + assertTrue(statement.toString().contains("SET " + value + " (author_id)")); + action.setColumnNames(null); + roundTrip(statement); + } + } + } + } + + @Test + void supportsColumnReferencesAndStatementBoundaries() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE posts (author_id INT REFERENCES users ON DELETE SET NULL (author_id) NOT NULL)"); + ForeignKeyReference reference = table.getColumnDefinitions().get(0) + .getColumnOptions().get(0).getForeignKeyReference(); + assertEquals(List.of("author_id"), + reference.getReferentialAction(ReferentialAction.Type.DELETE).getColumnNames()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"ON UPDATE SET NULL (a)", "ON UPDATE SET DEFAULT (a)", + "ON DELETE CASCADE (a)", "ON DELETE SET NULL ()", "ON DELETE SET NULL (a,)", + "ON DELETE SET NULL (a + 1)"}) + void rejectsInvalidActionColumns(String action) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT, FOREIGN KEY (a) REFERENCES p (a) " + action + ")")); + } + + 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(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +}