/* * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, * Version 1.0, and under the Eclipse Public License, Version 1.0 * (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.index; import java.util.ArrayList; import org.h2.engine.Database; import org.h2.engine.DbObject; import org.h2.engine.Session; import org.h2.message.DbException; import org.h2.result.Row; import org.h2.result.SearchRow; import org.h2.schema.Schema; import org.h2.table.Column; import org.h2.table.IndexColumn; import org.h2.table.RegularTable; import org.h2.table.Table; import org.h2.table.TableFilter; import org.h2.value.Value; import org.h2.value.ValueNull; /** * A multi-version index is a combination of a regular index, * and a in-memory tree index that contains uncommitted changes. * Uncommitted changes can include new rows, and deleted rows. */ public class MultiVersionIndex implements Index { private final Index base; private final TreeIndex delta; private final RegularTable table; private final Object sync; private final Column firstColumn; public MultiVersionIndex(Index base, RegularTable table) { this.base = base; this.table = table; IndexType deltaIndexType = IndexType.createNonUnique(false); this.delta = new TreeIndex(table, -1, "DELTA", base.getIndexColumns(), deltaIndexType); delta.setMultiVersion(true); this.sync = base.getDatabase(); this.firstColumn = base.getColumns()[0]; } public void add(Session session, Row row) { synchronized (sync) { base.add(session, row); if (removeIfExists(session, row)) { // for example rolling back an delete operation } else if (row.getSessionId() != 0) { // don't insert rows that are added when creating an index delta.add(session, row); } } } public void close(Session session) { synchronized (sync) { base.close(session); } } public Cursor find(TableFilter filter, SearchRow first, SearchRow last) { synchronized (sync) { Cursor baseCursor = base.find(filter, first, last); Cursor deltaCursor = delta.find(filter, first, last); return new MultiVersionCursor(filter.getSession(), this, baseCursor, deltaCursor, sync); } } public Cursor find(Session session, SearchRow first, SearchRow last) { synchronized (sync) { Cursor baseCursor = base.find(session, first, last); Cursor deltaCursor = delta.find(session, first, last); return new MultiVersionCursor(session, this, baseCursor, deltaCursor, sync); } } public Cursor findNext(Session session, SearchRow first, SearchRow last) { throw DbException.throwInternalError(); } public boolean canFindNext() { // TODO possible, but more complicated return false; } public boolean canGetFirstOrLast() { return base.canGetFirstOrLast() && delta.canGetFirstOrLast(); } public Cursor findFirstOrLast(Session session, boolean first) { if (first) { // TODO optimization: this loops through NULL elements Cursor cursor = find(session, null, null); while (cursor.next()) { SearchRow row = cursor.getSearchRow(); Value v = row.getValue(firstColumn.getColumnId()); if (v != ValueNull.INSTANCE) { return cursor; } } return cursor; } Cursor baseCursor = base.findFirstOrLast(session, false); Cursor deltaCursor = delta.findFirstOrLast(session, false); MultiVersionCursor cursor = new MultiVersionCursor(session, this, baseCursor, deltaCursor, sync); cursor.loadCurrent(); // TODO optimization: this loops through NULL elements while (cursor.previous()) { SearchRow row = cursor.getSearchRow(); if (row == null) { break; } Value v = row.getValue(firstColumn.getColumnId()); if (v != ValueNull.INSTANCE) { return cursor; } } return cursor; } public double getCost(Session session, int[] masks) { return base.getCost(session, masks); } public boolean needRebuild() { return base.needRebuild(); } /** * Check if there is an uncommitted row with the given key * within a different session. * * @param session the original session * @param row the row (only the key is checked) * @return true if there is an uncommitted row */ public boolean isUncommittedFromOtherSession(Session session, Row row) { Cursor c = delta.find(session, row, row); while (c.next()) { Row r = c.get(); return r.getSessionId() != session.getId(); } return false; } private boolean removeIfExists(Session session, Row row) { // maybe it was inserted by the same session just before Cursor c = delta.find(session, row, row); while (c.next()) { Row r = c.get(); if (r.getKey() == row.getKey() && r.getVersion() == row.getVersion()) { if (r != row && table.getScanIndex(session).compareRows(r, row) != 0) { row.setVersion(r.getVersion() + 1); } else { delta.remove(session, r); return true; } } } return false; } public void remove(Session session, Row row) { synchronized (sync) { base.remove(session, row); if (removeIfExists(session, row)) { // added and deleted in the same transaction: no change } else { delta.add(session, row); } } } public void remove(Session session) { synchronized (sync) { base.remove(session); } } public void truncate(Session session) { synchronized (sync) { delta.truncate(session); base.truncate(session); } } public void commit(int operation, Row row) { synchronized (sync) { removeIfExists(null, row); } } public int compareRows(SearchRow rowData, SearchRow compare) { return base.compareRows(rowData, compare); } public int getColumnIndex(Column col) { return base.getColumnIndex(col); } public Column[] getColumns() { return base.getColumns(); } public IndexColumn[] getIndexColumns() { return base.getIndexColumns(); } public String getCreateSQL() { return base.getCreateSQL(); } public String getCreateSQLForCopy(Table forTable, String quotedName) { return base.getCreateSQLForCopy(forTable, quotedName); } public String getDropSQL() { return base.getDropSQL(); } public IndexType getIndexType() { return base.getIndexType(); } public String getPlanSQL() { return base.getPlanSQL(); } public long getRowCount(Session session) { return base.getRowCount(session); } public Table getTable() { return base.getTable(); } public int getType() { return base.getType(); } public void removeChildrenAndResources(Session session) { synchronized (sync) { table.removeIndex(this); remove(session); } } public String getSQL() { return base.getSQL(); } public Schema getSchema() { return base.getSchema(); } public void checkRename() { base.checkRename(); } public ArrayList<DbObject> getChildren() { return base.getChildren(); } public String getComment() { return base.getComment(); } public Database getDatabase() { return base.getDatabase(); } public int getId() { return base.getId(); } public String getName() { return base.getName(); } public boolean isTemporary() { return base.isTemporary(); } public void rename(String newName) { base.rename(newName); } public void setComment(String comment) { base.setComment(comment); } public void setTemporary(boolean temporary) { base.setTemporary(temporary); } public long getRowCountApproximation() { return base.getRowCountApproximation(); } public Index getBaseIndex() { return base; } public Row getRow(Session session, long key) { return base.getRow(session, key); } public boolean isHidden() { return base.isHidden(); } public boolean isRowIdIndex() { return base.isRowIdIndex() && delta.isRowIdIndex(); } public boolean canScan() { return base.canScan(); } public void setSortedInsertMode(boolean sortedInsertMode) { base.setSortedInsertMode(sortedInsertMode); delta.setSortedInsertMode(sortedInsertMode); } }