/** * 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.ais.model; import com.foundationdb.ais.model.validation.AISInvariants; import com.foundationdb.server.error.BranchingGroupIndexException; import java.util.*; public class GroupIndex extends Index { // Index interface @Override public Table leafMostTable() { assert !tablesByDepth.isEmpty() : "no tables participate in this group index"; return tablesByDepth.lastEntry().getValue().table; } @Override public Table rootMostTable() { assert !tablesByDepth.isEmpty() : "no tables participate in this group index"; return tablesByDepth.firstEntry().getValue().table; } @Override public void checkMutability() { group.getRoot().checkMutability(); } @Override public Collection<Integer> getAllTableIDs() { List<Integer> branchIDs = new ArrayList<>(tablesByDepth.size()); for (Table table = leafMostTable(); table != null; table = table.getParentTable()) { branchIDs.add(table.getTableId()); } return branchIDs; } @Override public void addColumn(IndexColumn indexColumn) { Table indexTable = indexColumn.getColumn().getTable(); Integer indexTableDepth = indexTable.getDepth(); assert indexTableDepth != null; super.addColumn(indexColumn); GroupIndexHelper.actOnGroupIndexTables(this, indexColumn, GroupIndexHelper.ADD); // Add the table into our navigable map if needed. Confirm it's within the branch ParticipatingTable participatingTable = tablesByDepth.get(indexTableDepth); if (participatingTable == null) { Map.Entry<Integer, ParticipatingTable> rootwardEntry = tablesByDepth.floorEntry(indexTableDepth); Map.Entry<Integer, ParticipatingTable> leafwardEntry = tablesByDepth.ceilingEntry(indexTableDepth); checkIndexTableInBranchNew(indexColumn, indexTable, indexTableDepth, rootwardEntry, true); checkIndexTableInBranchNew(indexColumn, indexTable, indexTableDepth, leafwardEntry, false); participatingTable = new ParticipatingTable(indexTable); tablesByDepth.put(indexTableDepth, participatingTable); } else if (participatingTable.table != indexTable) { throw new BranchingGroupIndexException(indexColumn.getIndex().getIndexName().getName(), indexTable.getName(), participatingTable.table.getName()); } participatingTable.markInvolvedInIndex(indexColumn.getColumn()); } @Override public boolean isTableIndex() { return false; } @Override public void computeFieldAssociations(Map<Table, Integer> ordinalMap) { List<Table> branchTables = new ArrayList<>(); for (Table table = leafMostTable(); table != null; table = table.getParentTable()) { branchTables.add(table); } Collections.reverse(branchTables); Map<Table, Integer> offsetsMap = new HashMap<>(); int offset = 0; columnsPerFlattenedField = new ArrayList<>(); for (Table table : branchTables) { offsetsMap.put(table, offset); offset += table.getColumnsIncludingInternal().size(); columnsPerFlattenedField.addAll(table.getColumnsIncludingInternal()); } computeFieldAssociations(ordinalMap, offsetsMap); // Complete computation of inIndex bitsets for (ParticipatingTable participatingTable : tablesByDepth.values()) { participatingTable.close(); } } @Override public HKey hKey() { return leafMostTable().hKey(); } // GroupIndex interface // A row of the given table is being changed in the columns described by modifiedColumnPositions. // Return true iff there are any columns in common with those columns of the table contributing to the // index. A result of false means that the row change need not result in group index maintenance. public boolean columnsOverlap(Table table, BitSet modifiedColumnPositions) { ParticipatingTable participatingTable = tablesByDepth.get(table.getDepth()); if (participatingTable != null) { assert participatingTable.table == table; int n = modifiedColumnPositions.length(); for (int i = 0; i < n; i++) { if (modifiedColumnPositions.get(i) && participatingTable.inIndex.get(i)) { return true; } } return false; } else { // TODO: Can index maintenance be skipped in this case? return true; } } public Group getGroup() { return group; } public static GroupIndex create(AkibanInformationSchema ais, Group group, GroupIndex index) { GroupIndex copy = create(ais, group, index.getIndexName().getName(), index.getIndexId(), index.isUnique(), index.isPrimaryKey(), index.getJoinType()); if (index.isSpatial()) { copy.markSpatial(index.firstSpatialArgument(), index.spatialColumns(), index.getIndexMethod()); } return copy; } public static GroupIndex create(AkibanInformationSchema ais, Group group, String indexName, Integer indexId, Boolean isUnique, Boolean isPrimary, JoinType joinType) { return create(ais, group, indexName, indexId, isUnique, isPrimary, null, joinType); } public static GroupIndex create(AkibanInformationSchema ais, Group group, String indexName, Integer indexId, Boolean isUnique, Boolean isPrimary, TableName constraintName, JoinType joinType) { ais.checkMutability(); if(constraintName != null) { throw new IllegalArgumentException("Group indexes are never constraints"); } AISInvariants.checkDuplicateIndexesInGroup(group, indexName); GroupIndex index = new GroupIndex(group, indexName, indexId, isUnique, isPrimary, joinType); group.addIndex(index); return index; } private GroupIndex(Group group, String indexName, Integer indexId, Boolean isUnique, Boolean isPrimary, JoinType joinType) { super(group.getName(), indexName, indexId, isUnique, isPrimary, null, joinType); this.group = group; } public Column getColumnForFlattenedRow(int fieldIndex) { return columnsPerFlattenedField.get(fieldIndex); } public IndexToHKey indexToHKey(int tableDepth) { if (tableDepth > leafMostTable().getDepth()) { throw new IllegalArgumentException(Integer.toString(tableDepth)); } return indexToHKeys[tableDepth]; } public void disassociate() { GroupIndexHelper.actOnGroupIndexTables(this, GroupIndexHelper.REMOVE); } // For use by this class private void computeFieldAssociations(Map<Table, Integer> ordinalMap, Map<? extends Table, Integer> flattenedRowOffsets) { freezeColumns(); allColumns = new ArrayList<>(); allColumns.addAll(keyColumns); AssociationBuilder toIndexRowBuilder = new AssociationBuilder(); List<Column> indexColumns = new ArrayList<>(); // Add index key fields for (IndexColumn iColumn : getKeyColumns()) { Column column = iColumn.getColumn(); indexColumns.add(column); toIndexRowBuilder.rowCompEntry(columnPosition(flattenedRowOffsets, column), -1); } // Add hkey fields not already included int indexColumnPosition = indexColumns.size(); HKey hKey = hKey(); for (HKeySegment hKeySegment : hKey.segments()) { Integer ordinal = ordinalMap.get(hKeySegment.table()); assert ordinal != null : hKeySegment.table(); for (HKeyColumn hKeyColumn : hKeySegment.columns()) { Column undeclaredHKeyColumn = undeclaredHKeyColumn(hKeyColumn); if (!indexColumns.contains(undeclaredHKeyColumn)) { toIndexRowBuilder.rowCompEntry(columnPosition(flattenedRowOffsets, undeclaredHKeyColumn), -1); indexColumns.add(undeclaredHKeyColumn); allColumns.add(new IndexColumn(this, undeclaredHKeyColumn, indexColumnPosition++, true, 0)); } } } indexRowComposition = toIndexRowBuilder.createIndexRowComposition(); computeHKeyDerivations(ordinalMap); } private void computeHKeyDerivations(Map<Table, Integer> ordinalMap) { indexToHKeys = new IndexToHKey[leafMostTable().getDepth() + 1]; Table table = leafMostTable(); while (table != null) { int tableDepth = table.getDepth(); assert tableDepth <= leafMostTable().getDepth() : table; AssociationBuilder hKeyBuilder = new AssociationBuilder(); HKey hKey = table.hKey(); for (HKeySegment hKeySegment : hKey.segments()) { hKeyBuilder.toHKeyEntry(ordinalMap.get(hKeySegment.table()), -1); for (HKeyColumn hKeyColumn : hKeySegment.columns()) { int indexColumnPosition = positionOf(hKeyColumn.column()); if (indexColumnPosition == -1) { indexColumnPosition = substituteHKeyColumnPosition(hKeyColumn); } hKeyBuilder.toHKeyEntry(-1, indexColumnPosition); } } indexToHKeys[tableDepth] = hKeyBuilder.createIndexToHKey(); table = table.getParentTable(); } } private Column undeclaredHKeyColumn(HKeyColumn hKeyColumn) { Column undeclaredHKeyColumn = null; int rootMostDepth = rootMostTable().getDepth(); List<Column> equivalentColumns = hKeyColumn.equivalentColumns(); switch (getJoinType()) { case LEFT: // use a rootward bias, but no more rootward than the rootmost table for (Column equivalentColumn : equivalentColumns) { int equivalentColumnDepth = equivalentColumn.getTable().getDepth(); if (undeclaredHKeyColumn == null && equivalentColumnDepth >= rootMostDepth) { undeclaredHKeyColumn = equivalentColumn; } } break; case RIGHT: // use a leafward bias, but no more leafward than the leafdmost table int leafMostDepth = leafMostTable().getDepth(); for(ListIterator<Column> reverseCols = equivalentColumns.listIterator(equivalentColumns.size()); reverseCols.hasPrevious();) { Column equivalentColumn = reverseCols.previous(); int equivalentColumnDepth = equivalentColumn.getTable().getDepth(); if (undeclaredHKeyColumn == null && equivalentColumnDepth <= leafMostDepth) { undeclaredHKeyColumn = equivalentColumn; } } break; } if (undeclaredHKeyColumn == null) { undeclaredHKeyColumn = hKeyColumn.column(); } return undeclaredHKeyColumn; } private int substituteHKeyColumnPosition(HKeyColumn hKeyColumn) { int substituteHKeyColumnPosition = -1; // Given an hkey row, we need to construct an hkey for some table, either a table covered by the index // or some ancestor table. hKeyColumn is an hkey column of that table. If we're here, then // hKeyColumn.column() cannot be obtained from the index row itself, so the question is: which of the // hKeyColumn's equivalent columns should be used. // - If the hkey column is above the root: Use the rootmost equivalent column of the hkey columns in the index // row. // - Otherwise the hkey column belongs to a table covered by the index. // - For a left join index, use the nearest rootward equivalent column. // - For a right join index, use the nearest leafward equivalent column. List<Column> equivalentColumns = hKeyColumn.equivalentColumns(); // sorted by depth, root first Integer targetTableDepth = hKeyColumn.column().getTable().getDepth(); if (targetTableDepth < rootMostTable().getDepth()) { for (int i = 0; substituteHKeyColumnPosition == -1 && i < equivalentColumns.size(); i++) { Column equivalentColumn = equivalentColumns.get(i); substituteHKeyColumnPosition = positionOf(equivalentColumn); } } else { switch (getJoinType()) { case LEFT: for (int i = 0; i < equivalentColumns.size(); i++) { Column equivalentColumn = equivalentColumns.get(i); int equivalentColumnPosition = positionOf(equivalentColumn); if (equivalentColumnPosition != -1 && depth(equivalentColumn) < targetTableDepth) { substituteHKeyColumnPosition = equivalentColumnPosition; } } break; case RIGHT: for (int i = equivalentColumns.size() - 1; i >= 0; i--) { Column equivalentColumn = equivalentColumns.get(i); int equivalentColumnPosition = positionOf(equivalentColumn); if (equivalentColumnPosition != -1 && depth(equivalentColumn) > targetTableDepth) { substituteHKeyColumnPosition = equivalentColumnPosition; } } break; } } assert substituteHKeyColumnPosition != -1 : hKeyColumn; return substituteHKeyColumnPosition; } private int depth(Column column) { return column.getTable().getDepth(); } private int positionOf(Column column) { for (IndexColumn indexColumn : allColumns) { if (indexColumn.getColumn() == column) { return indexColumn.getPosition(); } } return -1; } private static int columnPosition(Map<? extends Table, Integer> flattenedRowOffsets, Column column) { int position = column.getPosition(); Integer offset = flattenedRowOffsets.get(column.getTable()); if (offset == null) { throw new NullPointerException("no offset for " + column.getTable() + " in " + flattenedRowOffsets); } position += offset; return position; } private void checkIndexTableInBranchNew(IndexColumn indexColumn, Table indexTable, int indexTableDepth, Map.Entry<Integer, ParticipatingTable> entry, boolean entryIsRootward) { if (entry == null) { return; } if (entry.getKey() == indexTableDepth) { throw new BranchingGroupIndexException(indexColumn.getIndex().getIndexName().getName(), indexTable.getName(), entry.getValue().table.getName()); } Table entryTable = entry.getValue().table; final Table rootward; final Table leafward; if (entryIsRootward) { assert entry.getKey() < indexTableDepth : String.format("failed %d < %d", entry.getKey(), indexTableDepth); rootward = entryTable; leafward = indexTable; } else { assert entry.getKey() > indexTableDepth : String.format("failed %d < %d", entry.getKey(), indexTableDepth); rootward = indexTable; leafward = entryTable; } if (!leafward.isDescendantOf(rootward)) { throw new BranchingGroupIndexException(indexColumn.getIndex().getIndexName().getName(), indexTable.getName(), entry.getValue().table.getName()); } } // Object state private final Group group; private final NavigableMap<Integer, ParticipatingTable> tablesByDepth = new TreeMap<>(); private List<Column> columnsPerFlattenedField; private IndexToHKey[] indexToHKeys; // Inner classes private static class ParticipatingTable { public void markInvolvedInIndex(Column column) { assert column.getTable() == table; inIndex.set(column.getPosition(), true); } public void close() { for (Column pkColumn : table.getPrimaryKeyIncludingInternal().getColumns()) { inIndex.set(pkColumn.getPosition(), true); } if (table.getParentJoin() != null) { for (JoinColumn joinColumn : table.getParentJoin().getJoinColumns()) { inIndex.set(joinColumn.getChild().getPosition(), true); } } } public ParticipatingTable(Table table) { this.table = table; this.inIndex = new BitSet(table.getColumnsIncludingInternal().size()); } // The table participating in the group index final Table table; // The columns of the table that contribute to the group index key or value. This includes PK columns, // FK columns, and any columns declared in the key. The PK and FK columns may not always be necessary, as // the logic here does not account for whether the index includes the leafward or rootward side of an FK. // As a result, we may decide to do index maintenance when it could otherwise be safely avoided. final BitSet inIndex; } }