/** * 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.server.store; import com.foundationdb.ais.model.Column; import com.foundationdb.ais.model.Group; import com.foundationdb.ais.model.GroupIndex; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.Table; import com.foundationdb.qp.operator.API; import com.foundationdb.qp.operator.Cursor; import com.foundationdb.qp.operator.Operator; import com.foundationdb.qp.operator.QueryBindings; import com.foundationdb.qp.operator.QueryContext; import com.foundationdb.qp.operator.SimpleQueryContext; import com.foundationdb.qp.operator.StoreAdapter; import com.foundationdb.qp.rowtype.TableRowType; import com.foundationdb.qp.row.FlattenedRow; import com.foundationdb.qp.row.HKey; import com.foundationdb.qp.row.Row; import com.foundationdb.qp.rowtype.FlattenedRowType; import com.foundationdb.qp.rowtype.RowType; import com.foundationdb.qp.rowtype.Schema; import com.foundationdb.util.tap.InOutTap; import com.foundationdb.util.tap.PointTap; import com.foundationdb.util.tap.Tap; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; class StoreGIMaintenance { public void run(StoreGIHandler.Action action, HKey hKey, Row forRow, StoreAdapter adapter, StoreGIHandler handler) { if (storePlan.noMaintenanceRequired()) return; Cursor cursor = null; boolean runTapEntered = false; ALL_TAP.in(); try { Operator planOperator = rootOperator(); if (planOperator == null) return; QueryContext context = new SimpleQueryContext(adapter); QueryBindings bindings = context.createBindings(); List<Column> lookupCols = rowType.table().getPrimaryKeyIncludingInternal().getColumns(); bindings.setHKey(StoreGIMaintenance.HKEY_BINDING_POSITION, hKey); for (int i = 0; i < lookupCols.size(); ++i) { int bindingsIndex = i + 1; Column col = lookupCols.get(i); bindings.setValue(bindingsIndex, forRow.value(col.getPosition().intValue())); } cursor = API.cursor(planOperator, context, bindings); RUN_TAP.in(); runTapEntered = true; cursor.openTopLevel(); Row row; while ((row = cursor.next()) != null) { boolean actioned = false; if (row.rowType().equals(planOperator.rowType())) { doAction(action, handler, row); actioned = true; } else if (storePlan.incomingRowIsWithinGI) { // "Natural" index cleanup. Look for the left half, but only if we need to Index.JoinType giJoin = groupIndex.getJoinType(); switch (giJoin) { case LEFT: if (row.rowType().equals(storePlan.leftHalf) && useInvertType(action, context, bindings) && !skipCascadeRow(action, row, handler)) { Row outerRow = new FlattenedRow(storePlan.topLevelFlattenType, row, null, row.hKey()); doAction(invert(action), handler, outerRow); actioned = true; } break; case RIGHT: if (row.rowType().equals(storePlan.rightHalf) && useInvertType(action, context, bindings) && !skipCascadeRow(action, row, handler)) { Row outerRow = new FlattenedRow(storePlan.topLevelFlattenType, null, row, row.hKey()); doAction(invert(action), handler, outerRow); actioned = true; } break; default: throw new AssertionError(giJoin.name()); } } else { // Hkey cleanup. Look for the right half. if (row.rowType().equals(storePlan.rightHalf) && !skipCascadeRow(action, row, handler)) { Row outerRow = new FlattenedRow(storePlan.topLevelFlattenType, null, row, row.hKey()); doAction(invert(action), handler, outerRow); actioned = true; } } if (!actioned) { extraTap(action).hit(); } } } finally { if (cursor != null) { cursor.closeTopLevel(); } if (runTapEntered) { RUN_TAP.out(); } ALL_TAP.out(); } } private boolean skipCascadeRow(StoreGIHandler.Action action, Row row, StoreGIHandler handler) { return action == StoreGIHandler.Action.CASCADE && row.rowType().typeComposition().tables().contains(handler.getSourceTable()); } private boolean useInvertType(StoreGIHandler.Action action, QueryContext context, QueryBindings bindings) { switch (groupIndex.getJoinType()) { case LEFT: switch (action) { case CASCADE_STORE: case STORE: return true; case CASCADE: case DELETE: if (siblingsLookup == null) return false; Cursor siblingsCounter = API.cursor(siblingsLookup, context, bindings); SIBLING_ALL_TAP.in(); try { siblingsCounter.openTopLevel(); int siblings = 0; while (siblingsCounter.next() != null) { SIBLING_ROW_TAP.hit(); if (++siblings > 1) return false; } return true; } finally { siblingsCounter.closeTopLevel(); SIBLING_ALL_TAP.out(); } default: throw new AssertionError(action.name()); } case RIGHT: return true; default: throw new AssertionError(groupIndex.getJoinType().name()); } } private void doAction(StoreGIHandler.Action action, StoreGIHandler handler, Row row) { InOutTap actionTap = actionTap(action); actionTap.in(); try { handler.handleRow(groupIndex, row, action); } finally { actionTap.out(); } } private static StoreGIHandler.Action invert(StoreGIHandler.Action action) { switch (action) { case STORE: return StoreGIHandler.Action.DELETE; case DELETE: return StoreGIHandler.Action.STORE; case CASCADE: return StoreGIHandler.Action.CASCADE_STORE; case CASCADE_STORE: return StoreGIHandler.Action.CASCADE_STORE; default: throw new AssertionError(action.name()); } } private Operator rootOperator() { return storePlan.rootOperator; } private InOutTap actionTap(StoreGIHandler.Action action) { if (action == null) return OTHER_TAP; switch (action) { case STORE: return STORE_TAP; case DELETE: return DELETE_TAP; default: return OTHER_TAP; } } private PointTap extraTap(StoreGIHandler.Action action) { if (action == null) return EXTRA_OTHER_ROW_TAP; switch (action) { case STORE: return EXTRA_STORE_ROW_TAP; case DELETE: return EXTRA_DELETE_ROW_TAP; default: return EXTRA_OTHER_ROW_TAP; } } public StoreGIMaintenance(BranchTables branchTables, GroupIndex groupIndex, TableRowType rowType) { this.storePlan = createGroupIndexMaintenancePlan(branchTables, groupIndex, rowType); siblingsLookup = createSiblingsFinder(groupIndex, branchTables, rowType); this.rowType = rowType; this.groupIndex = groupIndex; } private final PlanCreationInfo storePlan; private final Operator siblingsLookup; private final GroupIndex groupIndex; private final TableRowType rowType; // for use in this class private Operator createSiblingsFinder(GroupIndex groupIndex, BranchTables branchTables, TableRowType rowType) { // only bother doing this for tables *leafward* of the rootmost table in the GI if (rowType.table().getDepth() <= branchTables.rootMost().table().getDepth()) return null; Table parentTable = rowType.table().getParentTable(); if (parentTable == null) { return null; } final Group group = groupIndex.getGroup(); final TableRowType parentRowType = branchTables.parentRowType(rowType); assert parentRowType != null; Operator plan = API.groupScan_Default( groupIndex.getGroup(), HKEY_BINDING_POSITION, false, rowType.table(), branchTables.fromRoot().get(0).table() ); plan = API.groupLookup_Default(plan, group, rowType, Collections.singleton(parentRowType), API.InputPreservationOption.DISCARD_INPUT, 1); plan = API.groupLookup_Default(plan, group, parentRowType, Collections.singleton(rowType), API.InputPreservationOption.DISCARD_INPUT, 1); return plan; } private static List<TableRowType> ancestors(RowType rowType, List<TableRowType> branchTables) { List<TableRowType> ancestors = new ArrayList<>(); for(TableRowType ancestor : branchTables) { if (ancestor.equals(rowType)) { return ancestors; } ancestors.add(ancestor); } throw new RuntimeException(rowType + "not found in " + branchTables); } private static PlanCreationInfo createGroupIndexMaintenancePlan(BranchTables branchTables, GroupIndex groupIndex, TableRowType rowType) { if (branchTables.isEmpty()) { throw new RuntimeException("group index has empty branch: " + groupIndex); } if (!branchTables.fromRoot().contains(rowType)) { throw new RuntimeException(rowType + " not in branch for " + groupIndex + ": " + branchTables); } PlanCreationInfo result = new PlanCreationInfo(rowType, groupIndex); Operator plan = API.groupScan_Default( groupIndex.getGroup(), HKEY_BINDING_POSITION, false, rowType.table(), branchTables.fromRoot().get(0).table() ); if (branchTables.fromRoot().size() == 1) { result.rootOperator = plan; return result; } if (!branchTables.leafMost().equals(rowType)) { // the incoming row isn't the leaf, so we have to get its ancestors along the branch List<TableRowType> children = branchTables.childrenOf(rowType); plan = API.groupLookup_Default( plan, groupIndex.getGroup(), rowType, children, API.InputPreservationOption.KEEP_INPUT, 1 ); } if (!branchTables.fromRoot().get(0).equals(rowType)) { plan = API.groupLookup_Default( plan, groupIndex.getGroup(), rowType, ancestors(rowType, branchTables.fromRoot()), API.InputPreservationOption.KEEP_INPUT, 1 ); } // RIGHT JOIN until the GI, and then the GI's join types RowType parentRowType = null; API.JoinType joinType = API.JoinType.RIGHT_JOIN; int branchStartDepth = branchTables.rootMost().table().getDepth() - 1; boolean withinBranch = branchStartDepth == -1; API.JoinType withinBranchJoin = operatorJoinType(groupIndex); result.incomingRowIsWithinGI = rowType.table().getDepth() >= branchTables.rootMost().table().getDepth(); for (TableRowType branchRowType : branchTables.fromRoot()) { boolean breakAtTop = result.incomingRowIsWithinGI && withinBranchJoin == API.JoinType.LEFT_JOIN; if (breakAtTop && branchRowType.equals(rowType)) { result.leftHalf = parentRowType; parentRowType = null; } if (parentRowType == null) { parentRowType = branchRowType; } else { plan = API.flatten_HKeyOrdered(plan, parentRowType, branchRowType, joinType); parentRowType = plan.rowType(); } if (branchRowType.table().getDepth() == branchStartDepth) { withinBranch = true; } else if (withinBranch) { joinType = withinBranchJoin; } if ( (!breakAtTop) && branchRowType.equals(rowType)) { result.leftHalf = parentRowType; parentRowType = null; } } result.rightHalf = parentRowType; if (result.leftHalf != null && result.rightHalf != null) { API.JoinType topJoinType = rowType.table().getDepth() <= branchTables.rootMost().table().getDepth() ? API.JoinType.RIGHT_JOIN : joinType; plan = API.flatten_HKeyOrdered(plan, result.leftHalf, result.rightHalf, topJoinType, KEEP_BOTH); result.topLevelFlattenType = (FlattenedRowType) plan.rowType(); } result.rootOperator = plan; return result; } private static final EnumSet<API.FlattenOption> KEEP_BOTH = EnumSet.of( API.FlattenOption.KEEP_PARENT, API.FlattenOption.KEEP_CHILD ); private static API.JoinType operatorJoinType(Index index) { switch (index.getJoinType()) { case LEFT: return API.JoinType.LEFT_JOIN; case RIGHT: return API.JoinType.RIGHT_JOIN; default: throw new AssertionError(index.getJoinType().name()); } } // package consts private static final int HKEY_BINDING_POSITION = 0; private static final InOutTap ALL_TAP = Tap.createTimer("GI maintenance: all"); private static final InOutTap RUN_TAP = Tap.createTimer("GI maintenance: run"); private static final InOutTap STORE_TAP = Tap.createTimer("GI maintenance: STORE"); private static final InOutTap DELETE_TAP = Tap.createTimer("GI maintenance: DELETE"); private static final InOutTap OTHER_TAP = Tap.createTimer("GI maintenance: OTHER"); private static final InOutTap SIBLING_ALL_TAP = Tap.createTimer("GI maintenance: sibling all"); private static final PointTap SIBLING_ROW_TAP = Tap.createCount("GI maintenance: sibling row"); private static final PointTap EXTRA_STORE_ROW_TAP = Tap.createCount("GI maintenance: extra store"); private static final PointTap EXTRA_DELETE_ROW_TAP = Tap.createCount("GI maintenance: extra delete"); private static final PointTap EXTRA_OTHER_ROW_TAP = Tap.createCount("GI maintenance: extra other"); // nested classes static class BranchTables { // BranchTables interface public List<TableRowType> fromRoot() { return allTablesForBranch; } public List<TableRowType> fromRootMost() { return onlyBranch; } public boolean isEmpty() { return fromRootMost().isEmpty(); } public TableRowType rootMost() { return onlyBranch.get(0); } public TableRowType leafMost() { return onlyBranch.get(onlyBranch.size()-1); } public List<TableRowType> childrenOf(TableRowType rowType) { int inputDepth = rowType.table().getDepth(); int childDepth = inputDepth + 1; return allTablesForBranch.subList(childDepth, allTablesForBranch.size()); } public TableRowType parentRowType(TableRowType rowType) { TableRowType parentType = null; for (TableRowType type : allTablesForBranch) { if (type.equals(rowType)) { return parentType; } parentType = type; } throw new IllegalArgumentException(rowType + " not in branch: " + allTablesForBranch); } public BranchTables(Schema schema, GroupIndex groupIndex) { List<TableRowType> localTables = new ArrayList<>(); Table rootmost = groupIndex.rootMostTable(); int branchRootmostIndex = -1; for (Table table = groupIndex.leafMostTable(); table != null; table = table.getParentTable()) { if (table.equals(rootmost)) { assert branchRootmostIndex == -1 : branchRootmostIndex; branchRootmostIndex = table.getDepth(); } localTables.add(schema.tableRowType(table)); } if (branchRootmostIndex < 0) { throw new RuntimeException("branch root not found! " + rootmost + " within " + localTables); } Collections.reverse(localTables); this.allTablesForBranch = Collections.unmodifiableList(localTables); this.onlyBranch = branchRootmostIndex == 0 ? allTablesForBranch : allTablesForBranch.subList(branchRootmostIndex, allTablesForBranch.size()); } // object state private final List<TableRowType> allTablesForBranch; private final List<TableRowType> onlyBranch; } static class PlanCreationInfo { public boolean noMaintenanceRequired() { return (!incomingRowIsWithinGI) && (incomingRowType.table().getDepth() == 0); } @Override public String toString() { return toString; } PlanCreationInfo(RowType forRow, GroupIndex forGi) { this.toString = String.format("for %s in %s", forRow, forGi.getIndexName().getName()); this.incomingRowType = forRow; } public final String toString; public final RowType incomingRowType; public Operator rootOperator; public FlattenedRowType topLevelFlattenType; public RowType leftHalf; public RowType rightHalf; public boolean incomingRowIsWithinGI; } }