package net.sourceforge.mayfly.datastore.constraint;
import net.sourceforge.mayfly.MayflyException;
import net.sourceforge.mayfly.MayflyInternalException;
import net.sourceforge.mayfly.datastore.Cell;
import net.sourceforge.mayfly.datastore.DataStore;
import net.sourceforge.mayfly.datastore.NullCell;
import net.sourceforge.mayfly.datastore.Row;
import net.sourceforge.mayfly.datastore.Rows;
import net.sourceforge.mayfly.datastore.TableData;
import net.sourceforge.mayfly.datastore.TableReference;
import net.sourceforge.mayfly.evaluation.select.Evaluator;
import net.sourceforge.mayfly.parser.Location;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.List;
public class ForeignKey extends Constraint {
/**
* @internal
* True if a foreign key can point to not just a primary key
* or a column with a unique constraint, but also to a column
* which itself is a foreign key pointing elsewhere. True
* for compatibility with MySQL; false for compatibility with
* most other databases.
*/
private static final boolean
FOREIGN_KEY_CAN_POINT_TO_FOREIGN_KEY = true;
private final String referencerSchema;
final String referencerTable;
final String referencerColumn;
final TableReference targetTable;
final String targetColumn;
private final Action onDelete;
private final Action onUpdate;
public ForeignKey(String referencerTable, String referencerColumn,
String targetTable, String targetColumn) {
this(DataStore.ANONYMOUS_SCHEMA_NAME, referencerTable, referencerColumn,
new TableReference(DataStore.ANONYMOUS_SCHEMA_NAME, targetTable),
targetColumn, new NoAction(), new NoAction(), null);
}
public ForeignKey(
String referencerSchema, String referencerTable, String referencerColumn,
TableReference targetTable, String targetColumn,
Action onDelete, Action onUpdate, String constraintName) {
super(constraintName);
this.referencerSchema = referencerSchema;
this.referencerTable = referencerTable;
this.referencerColumn = referencerColumn;
this.targetTable = targetTable;
this.targetColumn = targetColumn;
this.onDelete = onDelete;
this.onUpdate = onUpdate;
if (onUpdate instanceof Cascade) {
throw new MayflyException("ON UPDATE CASCADE not implemented");
}
else if (onUpdate instanceof SetNull) {
throw new MayflyException("ON UPDATE SET NULL not implemented");
}
else if (onUpdate instanceof SetDefault) {
throw new MayflyException("ON UPDATE SET DEFAULT not implemented");
}
}
@Override
public void checkExistingRows(DataStore store, TableReference table) {
checkWeAreInTheRightPlace(table.schema(), table.tableName());
TableData tableData = store.schema(table.schema()).table(table.tableName());
for (int i = 0; i < tableData.rowCount(); ++i) {
Row row = tableData.row(i);
checkInsert(store, row, Location.UNKNOWN);
}
}
@Override
public void checkInsert(DataStore store, String schema, String table,
Row proposedRow, Location location) {
checkWeAreInTheRightPlace(schema, table);
checkInsert(store, proposedRow, location);
}
private void checkInsert(DataStore store, Row proposedRow, Location location) {
TableData foundTable = store.table(targetTable);
Cell value = proposedRow.cell(referencerColumn);
if (!(value instanceof NullCell) &&
!foundTable.hasValue(targetColumn, value)) {
/*
Check for the case in which the row we are in the
process of inserting satisfies the constraint.
*/
if (refersToSameTable()) {
Cell newPossibleTarget = proposedRow.cell(targetColumn);
if (newPossibleTarget.sqlEquals(value, location)) {
return;
}
}
throwInsertException(referencerSchema, value, location);
}
}
/**
* @internal
* Return -1 if first must be inserted before second,
* 1 if second must be inserted before first,
* or 0 if they can be inserted in either order (as
* far as can be determined by just looking at those two).
*/
@Override
public boolean mustInsertBefore(Row first, Row second) {
if (first == second) {
// A row which satisfies its own constraint is OK.
return false;
}
if (refersToSameTable()) {
if (second.cell(referencerColumn)
.sqlEquals(first.cell(targetColumn))) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
private boolean refersToSameTable() {
return targetTable.matches(referencerSchema, referencerTable);
}
private void checkWeAreInTheRightPlace(String schema, String table) {
if (!referencerSchema.equalsIgnoreCase(schema) ||
!referencerTable.equalsIgnoreCase(table)) {
throw new MayflyInternalException(
"I'm confused about what tables foreign key constraints" +
" are attached to");
}
}
private void throwInsertException(String schema, Cell value, Location location) {
String targetTableName = targetTable.displayName(schema);
throw new MayflyException("foreign key violation: attempt in table " +
referencerDisplayName(schema) + ", column " + referencerColumn +
" to reference non-present value " + value.asBriefString() +
" in table " + targetTableName +
", column " +
targetColumn,
location);
}
@Override
public DataStore checkDelete(DataStore store, String schema, String table,
Row rowToDelete, Row replacementRow) {
if (tableIsMyTarget(schema, table)) {
Cell oldValue = rowToDelete.cell(targetColumn);
TableData referencer =
store.table(referencerSchema, referencerTable);
if (replacementRow != null) {
Cell newValue = replacementRow.cell(targetColumn);
if (oldValue.sqlEquals(newValue)) {
return store;
}
if (referencer.hasValue(referencerColumn, oldValue)) {
return onUpdate.handleUpdate(oldValue, newValue,
store, referencerSchema, referencerTable,
referencerColumn, targetTable, targetColumn);
}
}
else {
if (referencer.hasValue(referencerColumn, oldValue)) {
return onDelete.handleDelete(oldValue, store,
referencerSchema, referencerTable, referencerColumn,
targetTable, targetColumn);
}
}
}
return store;
}
@Override
public void checkDropTable(DataStore store, String schema, String table) {
if (tableIsMyTarget(schema, table) && !refersToSameTable()) {
throw new MayflyException(
"cannot drop " + table +
" because a foreign key in table " +
referencerDisplayName(schema) +
" refers to it"
);
}
}
private String referencerDisplayName(String schema) {
return TableReference.formatTableName(schema, referencerSchema, referencerTable);
}
private boolean tableIsMyTarget(String schema, String table) {
return targetTable.matches(schema, table);
}
/**
* @internal
* @returns should we keep this key?
*/
public boolean checkDropReferencerColumn(TableReference table, String column) {
checkWeAreInTheRightPlace(table.schema(), table.tableName());
if (refersTo(column)) {
return false;
}
return true;
}
@Override
public boolean refersTo(String column) {
return column.equalsIgnoreCase(referencerColumn);
}
@Override
public void checkDropTargetColumn(TableReference table, String column) {
if (tableIsMyTarget(table.schema(), table.tableName()) &&
column.equalsIgnoreCase(targetColumn)) {
throw new MayflyException("the column " + column +
" is referenced by a foreign key in table " +
//formatTableName(defaultSchema, referencerSchema, referencerTable)
referencerTable +
", column " + referencerColumn
);
}
}
@Override
public Constraint renameColumn(String oldName, String newName) {
if (oldName.equalsIgnoreCase(referencerColumn)) {
return new ForeignKey(
referencerSchema, referencerTable, newName,
targetTable, targetColumn,
onDelete, onUpdate, constraintName);
}
else {
return this;
}
}
@Override
public Constraint renameTable(String oldName, String newName) {
if (oldName.equalsIgnoreCase(referencerTable)) {
return new ForeignKey(
referencerSchema, newName, referencerColumn,
targetTable, targetColumn,
onDelete, onUpdate, constraintName);
} else if (oldName.equalsIgnoreCase(targetTable.tableName())) {
TableReference newTable = new TableReference(
targetTable.schema(), newName);
return new ForeignKey(
referencerSchema, referencerTable, referencerColumn,
newTable, targetColumn,
onDelete, onUpdate, constraintName);
}
else {
return this;
}
}
/**
* @internal
* Doesn't apply for foreign key; instead we check in
* {@link #checkDelete(DataStore, String, String, Row, Row)}
* and
* {@link #checkInsert(DataStore, String, String, Row, Location)}.
*/
@Override
public void check(Rows existingRows, Row proposedRow,
TableReference table, Location location) {
}
/**
* @internal
* For foreign key we currently check in
* {@link #checkDropReferencerColumn(TableReference, String)}.
*/
@Override
public boolean checkDropColumn(TableReference table, String column) {
return checkDropReferencerColumn(table, column);
}
@Override
public boolean canBeTargetOfForeignKey(String targetColumn) {
if (FOREIGN_KEY_CAN_POINT_TO_FOREIGN_KEY) {
return targetColumn.equalsIgnoreCase(referencerColumn);
}
return super.canBeTargetOfForeignKey(targetColumn);
}
@Override
public boolean refersTo(String table, Evaluator evaluator) {
return targetTable.matches(DataStore.ANONYMOUS_SCHEMA_NAME, table);
}
@Override
public List referencedTables() {
if (refersToSameTable()) {
return Collections.EMPTY_LIST;
}
else {
return Collections.singletonList(targetTable.tableName());
}
}
@Override
public void dump(Writer out) throws IOException {
out.write("FOREIGN KEY(");
out.write(referencerColumn);
out.write(") REFERENCES ");
out.write(targetTable.displayName(DataStore.ANONYMOUS_SCHEMA_NAME));
out.write("(");
out.write(targetColumn);
out.write(")");
if (!(onDelete instanceof NoAction)) {
out.write(" ON DELETE ");
onDelete.dump(out);
}
}
}