Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,83 +13,67 @@
import net.sf.jsqlparser.statement.ReferentialAction;
import net.sf.jsqlparser.statement.ReferentialAction.Action;
import net.sf.jsqlparser.statement.ReferentialAction.Type;
import net.sf.jsqlparser.statement.select.PlainSelect;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.function.Consumer;
import net.sf.jsqlparser.expression.Expression;
import java.util.Optional;
import java.util.Set;

public class ForeignKeyIndex extends NamedConstraint {

private Table table;
private List<String> referencedColumnNames;
private Set<ReferentialAction> referentialActions = new LinkedHashSet<>(2);
private ForeignKeyReference reference;
private ForeignKeyReference reference = new ForeignKeyReference();

/** Returns the mutable reference shared by the structured and legacy accessors. */
public ForeignKeyReference getReference() {
if (reference == null) {
reference = new ForeignKeyReference();
reference.setTable(table);
reference.setReferencedColumnNames(referencedColumnNames);
for (ReferentialAction action : referentialActions) {
reference.setReferentialAction(action.getType(), action.getAction());
}
}
return reference;
}

/**
* Replaces the reference. A null value detaches its current table, referenced columns and
* referential actions from reference-only options such as MATCH, preserving the legacy setter
* contract without restoring stale values from before structured edits.
*/
public void setReference(ForeignKeyReference reference) {
this.reference = reference;
if (reference != null) {
table = reference.getTable();
referencedColumnNames = reference.getReferencedColumnNames();
referentialActions.clear();
referentialActions.addAll(reference.getReferentialActions());
if (reference == null) {
ForeignKeyReference detached = new ForeignKeyReference()
.withTable(this.reference.getTable())
.withReferencedColumnNames(this.reference.getReferencedColumnNames());
detached.getReferentialActions().addAll(this.reference.getReferentialActions());
this.reference = detached;
} else {
this.reference = reference;
}
}

public ForeignKeyReference.MatchType getMatchType() {
return reference != null ? reference.getMatchType() : null;
return reference.getMatchType();
}

public void setMatchType(ForeignKeyReference.MatchType matchType) {
getReference().setMatchType(matchType);
}

public Table getTable() {
return reference != null ? reference.getTable() : table;
return reference.getTable();
}

public void setTable(Table table) {
this.table = table;
if (reference != null) {
reference.setTable(table);
}
reference.setTable(table);
}

public List<String> getReferencedColumnNames() {
return reference != null ? reference.getReferencedColumnNames() : referencedColumnNames;
return reference.getReferencedColumnNames();
}

public void setReferencedColumnNames(List<String> referencedColumnNames) {
this.referencedColumnNames = referencedColumnNames;
if (reference != null) {
reference.setReferencedColumnNames(referencedColumnNames);
}
reference.setReferencedColumnNames(referencedColumnNames);
}

/**
* @param type
* @param action
*/
public void setReferentialAction(Type type, Action action) {
setReferentialAction(type, action, true);
reference.setReferentialAction(type, action);
}

public ForeignKeyIndex withReferentialAction(Type type, Action action) {
Expand All @@ -101,39 +85,15 @@ public ForeignKeyIndex withReferentialAction(Type type, Action action) {
* @param type
*/
public void removeReferentialAction(Type type) {
setReferentialAction(type, null, false);
reference.removeReferentialAction(type);
}

/**
* @param type
* @return
*/
public ReferentialAction getReferentialAction(Type type) {
if (reference != null) {
return reference.getReferentialAction(type);
}
return referentialActions.stream().filter(ra -> type.equals(ra.getType())).findFirst()
.orElse(null);
}

private void setReferentialAction(Type type, Action action, boolean set) {
ReferentialAction found = getReferentialAction(type);
if (set) {
if (reference != null) {
reference.setReferentialAction(type, action);
return;
}
if (found == null) {
referentialActions.add(new ReferentialAction(type, action));
} else {
found.setAction(action);
}
} else if (found != null) {
referentialActions.remove(found);
if (reference != null) {
reference.removeReferentialAction(type);
}
}
return reference.getReferentialAction(type);
}

@Deprecated
Expand Down Expand Up @@ -169,14 +129,7 @@ public void setOnUpdateReferenceOption(String onUpdateReferenceOption) {
@Override
public void appendTo(StringBuilder b, Consumer<Expression> expressionPrinter) {
super.appendTo(b, expressionPrinter);
b.append(' ');
if (reference != null) {
b.append(reference);
} else {
b.append("REFERENCES ").append(table)
.append(PlainSelect.getStringList(getReferencedColumnNames(), true, true));
referentialActions.forEach(b::append);
}
b.append(' ').append(reference);
appendConstraintSuffixTo(b);
appendConstraintAttributesTo(b);
}
Expand Down Expand Up @@ -212,17 +165,13 @@ public ForeignKeyIndex withOnUpdateReferenceOption(String onUpdateReferenceOptio
}

public ForeignKeyIndex addReferencedColumnNames(String... referencedColumnNames) {
List<String> collection =
Optional.ofNullable(getReferencedColumnNames()).orElseGet(ArrayList::new);
Collections.addAll(collection, referencedColumnNames);
return this.withReferencedColumnNames(collection);
reference.addReferencedColumnNames(referencedColumnNames);
return this;
}

public ForeignKeyIndex addReferencedColumnNames(Collection<String> referencedColumnNames) {
List<String> collection =
Optional.ofNullable(getReferencedColumnNames()).orElseGet(ArrayList::new);
collection.addAll(referencedColumnNames);
return this.withReferencedColumnNames(collection);
reference.addReferencedColumnNames(referencedColumnNames);
return this;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,8 @@ public void setReferentialAction(Type type, Action action) {
}

public void removeReferentialAction(Type type) {
ReferentialAction current = getReferentialAction(type);
if (current != null) {
referentialActions.remove(current);
}
// Actions are mutable; their hash code can change after insertion into the set.
referentialActions.removeIf(action -> type.equals(action.getType()));
}

public ForeignKeyReference withTable(Table table) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;

import java.util.ArrayList;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.ReferentialAction;
import net.sf.jsqlparser.statement.ReferentialAction.Action;
import net.sf.jsqlparser.statement.ReferentialAction.Type;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.create.table.ConstraintAttributes;
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 ForeignKeyReferenceMutationTest {
@Test
void keepsLegacyAndStructuredAccessOnTheSameValues() {
ForeignKeyIndex index = new ForeignKeyIndex();
Table table = new Table("parent");
List<String> columns = new ArrayList<>(List.of("id"));
index.setTable(table);
index.setReferencedColumnNames(columns);
index.setReferentialAction(Type.DELETE, Action.CASCADE);

ForeignKeyReference reference = index.getReference();
assertSame(table, reference.getTable());
assertSame(columns, reference.getReferencedColumnNames());
assertEquals(Action.CASCADE, reference.getReferentialAction(Type.DELETE).getAction());

Table replacement = new Table("other_parent");
reference.setTable(replacement);
reference.setReferencedColumnNames(new ArrayList<>(List.of("other_id")));
reference.setReferentialAction(Type.DELETE, Action.RESTRICT);
assertSame(replacement, index.getTable());
assertSame(reference.getReferencedColumnNames(), index.getReferencedColumnNames());
assertEquals(Action.RESTRICT, index.getReferentialAction(Type.DELETE).getAction());

index.addReferencedColumnNames("tenant_id");
index.getReferencedColumnNames().remove("other_id");
assertEquals(List.of("tenant_id"), reference.getReferencedColumnNames());
index.removeReferentialAction(Type.DELETE);
assertNull(reference.getReferentialAction(Type.DELETE));
reference.getReferentialActions().add(new ReferentialAction(Type.UPDATE, Action.CASCADE));
assertSame(reference.getReferentialAction(Type.UPDATE),
index.getReferentialAction(Type.UPDATE));
}

@Test
void preservesNullableLegacyFieldsAndAnEmptyReference() {
ForeignKeyIndex index = new ForeignKeyIndex();
index.setReference(null);
assertNull(index.getTable());
assertNull(index.getReferencedColumnNames());
assertNull(index.getReferentialAction(Type.DELETE));
assertNull(index.getMatchType());
index.setTable(new Table("parent"));
index.setReferencedColumnNames(new ArrayList<>(List.of("id")));
index.setTable(null);
index.setReferencedColumnNames(null);
assertNull(index.getReference().getTable());
assertNull(index.getReference().getReferencedColumnNames());
}

@Test
void clearingReferenceRetainsLegacyValuesAndDiscardsReferenceOnlyOptions() {
ForeignKeyReference reference = new ForeignKeyReference()
.withTable(new Table("parent"))
.withReferencedColumnNames(new ArrayList<>(List.of("id")))
.withMatchType(ForeignKeyReference.MatchType.FULL)
.withUsingPeriod(true)
.withReferentialAction(Type.DELETE, Action.CASCADE);
reference.setConstraintName("column_fk");
reference.setConstraintAttributes(new ConstraintAttributes());
ForeignKeyIndex index = new ForeignKeyIndex().withReference(reference);

index.setReference(null);

assertSame(reference.getTable(), index.getTable());
assertSame(reference.getReferencedColumnNames(), index.getReferencedColumnNames());
assertEquals(Action.CASCADE, index.getReferentialAction(Type.DELETE).getAction());
assertNull(index.getMatchType());
ForeignKeyReference detached = index.getReference();
assertNotSame(reference, detached);
assertFalse(detached.isUsingPeriod());
assertNull(detached.getConstraintName());
assertNull(detached.getConstraintAttributes());
}

@Test
void clearingReferenceRetainsTheLatestStructuredValues() {
ForeignKeyReference reference = new ForeignKeyReference()
.withTable(new Table("parent"))
.withReferencedColumnNames(new ArrayList<>(List.of("id")));
ForeignKeyIndex index = new ForeignKeyIndex().withReference(reference);
Table replacement = new Table("other_parent");
List<String> columns = new ArrayList<>(List.of("other_id"));
reference.setTable(replacement);
reference.setReferencedColumnNames(columns);
reference.setReferentialAction(Type.DELETE, Action.SET_NULL);
reference.getReferentialAction(Type.DELETE).setColumnNames(List.of("child_id"));

index.setReference(null);

assertSame(replacement, index.getTable());
assertSame(columns, index.getReferencedColumnNames());
assertEquals(List.of("child_id"), index.getReferentialAction(Type.DELETE).getColumnNames());
assertEquals(List.of("child_id"),
index.getReference().getReferentialAction(Type.DELETE).getColumnNames());
}

@Test
void accessingReferenceDoesNotReplaceActionsOrDiscardTheirColumns() {
ForeignKeyIndex index = new ForeignKeyIndex();
index.setReferentialAction(Type.DELETE, Action.SET_NULL);
ReferentialAction action = index.getReferentialAction(Type.DELETE);
action.setColumnNames(List.of("child_id"));

assertSame(action, index.getReference().getReferentialAction(Type.DELETE));
assertEquals(List.of("child_id"), index.getReferentialAction(Type.DELETE).getColumnNames());
action.setAction(Action.SET_DEFAULT);
assertEquals(Action.SET_DEFAULT,
index.getReference().getReferentialAction(Type.DELETE).getAction());
}

@Test
@SuppressWarnings("deprecation")
void deprecatedActionSettersUpdateStructuredReference() {
ForeignKeyIndex index = new ForeignKeyIndex();
ForeignKeyReference reference = index.getReference();
index.setOnDeleteReferenceOption("CASCADE");
index.setOnUpdateReferenceOption("RESTRICT");
assertEquals(Action.CASCADE, reference.getReferentialAction(Type.DELETE).getAction());
assertEquals("RESTRICT", index.getOnUpdateReferenceOption());
reference.setReferentialAction(Type.UPDATE, Action.CASCADE);
assertEquals("CASCADE", index.getOnUpdateReferenceOption());
index.setOnDeleteReferenceOption(null);
index.setOnUpdateReferenceOption(null);
assertNull(reference.getReferentialAction(Type.DELETE));
assertNull(reference.getReferentialAction(Type.UPDATE));
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void mutationsSurviveBothRenderersAndReparsing(boolean create) throws JSQLParserException {
String prefix = create ? "CREATE TABLE child (parent_id INT, " : "ALTER TABLE child ADD ";
Statement statement = CCJSqlParserUtil.parse(prefix
+ "CONSTRAINT fk FOREIGN KEY (parent_id) REFERENCES parent (id) ON DELETE CASCADE"
+ (create ? ")" : ""));
ForeignKeyIndex index = foreignKey(statement);
ForeignKeyReference reference = index.getReference();
reference.setTable(new Table("other_parent"));
reference.getReferencedColumnNames().set(0, "other_id");
index.setReferentialAction(Type.DELETE, Action.SET_NULL);
index.getReferentialAction(Type.DELETE).setColumnNames(List.of("parent_id"));
index.setMatchType(ForeignKeyReference.MatchType.SIMPLE);

StringBuilder deparsed = new StringBuilder();
statement.accept(new StatementDeParser(deparsed));
assertEquals(statement.toString(), deparsed.toString());
Statement reparsed = CCJSqlParserUtil.parse(deparsed.toString());
assertEquals(deparsed.toString(), reparsed.toString());
ForeignKeyIndex result = foreignKey(reparsed);
assertEquals("other_parent", result.getTable().getName());
assertEquals(List.of("other_id"), result.getReferencedColumnNames());
assertEquals(List.of("parent_id"),
result.getReferentialAction(Type.DELETE).getColumnNames());
assertEquals(ForeignKeyReference.MatchType.SIMPLE, result.getMatchType());
}

private static ForeignKeyIndex foreignKey(Statement statement) {
return (ForeignKeyIndex) (statement instanceof CreateTable
? ((CreateTable) statement).getIndexes().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getIndex());
}
}
Loading