Package com.foundationdb.sql.optimizer.rule

Source Code of com.foundationdb.sql.optimizer.rule.HalloweenRecognizer$Checker

/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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/>.
*/

package com.foundationdb.sql.optimizer.rule;

import com.foundationdb.ais.model.ForeignKey;
import com.foundationdb.ais.model.Index;
import com.foundationdb.ais.model.Table;
import com.foundationdb.server.error.UnsupportedSQLException;
import com.foundationdb.server.types.texpressions.Comparison;
import com.foundationdb.sql.optimizer.plan.*;

import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.IndexColumn;
import com.foundationdb.ais.model.Join;
import com.foundationdb.ais.model.JoinColumn;

import com.foundationdb.sql.optimizer.plan.BaseUpdateStatement.StatementType;
import com.foundationdb.sql.optimizer.plan.UpdateStatement.UpdateColumn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/** Identify queries that are susceptible to the Halloween problem.
* <ul>
* <li>Updating a primary or grouping foreign key,
* which can change hkeys and so group navigation.</li>
* <li>Updating a field of an index that is scanned.</li>
* <li>A <em>second</em> access to the target table.</li>
* </ul>
*/
public class HalloweenRecognizer extends BaseRule
{
    private static final Logger logger = LoggerFactory.getLogger(HalloweenRecognizer.class);

    @Override
    protected Logger getLogger() {
        return logger;
    }

    @Override
    public void apply(PlanContext plan) {
        if (plan.getPlan() instanceof DMLStatement) {
            DMLStatement stmt = (DMLStatement)plan.getPlan();
            TableNode targetTable = stmt.getTargetTable();
            boolean indexWasUnique = false;
            boolean bufferRequired = false;
            Set<Column> updateColumns = null;
            UpdateStatement updateStmt = null;

            if (stmt.getType() == StatementType.UPDATE) {
                updateStmt = findInput(stmt, UpdateStatement.class);
                assert (updateStmt != null);
                updateColumns = new HashSet<>();
                Set<Column> vulnerableColumns = new HashSet<>();
                update: {
                    for (UpdateStatement.UpdateColumn updateColumn : updateStmt.getUpdateColumns()) {
                        updateColumns.add(updateColumn.getColumn());
                    }
                    for (Column pkColumn : targetTable.getTable().getPrimaryKeyIncludingInternal().getColumns()) {
                        vulnerableColumns.add(pkColumn);
                    }
                    Join parentJoin = targetTable.getTable().getParentJoin();
                    if (parentJoin != null) {
                        for (JoinColumn joinColumn : parentJoin.getJoinColumns()) {
                            vulnerableColumns.add(joinColumn.getChild());
                        }
                    }
                    for (Column column : vulnerableColumns) {
                        if (updateColumns.contains(column)) {
                            bufferRequired = true;
                            break update;
                        }
                    }
                }
                // Can avoid transforming plan if all vulnerable columns exist as exact predicates
                if (bufferRequired) {
                    bufferRequired = !allHaveEquality(updateStmt, vulnerableColumns);
                }
                indexWasUnique = bufferRequired;
            }

            if (!bufferRequired) {
                Checker checker = new Checker(targetTable.getTable(),
                                              (stmt.getType() == StatementType.INSERT) ? 0 : 1,
                                              updateColumns);
                checker.check(stmt.getInput());
                bufferRequired = checker.bufferRequired;
                indexWasUnique = checker.indexWasUnique();

                if(indexWasUnique && (stmt.getType() == StatementType.UPDATE)) {
                    Set<Column> columns = new HashSet<>();
                    for(IndexColumn ic : checker.index.getKeyColumns()) {
                        columns.add(ic.getColumn());
                    }
                    // Again, avoid transform is possible
                    bufferRequired = indexWasUnique = !allHaveEquality(findInput(stmt, UpdateStatement.class), columns);
                }
            }

            boolean isFKReferenced = !targetTable.getTable().getReferencedForeignKeys().isEmpty();
            if(isFKReferenced) {
                // Reject any DELETE or UPDATE containing the referencing table when the action may change it
                for(ForeignKey fk : targetTable.getTable().getReferencedForeignKeys()) {
                    ForeignKey.Action action = null;
                    switch(stmt.getType()) {
                        case DELETE:
                            action = fk.getDeleteAction();
                        break;
                        case UPDATE:
                            assert updateColumns != null;
                            for(Column c : fk.getReferencedColumns()) {
                                if(updateColumns.contains(c)) {
                                    action = fk.getUpdateAction();
                                    break;
                                }
                            }
                        break;
                    }
                    if((action != null) && checkForReferencing(stmt.getInput(), action, fk.getReferencingTable())) {
                        throw new UnsupportedSQLException(
                            String.format("DML includes both referenced and referencing table %s with FOREIGN KEY action %s",
                                          fk.getReferencingTable().getName().toString(),
                                          action.name()));
                    }
                }
            }

            if(bufferRequired) {
                switch(stmt.getType()) {
                    case INSERT:
                    case DELETE:
                        injectBufferNode(stmt);
                        break;
                    case UPDATE:
                        if(indexWasUnique) {
                            if(isFKReferenced) {
                                throw new UnsupportedSQLException("Halloween vulnerable query on referenced table");
                            }
                            DMLStatement newDML = transformUpdate(stmt, updateStmt);
                            plan.setPlan(newDML);
                        } else {
                            injectBufferNode(stmt);
                        }
                        break;
                    default:
                        assert false : stmt.getType();
                }
            }
        }
    }

