package com.tesora.dve.sql.schema;
/*
* #%L
* Tesora Inc.
* Database Virtualization Engine
* %%
* Copyright (C) 2011 - 2014 Tesora Inc.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import com.tesora.dve.common.MultiMap;
import com.tesora.dve.common.catalog.ConstraintType;
import com.tesora.dve.common.catalog.FKMode;
import com.tesora.dve.common.catalog.IndexType;
import com.tesora.dve.common.catalog.Key;
import com.tesora.dve.exceptions.PEException;
import com.tesora.dve.sql.ParserException.Pass;
import com.tesora.dve.sql.SchemaException;
import com.tesora.dve.sql.expression.ScopeStack;
import com.tesora.dve.sql.expression.TableKey;
import com.tesora.dve.sql.jg.DunPart;
import com.tesora.dve.sql.jg.JoinEdge;
import com.tesora.dve.sql.node.expression.ColumnInstance;
import com.tesora.dve.sql.node.expression.ExpressionNode;
import com.tesora.dve.sql.node.expression.FunctionCall;
import com.tesora.dve.sql.node.expression.TableInstance;
import com.tesora.dve.sql.schema.cache.SchemaCacheKey;
import com.tesora.dve.sql.schema.cache.SchemaEdge;
import com.tesora.dve.sql.schema.validate.ForeignKeyValidateResult;
import com.tesora.dve.sql.schema.validate.ValidateResult;
import com.tesora.dve.sql.statement.dml.DeleteStatement;
import com.tesora.dve.sql.statement.dml.UpdateStatement;
import com.tesora.dve.sql.util.Functional;
public class PEForeignKey extends PEKey {
private ForeignKeyAction updateAction;
private ForeignKeyAction deleteAction;
private Name targetTableName;
private SchemaEdge<PETable> targetTable;
private UnqualifiedName physicalSymbol;
@SuppressWarnings("unchecked")
public PEForeignKey(SchemaContext pc, Name n, PETable tab, Name missingTableName, List<PEKeyColumnBase> cols,
ForeignKeyAction updateAction, ForeignKeyAction deleteAction) {
super(n, IndexType.BTREE, cols, null);
setConstraint(ConstraintType.FOREIGN);
this.updateAction = updateAction;
this.deleteAction = deleteAction;
targetTableName = missingTableName;
if (tab != null)
targetTable = StructuralUtils.buildEdge(pc,tab, false);
}
public void setSymbol(Name symbol) {
super.setSymbol(symbol);
if (symbol != null)
physicalSymbol = symbol.getUnqualified();
}
public void setPhysicalSymbol(UnqualifiedName psym) {
physicalSymbol = psym;
}
public UnqualifiedName getPhysicalSymbol() {
return physicalSymbol;
}
public ForeignKeyAction getUpdateAction() {
return updateAction;
}
public ForeignKeyAction getDeleteAction() {
return deleteAction;
}
public boolean isForward() {
return targetTableName != null;
}
public PETable getTargetTable(SchemaContext sc) {
if (targetTable == null) return null;
return targetTable.get(sc);
}
@SuppressWarnings("unchecked")
public void setTargetTable(SchemaContext sc, PETable pet) {
if (pet != null) {
targetTable = StructuralUtils.buildEdge(sc,pet, false);
targetTableName = null;
}
}
public void revertToForward(SchemaContext sc) {
PETable targ = targetTable.get(sc);
targetTableName = new QualifiedName(targ.getPEDatabase(sc).getName().getUnqualified(),targ.getName().getUnqualified());
revertToForwardInternal(sc);
}
// use ONLY in mt fk support, and ONLY in the tschema
public void revertToForwardMT(SchemaContext sc, Name forwardTableName) {
if (forwardTableName.isQualified())
targetTableName = forwardTableName;
else
targetTableName = new QualifiedName(targetTable.get(sc).getPEDatabase(sc).getName().getUnqualified(),forwardTableName.getUnqualified());
revertToForwardInternal(sc);
}
private void revertToForwardInternal(SchemaContext sc) {
for(PEKeyColumnBase pekc : getKeyColumns()) {
PEForeignKeyColumn pefkc = (PEForeignKeyColumn) pekc;
pefkc.revertToForward(sc);
}
targetTable = null;
}
public Name getTargetTableName(SchemaContext sc) {
return getTargetTableName(sc,false);
}
public Name getTargetTableName(SchemaContext sc, boolean qualifiedName) {
if (targetTable != null) {
if (qualifiedName || !targetTable.get(sc).getPEDatabase(sc).getCacheKey().equals(getTable(sc).getPEDatabase(sc).getCacheKey())) {
// target table in different database - use fully qualified name
return targetTable.get(sc).getName().prefix(targetTable.get(sc).getPEDatabase(sc).getName());
}
return targetTable.get(sc).getName().getUnqualified();
}
if (targetTableName.isQualified()) {
QualifiedName qn = (QualifiedName) targetTableName;
UnqualifiedName dbn = qn.getNamespace();
if (!qualifiedName && getTable(sc).getPEDatabase(sc).getName().equals(dbn))
return targetTableName.getUnqualified();
return targetTableName;
}
return targetTableName.getUnqualified();
}
// use ONLY in mt fk support, and ONLY in the tschema
public void resetTargetTableName(UnqualifiedName unq) {
if (!isForward()) throw new SchemaException(Pass.PLANNER,"Internal error: attempt to set target table name when not forward");
targetTableName = unq;
}
public List<PEColumn> getTargetColumns(SchemaContext sc) {
if (isForward()) return null;
List<PEColumn> out = new ArrayList<PEColumn>();
for(PEKeyColumnBase pekc : getKeyColumns()) {
PEForeignKeyColumn pefkc = (PEForeignKeyColumn) pekc;
out.add(pefkc.getTargetColumn(sc));
}
return out;
}
public PEKey findPrefixKey(SchemaContext sc, PETable inTable) {
List<PEKey> candidates = new ArrayList<PEKey>();
for(PEKey pek : inTable.getKeys(sc)) {
if (pek.getConstraint() == ConstraintType.FOREIGN) continue;
if (getKeyColumns().size() > pek.getColumns(sc).size()) continue;
candidates.add(pek);
}
for(int i = 0; i < getKeyColumns().size(); i++) {
PEColumn spc = getKeyColumns().get(i).getColumn();
for(Iterator<PEKey> iter = candidates.iterator(); iter.hasNext();) {
PEKey pek = iter.next();
if (!pek.getColumns(sc).get(i).equals(spc))
iter.remove();
}
}
if (candidates.isEmpty()) return null;
return candidates.get(0);
}
public void addColumn(int index, PEColumn src, UnqualifiedName targ) {
PEForeignKeyColumn pefkc = new PEForeignKeyColumn(src, targ, isForward());
columns.add(index, pefkc);
pefkc.setKey(this);
}
public PEKey buildPrefixKey(SchemaContext sc, PETable inTable) {
List<PEKeyColumnBase> cols = new ArrayList<PEKeyColumnBase>();
for(PEKeyColumnBase c : getKeyColumns()) {
cols.add(new PEKeyColumn(c.getColumn(),null,-1));
}
return new PEKey(null,IndexType.BTREE,cols,null,true);
}
@Override
public PEKey resolve(SchemaContext pc, ScopeStack stack) {
List<PEKeyColumnBase> resolved = new ArrayList<PEKeyColumnBase>();
boolean any = false;
for(PEKeyColumnBase pekcb : getKeyColumns()) {
if (pekcb.isUnresolved()) {
PEColumn found = stack.lookupInProcessColumn(pekcb.getName());
if (found == null)
throw new SchemaException(Pass.SECOND, "Cannot resolve column " + pekcb.getName());
resolved.add(pekcb.resolve(found));
any = true;
} else {
resolved.add(pekcb);
}
}
if (!any)
return this;
return new PEForeignKey(pc, getName(),
(targetTable == null ? null : targetTable.get(pc)),
targetTableName, resolved, updateAction, deleteAction);
}
public static PEForeignKey load(Key k, SchemaContext sc, PETable enclosingTable) {
PEForeignKey p = (PEForeignKey) sc.getLoaded(k,null);
if (p == null) {
if (k.isForeignKey())
return new PEForeignKey(sc,k, enclosingTable);
throw new SchemaException(Pass.SECOND,"Invalid call to PEForeignKey.load - found key of type " + k.getType());
}
return p;
}
@SuppressWarnings("unchecked")
protected PEForeignKey(SchemaContext sc, Key k, PETable enclosingTable) {
super(sc,k, enclosingTable);
this.updateAction = ForeignKeyAction.fromPersistent(k.getFKUpdateAction());
this.deleteAction = ForeignKeyAction.fromPersistent(k.getFKDeleteAction());
if (k.getReferencedTable() != null) {
this.targetTable = StructuralUtils.buildEdge(sc,PETable.load(k.getReferencedTable(),sc), true);
this.targetTableName = null;
} else {
this.targetTable = null;
this.targetTableName = new QualifiedName(new UnqualifiedName(k.getReferencedSchemaName()), new UnqualifiedName(k.getReferencedTableName()));
}
setPhysicalSymbol(new UnqualifiedName(k.getPhysicalSymbol()));
}
private void updatePersistent(SchemaContext sc, Key p) throws PEException {
p.setFKDeleteAction(deleteAction.getPersistent());
p.setFKUpdateAction(updateAction.getPersistent());
p.setPersisted(persisted);
p.setPhysicalSymbol(physicalSymbol.getUnquotedName().get());
if (targetTable != null) {
p.setReferencedTable(targetTable.get(sc).persistTree(sc));
} else if (targetTableName != null) {
String dbName = null;
String tabName = null;
if (targetTableName.isQualified()) {
QualifiedName qn = (QualifiedName) targetTableName;
dbName = qn.getNamespace().getUnquotedName().get();
tabName = qn.getUnqualified().getUnquotedName().get();
} else {
tabName = targetTableName.getUnquotedName().get();
}
p.setReferencedTable(dbName, tabName);
}
}
@Override
protected void populateNew(SchemaContext sc, Key p) throws PEException {
super.populateNew(sc,p);
updatePersistent(sc,p);
}
@Override
protected void updateExisting(SchemaContext sc, Key p) throws PEException {
super.updateExisting(sc, p);
updatePersistent(sc,p);
}
public boolean isColocated(SchemaContext sc) {
PETable leftTab = getTable(sc);
PETable rightTab = getTargetTable(sc);
if (rightTab == null) return false;
Map<PEColumn,PEColumn> mapping = new HashMap<PEColumn,PEColumn>();
for(PEKeyColumnBase pekc : getKeyColumns()) {
PEForeignKeyColumn pefkc = (PEForeignKeyColumn) pekc;
mapping.put(pefkc.getColumn(), pefkc.getTargetColumn(sc));
}
TableKey lk = TableKey.make(sc,leftTab,1);
TableKey rk = TableKey.make(sc,rightTab,2);
DunPart lp = new DunPart(lk,1);
DunPart rp = new DunPart(rk,2);
return JoinEdge.computeColocated(sc, lp,Collections.singleton(lk), rp, rk, mapping, null,true);
}
@Override
public void checkValid(SchemaContext sc, List<ValidateResult> results) {
if (isForward() || !isPersisted()) return;
boolean hasUniqueTargetCol = false;
for(int i = 0; i < getKeyColumns().size(); i++) {
PEForeignKeyColumn pefkc = (PEForeignKeyColumn) getKeyColumns().get(i);
for(PEKey pek : pefkc.getTargetColumn(sc).getReferencedBy(sc)) {
if (pek.isValidFkTarget(sc))
hasUniqueTargetCol = true;
}
}
if (!hasUniqueTargetCol ) {
results.add(new ForeignKeyValidateResult(sc,this,ForeignKeyValidateResult.FKValidateKind.NO_UNIQUE_KEY,true));
return;
}
if (!isColocated(sc)) {
// figure out the fk mode on the db in order to determine whether this is a warning or error
FKMode fkm = getTable(sc).getPEDatabase(sc).getFKMode();
results.add(new ForeignKeyValidateResult(sc,this,ForeignKeyValidateResult.FKValidateKind.NOT_COLOCATED,(fkm == FKMode.STRICT)));
}
}
@Override
public boolean collectDifferences(SchemaContext sc, List<String> messages, Persistable<PEKey, Key> oth,
boolean first, @SuppressWarnings("rawtypes") Set<Persistable> visited) {
PEForeignKey other = (PEForeignKey) oth.get();
if (visited.contains(this) && visited.contains(other)) {
return false;
}
visited.add(this);
visited.add(other);
if (maybeBuildDiffMessage(sc,messages, "delete action", getDeleteAction(), other.getDeleteAction(), first, visited))
return true;
if (maybeBuildDiffMessage(sc,messages, "update action", getUpdateAction(), other.getUpdateAction(), first, visited))
return true;
// then make sure that the targetTableName value is the same in both - this indicates whether the key is
// forward or not
if (maybeBuildDiffMessage(sc, messages, "forward ref", targetTableName, other.targetTableName, first, visited))
return true;
if (maybeBuildDiffMessage(sc, messages, "target table", getTargetTable(sc), other.getTargetTable(sc), first, visited))
return true;
if (super.collectDifferences(sc, messages, oth, first, visited))
return true;
return false;
}
@Override
public PEKey copy(SchemaContext sc, PETable containingTable) {
List<PEKeyColumnBase> contained = new ArrayList<PEKeyColumnBase>();
for(PEKeyColumnBase p : getKeyColumns()) {
contained.add(p.copy(sc, containingTable));
}
PETable targetTab = (targetTable != null ? targetTable.get(sc) : null);
PEForeignKey out = new PEForeignKey(sc,getName(),targetTab,targetTableName,contained,updateAction,deleteAction);
out.setSymbol(getSymbol());
out.setPhysicalSymbol(getPhysicalSymbol());
return out;
}
public static void doForeignKeyChecks(SchemaContext sc, DeleteStatement ds) {
TableInstance targ = ds.getTargetDeleteEdge().get();
PETable targTab = targ.getAbstractTable().asTable();
processFKChecksOnDelete(sc,targTab,null,new HashSet<PETable>());
}
public static void doForeignKeyChecks(SchemaContext sc, UpdateStatement us) {
MultiMap<PETable,PEColumn> updated = new MultiMap<PETable,PEColumn>();
for(ExpressionNode en : us.getUpdateExpressions()) {
FunctionCall fc = (FunctionCall) en;
ColumnInstance ci = (ColumnInstance) fc.getParametersEdge().get(0);
PEColumn col = ci.getPEColumn();
updated.put(col.getTable().asTable(),col);
}
for(PETable pet : updated.keySet()) {
Collection<PEColumn> sub = updated.get(pet);
if (sub == null || sub.isEmpty()) continue;
processFKChecksOnUpdate(sc,pet,Functional.toList(sub),null,new HashSet<PETable>());
}
}
// fk checks when modifying a parent:
// parent op child action result
// delete restrict do nothing
// delete no action do nothing
// delete set null if child is bcast allow, otherwise prohibit
// delete cascade do nothing (allow)
// update restrict do nothing
// update no action do nothing
// update set null if child is bcast allow, otherwise prohibit
// update cascade if child is bcast allow, otherwise prohibit
//
// in all cases, when an fk is not persisted, break the chain (since the action would not propogate on the sites)
// in mt mode if both tables are tenant id distributedonly allow the delete/update to proceed since it will only
// effect one site.
private static void processFKChecksOnDelete(SchemaContext sc,PETable modifiedTable,PETable referencingTable,Set<PETable> processed) {
if (referencingTable == null) {
for(SchemaCacheKey<PEAbstractTable<?>> referring : modifiedTable.getReferencingTables()) {
PETable actual = sc.getSource().find(sc, referring).asTable();
processFKChecksOnDelete(sc,modifiedTable,actual,processed);
}
} else {
if (!processed.add(referencingTable)) return;
RangeDistribution mr = modifiedTable.getDistributionVector(sc).getDistributedWhollyOnTenantColumn(sc);
RangeDistribution rr = referencingTable.getDistributionVector(sc).getDistributedWhollyOnTenantColumn(sc);
boolean bothTenantID = (mr != null && rr != null && mr.getCacheKey().equals(rr.getCacheKey()));
for(PEKey pek : referencingTable.getKeys(sc)) {
if (!pek.isForeign()) continue;
PEForeignKey pefk = (PEForeignKey) pek;
if (!pefk.isPersisted()) continue;
PETable targTab = pefk.getTargetTable(sc);
if (targTab == null) continue;
if (targTab != modifiedTable) continue;
ForeignKeyAction fka = pefk.getDeleteAction();
boolean mustRecurse = false;
if (fka == ForeignKeyAction.NO_ACTION || fka == ForeignKeyAction.RESTRICT) {
// no cascade, any action will stop on the individual p sites
mustRecurse = false;
} else if (fka == ForeignKeyAction.CASCADE) {
// we need to check referrers
mustRecurse = true;
} else if (fka == ForeignKeyAction.SET_NULL) {
if (!referencingTable.getDistributionVector(sc).isBroadcast() && !bothTenantID) {
throw new SchemaException(Pass.PLANNER, "Unable to delete from " + modifiedTable.getName()
+ " due to set null action on foreign key " + pefk.getName()
+ " in table " + referencingTable.getName());
}
mustRecurse = true;
// we've now converted a delete action to an update action, proceed as an update - use a new set
processFKChecksOnUpdate(sc,referencingTable,pefk.getColumns(sc),null,new HashSet<PETable>());
} else {
throw new SchemaException(Pass.PLANNER, "Unknown foreign key action kind: " + fka);
}
if (mustRecurse)
processFKChecksOnDelete(sc,referencingTable,null,processed);
}
}
}
private static void processFKChecksOnUpdate(SchemaContext sc,PETable modifiedTable,List<PEColumn> modifiedColumns,
PETable referencingTable,Set<PETable> processed) {
if (referencingTable == null) {
for(SchemaCacheKey<PEAbstractTable<?>> referring : modifiedTable.getReferencingTables()) {
PETable actual = sc.getSource().find(sc, referring).asTable();
processFKChecksOnUpdate(sc,modifiedTable,modifiedColumns,actual,processed);
}
} else {
if (!processed.add(referencingTable)) return;
RangeDistribution mr = modifiedTable.getDistributionVector(sc).getDistributedWhollyOnTenantColumn(sc);
RangeDistribution rr = referencingTable.getDistributionVector(sc).getDistributedWhollyOnTenantColumn(sc);
boolean bothTenantID = (mr != null && rr != null && mr.getCacheKey().equals(rr.getCacheKey()));
for(PEKey pek : referencingTable.getKeys(sc)) {
if (!pek.isForeign()) continue;
PEForeignKey pefk = (PEForeignKey) pek;
if (!pefk.isPersisted()) continue;
PETable targTab = pefk.getTargetTable(sc);
if (targTab == null) continue;
if (targTab != modifiedTable) continue;
List<PEColumn> targCols = pefk.getTargetColumns(sc);
if (!CollectionUtils.isEqualCollection(modifiedColumns, targCols)) continue;
ForeignKeyAction updateAction = pefk.getUpdateAction();
boolean mustRecurse = false;
if (updateAction == ForeignKeyAction.RESTRICT || updateAction == ForeignKeyAction.NO_ACTION) {
mustRecurse = false;
} else if (updateAction == ForeignKeyAction.SET_NULL || updateAction == ForeignKeyAction.CASCADE) {
if (referencingTable.getDistributionVector(sc).isBroadcast() || bothTenantID) {
// must propagate the update
mustRecurse = true;
} else {
throw new SchemaException(Pass.PLANNER, "Unable to update table " + modifiedTable.getName()
+ " due to cascade/set null action on foreign key " + pefk.getName()
+ " in table " + referencingTable.getName());
}
}
if (mustRecurse)
processFKChecksOnUpdate(sc,referencingTable,pefk.getColumns(sc),null,processed);
}
}
}
}