com.tesora.dve.sql.schema.PEForeignKey.java Source code

Java tutorial

Introduction

Here is the source code for com.tesora.dve.sql.schema.PEForeignKey.java

Source

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);
            }
        }
    }
}