    private static <T> T findInput(PlanNode node, Class<T> clazz) {
        while(node != null) {
            if(clazz.isInstance(node)) {
                return clazz.cast(node);
            }
            if(node instanceof BasePlanWithInput) {
                node = ((BasePlanWithInput)node).getInput();
            } else {
                break;
            }
        }
        return null;
    }

    /** Convert {@link UpdateStatement#getUpdateColumns()} to a list of expressions suitable for Project */
    private static List<ExpressionNode> updateListToProjectList(UpdateStatement update, TableSource tableSource) {
        List<ExpressionNode> projects = new ArrayList<>();
        for(Column c : update.getTargetTable().getTable().getColumns()) {
            projects.add(new ColumnExpression(tableSource, c));
        }
        for(UpdateColumn updateCol : update.getUpdateColumns()) {
            projects.set(updateCol.getColumn().getPosition(), updateCol.getExpression());
        }
        return projects;
    }

    /**
     * Transform
     * <pre>Out() <- Update(col=5) <- In()</pre>
     * to
     * <pre>Out() <- Insert() <- Project(col=5) <- Buffer() <- Delete() <- In()</pre>
     */
    private static DMLStatement transformUpdate(DMLStatement dml, UpdateStatement update) {
        assert dml.getOutput() == null;

        TableSource tableSource = dml.getSelectTable();
        PlanNode scanSource = update.getInput();
        PlanWithInput updateDest = update.getOutput();

        InsertStatement insert = new InsertStatement(
            new Project(
                new Buffer(
                    new DeleteStatement(scanSource, update.getTargetTable(), tableSource)
                ),
                updateListToProjectList(update, tableSource)
            ),
            update.getTargetTable(),
            tableSource.getTable().getTable().getColumns(),
            tableSource
        );

        scanSource.setOutput(insert);
        updateDest.replaceInput(update, insert);

        return new DMLStatement(insert, StatementType.INSERT, dml.getSelectTable(), dml.getTargetTable(),
                                dml.getResultField(), dml.getReturningTable(), dml.getColumnEquivalencies());
    }

    /**
     * Add a buffer below the top level I/U/D.
     * <pre>
     * Insert/Update/Delete()
     *   Scan/Map/Etc()
     * </pre>
     * to
     * <pre>
     * Insert/Update/Delete()
     *   Buffer()
     *     Scan/Map/Etc()
     * </pre>
     */
    private static void injectBufferNode(DMLStatement dml) {
        PlanNode node = findInput(dml, BaseUpdateStatement.class).getInput();
        // Push it below a Project to avoid double pack/unpack.
        // TODO: Pushing below Flatten and BaseLookup would buffer smaller rows but requires RowType.ancestorHKey(),
        //       which isn't generally true coming out of Sorters (e.g. ValuesHolderRow)
        while(node instanceof Project) {
            node = ((BasePlanWithInput)node).getInput();
        }
        PlanWithInput origDest = node.getOutput();
        PlanWithInput newInput = new Buffer(node);
        origDest.replaceInput(node, newInput);
    }

    /** Check if all columns are have exact equality conditions */
    private static boolean equalityConditionsPresent(Collection<Column> columns, List<ConditionExpression> conditions) {
        for (Column column : columns) {
            if (!equalityConditionPresent(column, conditions)) {
                return false;
            }
        }
        return true;
    }

    /** Check if column has an exact equality conditions */
    private static boolean equalityConditionPresent(Column column, List<ConditionExpression> conditions) {
        for (ConditionExpression cond : conditions) {
            if (cond instanceof ComparisonCondition) {
                ComparisonCondition cc = (ComparisonCondition)cond;
                ColumnExpression colExpr = null;
                ExpressionNode otherExpr = null;
                if(cc.getLeft() instanceof ColumnExpression) {
                    colExpr = (ColumnExpression)cc.getLeft();
                    otherExpr = cc.getRight();
                } else if (cc.getRight() instanceof ColumnExpression) {
                    colExpr = (ColumnExpression)cc.getRight();
                    otherExpr = cc.getLeft();
                }
                if ((colExpr != null) &&
                    (cc.getOperation() == Comparison.EQ) &&
                    (colExpr.getColumn() == column) &&
                    (otherExpr instanceof ConstantExpression || otherExpr instanceof ParameterExpression)) {
                    return true;
                }
            }
        }
        return false;
    }

    /** Check if all of the columns have equality conditions present. */
    private static boolean allHaveEquality(UpdateStatement updateStmt, Collection<Column> vulnerableColumns) {
        ExpressionsHKeyScan hkeyScan = findInput(updateStmt, ExpressionsHKeyScan.class);
        boolean allExact = (hkeyScan != null) &&
            equalityConditionsPresent(vulnerableColumns, hkeyScan.getConditions());
        if (!allExact) {
            Select select = findInput(updateStmt, Select.class);
            allExact = (select != null) &&
                equalityConditionsPresent(vulnerableColumns, select.getConditions());
        }
        if (!allExact) {
            SingleIndexScan singleIndex = findInput(updateStmt, SingleIndexScan.class);
            allExact = (singleIndex != null) &&
                equalityConditionsPresent(vulnerableColumns, singleIndex.getConditions());
        }
        return allExact;
    }

    /** @return {@code true} if {@code action} is vulnerable to referencing side changing and plan uses referencing. */
    private static boolean checkForReferencing(PlanNode planNode, ForeignKey.Action action, Table referencing) {
        switch(action) {
            case NO_ACTION:
            case RESTRICT:
                // Safe as no changes are made to referencing rows
                return false;
            default:
                Checker checker = new Checker(referencing, 0, null);
                checker.check(planNode);
                return checker.bufferRequired;
        }
    }


    static class Checker implements PlanVisitor, ExpressionVisitor {
        private final Table targetTable;
        private final Set<Column> updateColumns;
        private int targetMaxUses;
        private boolean bufferRequired;
        private Index index;

        public Checker(Table targetTable, int targetMaxUses, Set<Column> updateColumns) {
            this.targetTable = targetTable;
            this.targetMaxUses = targetMaxUses;
            this.updateColumns = updateColumns;
        }

        public void check(PlanNode root) {
            bufferRequired = false;
            root.accept(this);
        }

        public boolean indexWasUnique() {
            return (index != null) && index.isUnique();
        }

        private boolean shouldContinue() {
            // Need to identify *any* index that is driving the scan
            return !(bufferRequired && indexWasUnique());
        }

        private void indexScan(IndexScan scan) {
            if (scan instanceof SingleIndexScan) {
                boolean newBufferRequired = false;
                SingleIndexScan single = (SingleIndexScan)scan;
                if (single.isCovering()) { // Non-covering loads via XxxLookup.
                    for (TableSource table : single.getTables()) {
                        if (table.getTable().getTable() == targetTable) {
                            targetMaxUses--;
                            if (targetMaxUses < 0) {
                                newBufferRequired = true;
                            }
                            break;
                        }
                    }
                }
                if (updateColumns != null) {
                    for (IndexColumn indexColumn : single.getIndex().getAllColumns()) {
                        if (updateColumns.contains(indexColumn.getColumn())) {
                            newBufferRequired = true;
                            break;
                        }
                    }
                }

                if(newBufferRequired) {
                    bufferRequired = true;
                    index = single.getIndex();
                }
            }
            else if (scan instanceof MultiIndexIntersectScan) {
                MultiIndexIntersectScan multi = (MultiIndexIntersectScan)scan;
                indexScan(multi.getOutputIndexScan());
                indexScan(multi.getSelectorIndexScan());
            }
        }

        @Override
        public boolean visitEnter(PlanNode n) {
            return visit(n);
        }

        @Override
        public boolean visitLeave(PlanNode n) {
            return shouldContinue();
        }

        @Override
        public boolean visit(PlanNode n) {
            if (n instanceof IndexScan) {
                indexScan((IndexScan)n);
            }
            else if (n instanceof TableLoader) {
                for (TableSource table : ((TableLoader)n).getTables()) {
                    if (table.getTable().getTable() == targetTable) {
                        targetMaxUses--;
                        if (targetMaxUses < 0) {
                            bufferRequired = true;
                            break;
                        }
                    }
                }
            }
            return shouldContinue();
        }

        @Override
        public boolean visitEnter(ExpressionNode n) {
            return visit(n);
        }

        @Override
        public boolean visitLeave(ExpressionNode n) {
            return shouldContinue();
        }

        @Override
        public boolean visit(ExpressionNode n) {
            return shouldContinue();
        }
    }
}
TOP

Related Classes of com.foundationdb.sql.optimizer.rule.HalloweenRecognizer$Checker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.