com.healthmarketscience.jackcess.IndexData.java Source code

Java tutorial

Introduction

Here is the source code for com.healthmarketscience.jackcess.IndexData.java

Source

/*
Copyright (c) 2005 Health Market Science, Inc.
    
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
    
This library 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
Lesser General Public License for more details.
    
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
USA
    
You can contact Health Market Science at info@healthmarketscience.com
or at the following address:
    
Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import static com.healthmarketscience.jackcess.IndexCodes.*;
import static com.healthmarketscience.jackcess.ByteUtil.ByteStream;

/**
 * Access table index data.  This is the actual data which backs a logical
 * Index, where one or more logical indexes can be backed by the same index
 * data.
 * 
 * @author Tim McCune
 */
public abstract class IndexData {

    protected static final Log LOG = LogFactory.getLog(Index.class);

    /** special entry which is less than any other entry */
    public static final Entry FIRST_ENTRY = createSpecialEntry(RowId.FIRST_ROW_ID);

    /** special entry which is greater than any other entry */
    public static final Entry LAST_ENTRY = createSpecialEntry(RowId.LAST_ROW_ID);

    /** special object which will always be greater than any other value, when
        searching for an index entry range in a multi-value index */
    public static final Object MAX_VALUE = new Object();

    /** special object which will always be greater than any other value, when
        searching for an index entry range in a multi-value index */
    public static final Object MIN_VALUE = new Object();

    protected static final int INVALID_INDEX_PAGE_NUMBER = 0;

    /** Max number of columns in an index */
    static final int MAX_COLUMNS = 10;

    protected static final byte[] EMPTY_PREFIX = new byte[0];

    private static final short COLUMN_UNUSED = -1;

    static final byte ASCENDING_COLUMN_FLAG = (byte) 0x01;

    static final byte UNIQUE_INDEX_FLAG = (byte) 0x01;
    static final byte IGNORE_NULLS_INDEX_FLAG = (byte) 0x02;
    static final byte SPECIAL_INDEX_FLAG = (byte) 0x08; // set on MSysACEs and MSysAccessObjects indexes, purpose unknown
    static final byte UNKNOWN_INDEX_FLAG = (byte) 0x80; // always seems to be set on indexes in access 2000+

    private static final int MAGIC_INDEX_NUMBER = 1923;

    private static final ByteOrder ENTRY_BYTE_ORDER = ByteOrder.BIG_ENDIAN;

    /** type attributes for Entries which simplify comparisons */
    public enum EntryType {
        /** comparable type indicating this Entry should always compare less than
            valid RowIds */
        ALWAYS_FIRST,
        /** comparable type indicating this Entry should always compare less than
            other valid entries with equal entryBytes */
        FIRST_VALID,
        /** comparable type indicating this RowId should always compare
            normally */
        NORMAL,
        /** comparable type indicating this Entry should always compare greater
            than other valid entries with equal entryBytes */
        LAST_VALID,
        /** comparable type indicating this Entry should always compare greater
            than valid RowIds */
        ALWAYS_LAST;
    }

    static final Comparator<byte[]> BYTE_CODE_COMPARATOR = new Comparator<byte[]>() {
        public int compare(byte[] left, byte[] right) {
            if (left == right) {
                return 0;
            }
            if (left == null) {
                return -1;
            }
            if (right == null) {
                return 1;
            }

            int len = Math.min(left.length, right.length);
            int pos = 0;
            while ((pos < len) && (left[pos] == right[pos])) {
                ++pos;
            }
            if (pos < len) {
                return ((ByteUtil.asUnsignedByte(left[pos]) < ByteUtil.asUnsignedByte(right[pos])) ? -1 : 1);
            }
            return ((left.length < right.length) ? -1 : ((left.length > right.length) ? 1 : 0));
        }
    };

    /** owning table */
    private final Table _table;
    /** 0-based index data number */
    private final int _number;
    /** Page number of the root index data */
    private int _rootPageNumber;
    /** offset within the tableDefinition buffer of the uniqueEntryCount for
        this index */
    private final int _uniqueEntryCountOffset;
    /** The number of unique entries which have been added to this index.  note,
        however, that it is never decremented, only incremented (as observed in
        Access). */
    private int _uniqueEntryCount;
    /** List of columns and flags */
    private final List<ColumnDescriptor> _columns = new ArrayList<ColumnDescriptor>();
    /** the logical indexes which this index data backs */
    private final List<Index> _indexes = new ArrayList<Index>();
    /** flags for this index */
    private byte _indexFlags;
    /** Usage map of pages that this index owns */
    private UsageMap _ownedPages;
    /** <code>true</code> if the index entries have been initialized,
        <code>false</code> otherwise */
    private boolean _initialized;
    /** modification count for the table, keeps cursors up-to-date */
    private int _modCount;
    /** temp buffer used to read/write the index pages */
    private final TempBufferHolder _indexBufferH = TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
    /** temp buffer used to create index entries */
    private ByteStream _entryBuffer;
    /** max size for all the entries written to a given index data page */
    private final int _maxPageEntrySize;
    /** whether or not this index data is backing a primary key logical index */
    private boolean _primaryKey;
    /** FIXME, for SimpleIndex, we can't write multi-page indexes or indexes using the entry compression scheme */
    private boolean _readOnly;

    protected IndexData(Table table, int number, int uniqueEntryCount, int uniqueEntryCountOffset) {
        _table = table;
        _number = number;
        _uniqueEntryCount = uniqueEntryCount;
        _uniqueEntryCountOffset = uniqueEntryCountOffset;
        _maxPageEntrySize = calcMaxPageEntrySize(_table.getFormat());
    }

    /**
     * Creates an IndexData appropriate for the given table, using information
     * from the given table definition buffer.
     */
    public static IndexData create(Table table, ByteBuffer tableBuffer, int number, JetFormat format)
            throws IOException {
        int uniqueEntryCountOffset = (format.OFFSET_INDEX_DEF_BLOCK + (number * format.SIZE_INDEX_DEFINITION) + 4);
        int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset);

        return (table.doUseBigIndex() ? new BigIndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset)
                : new SimpleIndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset));
    }

    public Table getTable() {
        return _table;
    }

    public JetFormat getFormat() {
        return getTable().getFormat();
    }

    public PageChannel getPageChannel() {
        return getTable().getPageChannel();
    }

    /**
     * @return the "main" logical index which is backed by this data.
     */
    public Index getPrimaryIndex() {
        return _indexes.get(0);
    }

    /**
     * @return All of the Indexes backed by this data (unmodifiable List)
     */
    public List<Index> getIndexes() {
        return Collections.unmodifiableList(_indexes);
    }

    /**
     * Adds a logical index which this data is backing.
     */
    void addIndex(Index index) {

        // we keep foreign key indexes at the back of the list.  this way the
        // primary index will be a non-foreign key index (if any)
        if (index.isForeignKey()) {
            _indexes.add(index);
        } else {
            int pos = _indexes.size();
            while (pos > 0) {
                if (!_indexes.get(pos - 1).isForeignKey()) {
                    break;
                }
                --pos;
            }
            _indexes.add(pos, index);

            // also, keep track of whether or not this is a primary key index
            _primaryKey |= index.isPrimaryKey();
        }
    }

    public byte getIndexFlags() {
        return _indexFlags;
    }

    public int getIndexDataNumber() {
        return _number;
    }

    public int getUniqueEntryCount() {
        return _uniqueEntryCount;
    }

    public int getUniqueEntryCountOffset() {
        return _uniqueEntryCountOffset;
    }

    protected boolean isBackingPrimaryKey() {
        return _primaryKey;
    }

    /**
     * Whether or not {@code null} values are actually recorded in the index.
     */
    public boolean shouldIgnoreNulls() {
        return ((_indexFlags & IGNORE_NULLS_INDEX_FLAG) != 0);
    }

    /**
     * Whether or not index entries must be unique.
     * <p>
     * Some notes about uniqueness:
     * <ul>
     * <li>Access does not seem to consider multiple {@code null} entries
     *     invalid for a unique index</li>
     * <li>text indexes collapse case, and Access seems to compare <b>only</b>
     *     the index entry bytes, therefore two strings which differ only in
     *     case <i>will violate</i> the unique constraint</li>
     * </ul>
     */
    public boolean isUnique() {
        return (isBackingPrimaryKey() || ((_indexFlags & UNIQUE_INDEX_FLAG) != 0));
    }

    /**
     * Returns the Columns for this index (unmodifiable)
     */
    public List<ColumnDescriptor> getColumns() {
        return Collections.unmodifiableList(_columns);
    }

    /**
     * Whether or not the complete index state has been read.
     */
    public boolean isInitialized() {
        return _initialized;
    }

    protected int getRootPageNumber() {
        return _rootPageNumber;
    }

    protected void setReadOnly() {
        _readOnly = true;
    }

    protected boolean isReadOnly() {
        return _readOnly;
    }

    protected int getMaxPageEntrySize() {
        return _maxPageEntrySize;
    }

    /**
     * Returns the number of database pages owned by this index data.
     */
    public int getOwnedPageCount() {
        return _ownedPages.getPageCount();
    }

    void addOwnedPage(int pageNumber) throws IOException {
        _ownedPages.addPageNumber(pageNumber);
    }

    /**
     * Returns the number of index entries in the index.  Only called by unit
     * tests.
     * <p>
     * Forces index initialization.
     */
    protected int getEntryCount() throws IOException {
        initialize();
        EntryCursor cursor = cursor();
        Entry endEntry = cursor.getLastEntry();
        int count = 0;
        while (!endEntry.equals(cursor.getNextEntry())) {
            ++count;
        }
        return count;
    }

    /**
     * Forces initialization of this index (actual parsing of index pages).
     * normally, the index will not be initialized until the entries are
     * actually needed.
     */
    public void initialize() throws IOException {
        if (!_initialized) {
            readIndexEntries();
            _initialized = true;
        }
    }

    /**
     * Writes the current index state to the database.
     * <p>
     * Forces index initialization.
     */
    public void update() throws IOException {
        // make sure we've parsed the entries
        initialize();

        if (_readOnly) {
            throw new UnsupportedOperationException(
                    "FIXME cannot write indexes of this type yet, see Database javadoc for info on enabling large index support");
        }
        updateImpl();
    }

    /**
     * Read the rest of the index info from a tableBuffer
     * @param tableBuffer table definition buffer to read from initial info
     * @param availableColumns Columns that this index may use
     */
    public void read(ByteBuffer tableBuffer, List<Column> availableColumns) throws IOException {
        ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown

        for (int i = 0; i < MAX_COLUMNS; i++) {
            short columnNumber = tableBuffer.getShort();
            byte colFlags = tableBuffer.get();
            if (columnNumber != COLUMN_UNUSED) {
                // find the desired column by column number (which is not necessarily
                // the same as the column index)
                Column idxCol = null;
                for (Column col : availableColumns) {
                    if (col.getColumnNumber() == columnNumber) {
                        idxCol = col;
                        break;
                    }
                }
                if (idxCol == null) {
                    throw new IOException("Could not find column with number " + columnNumber + " for index");
                }
                _columns.add(newColumnDescriptor(idxCol, colFlags));
            }
        }

        int umapRowNum = tableBuffer.get();
        int umapPageNum = ByteUtil.get3ByteInt(tableBuffer);
        _ownedPages = UsageMap.read(getTable().getDatabase(), umapPageNum, umapRowNum, false);

        _rootPageNumber = tableBuffer.getInt();

        ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown
        _indexFlags = tableBuffer.get();
        ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_FLAGS); //Forward past other stuff
    }

    /**
     * Writes the index row count definitions into a table definition buffer.
     * @param buffer Buffer to write to
     * @param indexes List of IndexBuilders to write definitions for
     */
    protected static void writeRowCountDefinitions(TableCreator creator, ByteBuffer buffer) {
        // index row counts (empty data)
        ByteUtil.forward(buffer, (creator.getIndexCount() * creator.getFormat().SIZE_INDEX_DEFINITION));
    }

    /**
     * Writes the index definitions into a table definition buffer.
     * @param buffer Buffer to write to
     * @param indexes List of IndexBuilders to write definitions for
     */
    protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer) throws IOException {
        ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer();
        writeDataPage(rootPageBuffer, SimpleIndexData.NEW_ROOT_DATA_PAGE, creator.getTdefPageNumber(),
                creator.getFormat());

        for (IndexBuilder idx : creator.getIndexes()) {
            buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value

            // write column information (always MAX_COLUMNS entries)
            List<IndexBuilder.Column> idxColumns = idx.getColumns();
            for (int i = 0; i < MAX_COLUMNS; ++i) {

                short columnNumber = COLUMN_UNUSED;
                byte flags = 0;

                if (i < idxColumns.size()) {

                    // determine column info
                    IndexBuilder.Column idxCol = idxColumns.get(i);
                    flags = idxCol.getFlags();

                    // find actual table column number
                    for (Column col : creator.getColumns()) {
                        if (col.getName().equalsIgnoreCase(idxCol.getName())) {
                            columnNumber = col.getColumnNumber();
                            break;
                        }
                    }
                    if (columnNumber == COLUMN_UNUSED) {
                        // should never happen as this is validated before
                        throw new IllegalArgumentException("Column with name " + idxCol.getName() + " not found");
                    }
                }

                buffer.putShort(columnNumber); // table column number
                buffer.put(flags); // column flags (e.g. ordering)
            }

            TableCreator.IndexState idxState = creator.getIndexState(idx);

            buffer.put(idxState.getUmapRowNumber()); // umap row
            ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); // umap page

            // write empty root index page
            creator.getPageChannel().writePage(rootPageBuffer, idxState.getRootPageNumber());

            buffer.putInt(idxState.getRootPageNumber());
            buffer.putInt(0); // unknown
            buffer.put(idx.getFlags()); // index flags (unique, etc.)
            ByteUtil.forward(buffer, 5); // unknown
        }
    }

    /**
     * Adds a row to this index
     * <p>
     * Forces index initialization.
     * 
     * @param row Row to add
     * @param rowId rowId of the row to be added
     */
    public void addRow(Object[] row, RowId rowId) throws IOException {
        int nullCount = countNullValues(row);
        boolean isNullEntry = (nullCount == _columns.size());
        if (shouldIgnoreNulls() && isNullEntry) {
            // nothing to do
            return;
        }
        if (isBackingPrimaryKey() && (nullCount > 0)) {
            throw new IOException(
                    "Null value found in row " + Arrays.asList(row) + " for primary key index " + this);
        }

        // make sure we've parsed the entries
        initialize();

        Entry newEntry = new Entry(createEntryBytes(row), rowId);
        if (addEntry(newEntry, isNullEntry, row)) {
            ++_modCount;
        } else {
            LOG.warn("Added duplicate index entry " + newEntry + " for row: " + Arrays.asList(row));
        }
    }

    /**
     * Adds an entry to the correct index dataPage, maintaining the order.
     */
    private boolean addEntry(Entry newEntry, boolean isNullEntry, Object[] row) throws IOException {
        DataPage dataPage = findDataPage(newEntry);
        int idx = dataPage.findEntry(newEntry);
        if (idx < 0) {
            // this is a new entry
            idx = missingIndexToInsertionPoint(idx);

            Position newPos = new Position(dataPage, idx, newEntry, true);
            Position nextPos = getNextPosition(newPos);
            Position prevPos = getPreviousPosition(newPos);

            // determine if the addition of this entry would break the uniqueness
            // constraint.  See isUnique() for some notes about uniqueness as
            // defined by Access.
            boolean isDupeEntry = (((nextPos != null) && newEntry.equalsEntryBytes(nextPos.getEntry()))
                    || ((prevPos != null) && newEntry.equalsEntryBytes(prevPos.getEntry())));
            if (isUnique() && !isNullEntry && isDupeEntry) {
                throw new IOException(
                        "New row " + Arrays.asList(row) + " violates uniqueness constraint for index " + this);
            }

            if (!isDupeEntry) {
                ++_uniqueEntryCount;
            }

            dataPage.addEntry(idx, newEntry);
            return true;
        }
        return false;
    }

    /**
     * Removes a row from this index
     * <p>
     * Forces index initialization.
     * 
     * @param row Row to remove
     * @param rowId rowId of the row to be removed
     */
    public void deleteRow(Object[] row, RowId rowId) throws IOException {
        int nullCount = countNullValues(row);
        if (shouldIgnoreNulls() && (nullCount == _columns.size())) {
            // nothing to do
            return;
        }

        // make sure we've parsed the entries
        initialize();

        Entry oldEntry = new Entry(createEntryBytes(row), rowId);
        if (removeEntry(oldEntry)) {
            ++_modCount;
        } else {
            LOG.warn("Failed removing index entry " + oldEntry + " for row: " + Arrays.asList(row));
        }
    }

    /**
     * Removes an entry from the relevant index dataPage, maintaining the order.
     * Will search by RowId if entry is not found (in case a partial entry was
     * provided).
     */
    private boolean removeEntry(Entry oldEntry) throws IOException {
        DataPage dataPage = findDataPage(oldEntry);
        int idx = dataPage.findEntry(oldEntry);
        boolean doRemove = false;
        if (idx < 0) {
            // the caller may have only read some of the row data, if this is the
            // case, just search for the page/row numbers
            // FIXME, we could force caller to get relevant values?
            EntryCursor cursor = cursor();
            Position tmpPos = null;
            Position endPos = cursor._lastPos;
            while (!endPos.equals(tmpPos = cursor.getAnotherPosition(Cursor.MOVE_FORWARD))) {
                if (tmpPos.getEntry().getRowId().equals(oldEntry.getRowId())) {
                    dataPage = tmpPos.getDataPage();
                    idx = tmpPos.getIndex();
                    doRemove = true;
                    break;
                }
            }
        } else {
            doRemove = true;
        }

        if (doRemove) {
            // found it!
            dataPage.removeEntry(idx);
        }

        return doRemove;
    }

    /**
     * Gets a new cursor for this index.
     * <p>
     * Forces index initialization.
     */
    public EntryCursor cursor() throws IOException {
        return cursor(null, true, null, true);
    }

    /**
     * Gets a new cursor for this index, narrowed to the range defined by the
     * given startRow and endRow.
     * <p>
     * Forces index initialization.
     * 
     * @param startRow the first row of data for the cursor, or {@code null} for
     *                 the first entry
     * @param startInclusive whether or not startRow is inclusive or exclusive
     * @param endRow the last row of data for the cursor, or {@code null} for
     *               the last entry
     * @param endInclusive whether or not endRow is inclusive or exclusive
     */
    public EntryCursor cursor(Object[] startRow, boolean startInclusive, Object[] endRow, boolean endInclusive)
            throws IOException {
        initialize();
        Entry startEntry = FIRST_ENTRY;
        byte[] startEntryBytes = null;
        if (startRow != null) {
            startEntryBytes = createEntryBytes(startRow);
            startEntry = new Entry(startEntryBytes, (startInclusive ? RowId.FIRST_ROW_ID : RowId.LAST_ROW_ID));
        }
        Entry endEntry = LAST_ENTRY;
        if (endRow != null) {
            // reuse startEntryBytes if startRow and endRow are same array.  this is
            // common for "lookup" code
            byte[] endEntryBytes = ((startRow == endRow) ? startEntryBytes : createEntryBytes(endRow));
            endEntry = new Entry(endEntryBytes, (endInclusive ? RowId.LAST_ROW_ID : RowId.FIRST_ROW_ID));
        }
        return new EntryCursor(findEntryPosition(startEntry), findEntryPosition(endEntry));
    }

    private Position findEntryPosition(Entry entry) throws IOException {
        DataPage dataPage = findDataPage(entry);
        int idx = dataPage.findEntry(entry);
        boolean between = false;
        if (idx < 0) {
            // given entry was not found exactly.  our current position is now
            // really between two indexes, but we cannot support that as an integer
            // value, so we set a flag instead
            idx = missingIndexToInsertionPoint(idx);
            between = true;
        }
        return new Position(dataPage, idx, entry, between);
    }

    private Position getNextPosition(Position curPos) throws IOException {
        // get the next index (between-ness is handled internally)
        int nextIdx = curPos.getNextIndex();
        Position nextPos = null;
        if (nextIdx < curPos.getDataPage().getEntries().size()) {
            nextPos = new Position(curPos.getDataPage(), nextIdx);
        } else {
            int nextPageNumber = curPos.getDataPage().getNextPageNumber();
            DataPage nextDataPage = null;
            while (nextPageNumber != INVALID_INDEX_PAGE_NUMBER) {
                DataPage dp = getDataPage(nextPageNumber);
                if (!dp.isEmpty()) {
                    nextDataPage = dp;
                    break;
                }
                nextPageNumber = dp.getNextPageNumber();
            }
            if (nextDataPage != null) {
                nextPos = new Position(nextDataPage, 0);
            }
        }
        return nextPos;
    }

    /**
     * Returns the Position before the given one, or {@code null} if none.
     */
    private Position getPreviousPosition(Position curPos) throws IOException {
        // get the previous index (between-ness is handled internally)
        int prevIdx = curPos.getPrevIndex();
        Position prevPos = null;
        if (prevIdx >= 0) {
            prevPos = new Position(curPos.getDataPage(), prevIdx);
        } else {
            int prevPageNumber = curPos.getDataPage().getPrevPageNumber();
            DataPage prevDataPage = null;
            while (prevPageNumber != INVALID_INDEX_PAGE_NUMBER) {
                DataPage dp = getDataPage(prevPageNumber);
                if (!dp.isEmpty()) {
                    prevDataPage = dp;
                    break;
                }
                prevPageNumber = dp.getPrevPageNumber();
            }
            if (prevDataPage != null) {
                prevPos = new Position(prevDataPage, (prevDataPage.getEntries().size() - 1));
            }
        }
        return prevPos;
    }

    /**
     * Returns the valid insertion point for an index indicating a missing
     * entry.
     */
    protected static int missingIndexToInsertionPoint(int idx) {
        return -(idx + 1);
    }

    /**
     * Constructs an array of values appropriate for this index from the given
     * column values, expected to match the columns for this index.
     * @return the appropriate sparse array of data
     * @throws IllegalArgumentException if the wrong number of values are
     *         provided
     */
    public Object[] constructIndexRowFromEntry(Object... values) {
        if (values.length != _columns.size()) {
            throw new IllegalArgumentException(
                    "Wrong number of column values given " + values.length + ", expected " + _columns.size());
        }
        int valIdx = 0;
        Object[] idxRow = new Object[getTable().getColumnCount()];
        for (ColumnDescriptor col : _columns) {
            idxRow[col.getColumnIndex()] = values[valIdx++];
        }
        return idxRow;
    }

    /**
     * Constructs an array of values appropriate for this index from the given
     * column value.
     * @return the appropriate sparse array of data or {@code null} if not all
     *         columns for this index were provided
     */
    public Object[] constructIndexRow(String colName, Object value) {
        return constructIndexRow(Collections.singletonMap(colName, value));
    }

    /**
     * Constructs an array of values appropriate for this index from the given
     * column values.
     * @return the appropriate sparse array of data or {@code null} if not all
     *         columns for this index were provided
     */
    public Object[] constructIndexRow(Map<String, ?> row) {
        for (ColumnDescriptor col : _columns) {
            if (!row.containsKey(col.getName())) {
                return null;
            }
        }

        Object[] idxRow = new Object[getTable().getColumnCount()];
        for (ColumnDescriptor col : _columns) {
            idxRow[col.getColumnIndex()] = row.get(col.getName());
        }
        return idxRow;
    }

    @Override
    public String toString() {
        StringBuilder rtn = new StringBuilder();
        rtn.append("\n\tData number: ").append(_number);
        rtn.append("\n\tPage number: ").append(_rootPageNumber);
        rtn.append("\n\tIs Backing Primary Key: ").append(isBackingPrimaryKey());
        rtn.append("\n\tIs Unique: ").append(isUnique());
        rtn.append("\n\tIgnore Nulls: ").append(shouldIgnoreNulls());
        rtn.append("\n\tColumns: ").append(_columns);
        rtn.append("\n\tInitialized: ").append(_initialized);
        if (_initialized) {
            try {
                rtn.append("\n\tEntryCount: ").append(getEntryCount());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return rtn.toString();
    }

    /**
     * Write the given index page out to a buffer
     */
    protected void writeDataPage(DataPage dataPage) throws IOException {
        if (dataPage.getCompressedEntrySize() > _maxPageEntrySize) {
            if (this instanceof SimpleIndexData) {
                throw new UnsupportedOperationException(
                        "FIXME cannot write large index yet, see Database javadoc for info on enabling large index support");
            }
            throw new IllegalStateException("data page is too large");
        }

        ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());

        writeDataPage(buffer, dataPage, getTable().getTableDefPageNumber(), getFormat());

        getPageChannel().writePage(buffer, dataPage.getPageNumber());
    }

    /**
     * Writes the data page info to the given buffer.
     */
    protected static void writeDataPage(ByteBuffer buffer, DataPage dataPage, int tdefPageNumber, JetFormat format)
            throws IOException {
        buffer.put(dataPage.isLeaf() ? PageTypes.INDEX_LEAF : PageTypes.INDEX_NODE); //Page type
        buffer.put((byte) 0x01); //Unknown
        buffer.putShort((short) 0); //Free space
        buffer.putInt(tdefPageNumber);

        buffer.putInt(0); //Unknown
        buffer.putInt(dataPage.getPrevPageNumber()); //Prev page
        buffer.putInt(dataPage.getNextPageNumber()); //Next page
        buffer.putInt(dataPage.getChildTailPageNumber()); //ChildTail page

        byte[] entryPrefix = dataPage.getEntryPrefix();
        buffer.putShort((short) entryPrefix.length); // entry prefix byte count
        buffer.put((byte) 0); //Unknown

        byte[] entryMask = new byte[format.SIZE_INDEX_ENTRY_MASK];
        // first entry includes the prefix
        int totalSize = entryPrefix.length;
        for (Entry entry : dataPage.getEntries()) {
            totalSize += (entry.size() - entryPrefix.length);
            int idx = totalSize / 8;
            entryMask[idx] |= (1 << (totalSize % 8));
        }
        buffer.put(entryMask);

        // first entry includes the prefix
        buffer.put(entryPrefix);

        for (Entry entry : dataPage.getEntries()) {
            entry.write(buffer, entryPrefix);
        }

        // update free space
        buffer.putShort(2, (short) (format.PAGE_SIZE - buffer.position()));
    }

    /**
     * Reads an index page, populating the correct collection based on the page
     * type (node or leaf).
     */
    protected void readDataPage(DataPage dataPage) throws IOException {
        ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
        getPageChannel().readPage(buffer, dataPage.getPageNumber());

        boolean isLeaf = isLeafPage(buffer);
        dataPage.setLeaf(isLeaf);

        // note, "header" data is in LITTLE_ENDIAN format, entry data is in
        // BIG_ENDIAN format
        int entryPrefixLength = ByteUtil.getUnsignedShort(buffer, getFormat().OFFSET_INDEX_COMPRESSED_BYTE_COUNT);
        int entryMaskLength = getFormat().SIZE_INDEX_ENTRY_MASK;
        int entryMaskPos = getFormat().OFFSET_INDEX_ENTRY_MASK;
        int entryPos = entryMaskPos + entryMaskLength;
        int lastStart = 0;
        int totalEntrySize = 0;
        byte[] entryPrefix = null;
        List<Entry> entries = new ArrayList<Entry>();
        TempBufferHolder tmpEntryBufferH = TempBufferHolder.newHolder(TempBufferHolder.Type.HARD, true,
                ENTRY_BYTE_ORDER);

        Entry prevEntry = FIRST_ENTRY;
        for (int i = 0; i < entryMaskLength; i++) {
            byte entryMask = buffer.get(entryMaskPos + i);
            for (int j = 0; j < 8; j++) {
                if ((entryMask & (1 << j)) != 0) {
                    int length = (i * 8) + j - lastStart;
                    buffer.position(entryPos + lastStart);

                    // determine if we can read straight from the index page (if no
                    // entryPrefix).  otherwise, create temp buf with complete entry.
                    ByteBuffer curEntryBuffer = buffer;
                    int curEntryLen = length;
                    if (entryPrefix != null) {
                        curEntryBuffer = getTempEntryBuffer(buffer, length, entryPrefix, tmpEntryBufferH);
                        curEntryLen += entryPrefix.length;
                    }
                    totalEntrySize += curEntryLen;

                    Entry entry = newEntry(curEntryBuffer, curEntryLen, isLeaf);
                    if (prevEntry.compareTo(entry) >= 0) {
                        throw new IOException("Unexpected order in index entries, " + prevEntry + " >= " + entry);
                    }

                    entries.add(entry);

                    if ((entries.size() == 1) && (entryPrefixLength > 0)) {
                        // read any shared entry prefix
                        entryPrefix = new byte[entryPrefixLength];
                        buffer.position(entryPos + lastStart);
                        buffer.get(entryPrefix);
                    }

                    lastStart += length;
                    prevEntry = entry;
                }
            }
        }

        dataPage.setEntryPrefix(entryPrefix != null ? entryPrefix : EMPTY_PREFIX);
        dataPage.setEntries(entries);
        dataPage.setTotalEntrySize(totalEntrySize);

        int prevPageNumber = buffer.getInt(getFormat().OFFSET_PREV_INDEX_PAGE);
        int nextPageNumber = buffer.getInt(getFormat().OFFSET_NEXT_INDEX_PAGE);
        int childTailPageNumber = buffer.getInt(getFormat().OFFSET_CHILD_TAIL_INDEX_PAGE);

        dataPage.setPrevPageNumber(prevPageNumber);
        dataPage.setNextPageNumber(nextPageNumber);
        dataPage.setChildTailPageNumber(childTailPageNumber);
    }

    /**
     * Returns a new Entry of the correct type for the given data and page type.
     */
    private static Entry newEntry(ByteBuffer buffer, int entryLength, boolean isLeaf) throws IOException {
        if (isLeaf) {
            return new Entry(buffer, entryLength);
        }
        return new NodeEntry(buffer, entryLength);
    }

    /**
     * Returns an entry buffer containing the relevant data for an entry given
     * the valuePrefix.
     */
    private ByteBuffer getTempEntryBuffer(ByteBuffer indexPage, int entryLen, byte[] valuePrefix,
            TempBufferHolder tmpEntryBufferH) {
        ByteBuffer tmpEntryBuffer = tmpEntryBufferH.getBuffer(getPageChannel(), valuePrefix.length + entryLen);

        // combine valuePrefix and rest of entry from indexPage, then prep for
        // reading
        tmpEntryBuffer.put(valuePrefix);
        tmpEntryBuffer.put(indexPage.array(), indexPage.position(), entryLen);
        tmpEntryBuffer.flip();

        return tmpEntryBuffer;
    }

    /**
     * Determines if the given index page is a leaf or node page.
     */
    private static boolean isLeafPage(ByteBuffer buffer) throws IOException {
        byte pageType = buffer.get(0);
        if (pageType == PageTypes.INDEX_LEAF) {
            return true;
        } else if (pageType == PageTypes.INDEX_NODE) {
            return false;
        }
        throw new IOException("Unexpected page type " + pageType);
    }

    /**
     * Determines the number of {@code null} values for this index from the
     * given row.
     */
    private int countNullValues(Object[] values) {
        if (values == null) {
            return _columns.size();
        }

        // annoyingly, the values array could come from different sources, one
        // of which will make it a different size than the other.  we need to
        // handle both situations.
        int nullCount = 0;
        for (ColumnDescriptor col : _columns) {
            Object value = values[col.getColumnIndex()];
            if (col.isNullValue(value)) {
                ++nullCount;
            }
        }

        return nullCount;
    }

    /**
     * Creates the entry bytes for a row of values.
     */
    private byte[] createEntryBytes(Object[] values) throws IOException {
        if (values == null) {
            return null;
        }

        if (_entryBuffer == null) {
            _entryBuffer = new ByteStream();
        }
        _entryBuffer.reset();

        for (ColumnDescriptor col : _columns) {
            Object value = values[col.getColumnIndex()];
            if (Column.isRawData(value)) {
                // ignore it, we could not parse it
                continue;
            }

            if (value == MIN_VALUE) {
                // null is the "least" value
                _entryBuffer.write(getNullEntryFlag(col.isAscending()));
                continue;
            }
            if (value == MAX_VALUE) {
                // the opposite null is the "greatest" value
                _entryBuffer.write(getNullEntryFlag(!col.isAscending()));
                continue;
            }

            col.writeValue(value, _entryBuffer);
        }

        return _entryBuffer.toByteArray();
    }

    /**
     * Writes the current index state to the database.  Index has already been
     * initialized.
     */
    protected abstract void updateImpl() throws IOException;

    /**
     * Reads the actual index entries.
     */
    protected abstract void readIndexEntries() throws IOException;

    /**
     * Finds the data page for the given entry.
     */
    protected abstract DataPage findDataPage(Entry entry) throws IOException;

    /**
     * Gets the data page for the pageNumber.
     */
    protected abstract DataPage getDataPage(int pageNumber) throws IOException;

    /**
     * Flips the first bit in the byte at the given index.
     */
    private static byte[] flipFirstBitInByte(byte[] value, int index) {
        value[index] = (byte) (value[index] ^ 0x80);

        return value;
    }

    /**
     * Flips all the bits in the byte array.
     */
    private static byte[] flipBytes(byte[] value) {
        return flipBytes(value, 0, value.length);
    }

    /**
     * Flips the bits in the specified bytes in the byte array.
     */
    static byte[] flipBytes(byte[] value, int offset, int length) {
        for (int i = offset; i < (offset + length); ++i) {
            value[i] = (byte) (~value[i]);
        }
        return value;
    }

    /**
     * Writes the value of the given column type to a byte array and returns it.
     */
    private static byte[] encodeNumberColumnValue(Object value, Column column) throws IOException {
        // always write in big endian order
        return column.write(value, 0, ENTRY_BYTE_ORDER).array();
    }

    /**
     * Creates one of the special index entries.
     */
    private static Entry createSpecialEntry(RowId rowId) {
        return new Entry((byte[]) null, rowId);
    }

    /**
     * Constructs a ColumnDescriptor of the relevant type for the given Column.
     */
    private ColumnDescriptor newColumnDescriptor(Column col, byte flags) throws IOException {
        switch (col.getType()) {
        case TEXT:
        case MEMO:
            Column.SortOrder sortOrder = col.getTextSortOrder();
            if (Column.GENERAL_LEGACY_SORT_ORDER.equals(sortOrder)) {
                return new GenLegTextColumnDescriptor(col, flags);
            }
            if (Column.GENERAL_SORT_ORDER.equals(sortOrder)) {
                return new GenTextColumnDescriptor(col, flags);
            }
            // unsupported sort order
            LOG.warn("Unsupported collating sort order " + sortOrder + " for text index, making read-only");
            setReadOnly();
            return new ReadOnlyColumnDescriptor(col, flags);
        case INT:
        case LONG:
        case MONEY:
        case COMPLEX_TYPE:
            return new IntegerColumnDescriptor(col, flags);
        case FLOAT:
        case DOUBLE:
        case SHORT_DATE_TIME:
            return new FloatingPointColumnDescriptor(col, flags);
        case NUMERIC:
            return (col.getFormat().LEGACY_NUMERIC_INDEXES ? new LegacyFixedPointColumnDescriptor(col, flags)
                    : new FixedPointColumnDescriptor(col, flags));
        case BYTE:
            return new ByteColumnDescriptor(col, flags);
        case BOOLEAN:
            return new BooleanColumnDescriptor(col, flags);
        case GUID:
            return new GuidColumnDescriptor(col, flags);

        default:
            // FIXME we can't modify this index at this point in time
            LOG.warn("Unsupported data type " + col.getType() + " for index, making read-only");
            setReadOnly();
            return new ReadOnlyColumnDescriptor(col, flags);
        }
    }

    /**
     * Returns the EntryType based on the given entry info.
     */
    private static EntryType determineEntryType(byte[] entryBytes, RowId rowId) {
        if (entryBytes != null) {
            return ((rowId.getType() == RowId.Type.NORMAL) ? EntryType.NORMAL
                    : ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ? EntryType.FIRST_VALID
                            : EntryType.LAST_VALID));
        } else if (!rowId.isValid()) {
            // this is a "special" entry (first/last)
            return ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ? EntryType.ALWAYS_FIRST : EntryType.ALWAYS_LAST);
        }
        throw new IllegalArgumentException("Values was null for valid entry");
    }

    /**
     * Returns the maximum amount of entry data which can be encoded on any
     * index page.
     */
    private static int calcMaxPageEntrySize(JetFormat format) {
        // the max data we can fit on a page is the min of the space on the page
        // vs the number of bytes which can be encoded in the entry mask
        int pageDataSize = (format.PAGE_SIZE - (format.OFFSET_INDEX_ENTRY_MASK + format.SIZE_INDEX_ENTRY_MASK));
        int entryMaskSize = (format.SIZE_INDEX_ENTRY_MASK * 8);
        return Math.min(pageDataSize, entryMaskSize);
    }

    /**
     * Information about the columns in an index.  Also encodes new index
     * values.
     */
    public static abstract class ColumnDescriptor {
        private final Column _column;
        private final byte _flags;

        private ColumnDescriptor(Column column, byte flags) throws IOException {
            _column = column;
            _flags = flags;
        }

        public Column getColumn() {
            return _column;
        }

        public byte getFlags() {
            return _flags;
        }

        public boolean isAscending() {
            return ((getFlags() & ASCENDING_COLUMN_FLAG) != 0);
        }

        public int getColumnIndex() {
            return getColumn().getColumnIndex();
        }

        public String getName() {
            return getColumn().getName();
        }

        protected boolean isNullValue(Object value) {
            return (value == null);
        }

        protected final void writeValue(Object value, ByteStream bout) throws IOException {
            if (isNullValue(value)) {
                // write null value
                bout.write(getNullEntryFlag(isAscending()));
                return;
            }

            // write the start flag
            bout.write(getStartEntryFlag(isAscending()));
            // write the rest of the value
            writeNonNullValue(value, bout);
        }

        protected abstract void writeNonNullValue(Object value, ByteStream bout) throws IOException;

        @Override
        public String toString() {
            return "ColumnDescriptor " + getColumn() + "\nflags: " + getFlags();
        }
    }

    /**
     * ColumnDescriptor for integer based columns.
     */
    private static final class IntegerColumnDescriptor extends ColumnDescriptor {
        private IntegerColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            byte[] valueBytes = encodeNumberColumnValue(value, getColumn());

            // bit twiddling rules:
            // - isAsc  => flipFirstBit
            // - !isAsc => flipFirstBit, flipBytes

            flipFirstBitInByte(valueBytes, 0);
            if (!isAscending()) {
                flipBytes(valueBytes);
            }

            bout.write(valueBytes);
        }
    }

    /**
     * ColumnDescriptor for floating point based columns.
     */
    private static final class FloatingPointColumnDescriptor extends ColumnDescriptor {
        private FloatingPointColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            byte[] valueBytes = encodeNumberColumnValue(value, getColumn());

            // determine if the number is negative by testing if the first bit is
            // set
            boolean isNegative = ((valueBytes[0] & 0x80) != 0);

            // bit twiddling rules:
            // isAsc && !isNeg => flipFirstBit
            // isAsc && isNeg => flipBytes
            // !isAsc && !isNeg => flipFirstBit, flipBytes
            // !isAsc && isNeg => nothing

            if (!isNegative) {
                flipFirstBitInByte(valueBytes, 0);
            }
            if (isNegative == isAscending()) {
                flipBytes(valueBytes);
            }

            bout.write(valueBytes);
        }
    }

    /**
     * ColumnDescriptor for fixed point based columns (legacy sort order).
     */
    private static class LegacyFixedPointColumnDescriptor extends ColumnDescriptor {
        private LegacyFixedPointColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        protected void handleNegationAndOrder(boolean isNegative, byte[] valueBytes) {
            if (isNegative == isAscending()) {
                flipBytes(valueBytes);
            }

            // reverse the sign byte (after any previous byte flipping)
            valueBytes[0] = (isNegative ? (byte) 0x00 : (byte) 0xFF);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            byte[] valueBytes = encodeNumberColumnValue(value, getColumn());

            // determine if the number is negative by testing if the first bit is
            // set
            boolean isNegative = ((valueBytes[0] & 0x80) != 0);

            // bit twiddling rules:
            // isAsc && !isNeg => setReverseSignByte             => FF 00 00 ...
            // isAsc && isNeg => flipBytes, setReverseSignByte   => 00 FF FF ...
            // !isAsc && !isNeg => flipBytes, setReverseSignByte => FF FF FF ...
            // !isAsc && isNeg => setReverseSignByte             => 00 00 00 ...

            // v2007 bit twiddling rules (old ordering was a bug, MS kb 837148):
            // isAsc && !isNeg => setSignByte 0xFF            => FF 00 00 ...
            // isAsc && isNeg => setSignByte 0xFF, flipBytes  => 00 FF FF ...
            // !isAsc && !isNeg => setSignByte 0xFF           => FF 00 00 ...
            // !isAsc && isNeg => setSignByte 0xFF, flipBytes => 00 FF FF ...
            handleNegationAndOrder(isNegative, valueBytes);

            bout.write(valueBytes);
        }
    }

    /**
     * ColumnDescriptor for new-style fixed point based columns.
     */
    private static final class FixedPointColumnDescriptor extends LegacyFixedPointColumnDescriptor {
        private FixedPointColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void handleNegationAndOrder(boolean isNegative, byte[] valueBytes) {
            // see notes above in FixedPointColumnDescriptor for bit twiddling rules

            // reverse the sign byte (before any byte flipping)
            valueBytes[0] = (byte) 0xFF;

            if (isNegative == isAscending()) {
                flipBytes(valueBytes);
            }
        }
    }

    /**
     * ColumnDescriptor for byte based columns.
     */
    private static final class ByteColumnDescriptor extends ColumnDescriptor {
        private ByteColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            byte[] valueBytes = encodeNumberColumnValue(value, getColumn());

            // bit twiddling rules:
            // - isAsc  => nothing
            // - !isAsc => flipBytes
            if (!isAscending()) {
                flipBytes(valueBytes);
            }

            bout.write(valueBytes);
        }
    }

    /**
     * ColumnDescriptor for boolean columns.
     */
    private static final class BooleanColumnDescriptor extends ColumnDescriptor {
        private BooleanColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected boolean isNullValue(Object value) {
            // null values are handled as booleans
            return false;
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            bout.write(Column.toBooleanValue(value) ? (isAscending() ? ASC_BOOLEAN_TRUE : DESC_BOOLEAN_TRUE)
                    : (isAscending() ? ASC_BOOLEAN_FALSE : DESC_BOOLEAN_FALSE));
        }
    }

    /**
     * ColumnDescriptor for "general legacy" sort order text based columns.
     */
    private static final class GenLegTextColumnDescriptor extends ColumnDescriptor {
        private GenLegTextColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            GeneralLegacyIndexCodes.GEN_LEG_INSTANCE.writeNonNullIndexTextValue(value, bout, isAscending());
        }
    }

    /**
     * ColumnDescriptor for "general" sort order (2010+) text based columns.
     */
    private static final class GenTextColumnDescriptor extends ColumnDescriptor {
        private GenTextColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            GeneralIndexCodes.GEN_INSTANCE.writeNonNullIndexTextValue(value, bout, isAscending());
        }
    }

    /**
     * ColumnDescriptor for guid columns.
     */
    private static final class GuidColumnDescriptor extends ColumnDescriptor {
        private GuidColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            byte[] valueBytes = encodeNumberColumnValue(value, getColumn());

            // index format <8-bytes> 0x09 <8-bytes> 0x08

            // bit twiddling rules:
            // - isAsc  => nothing
            // - !isAsc => flipBytes, _but keep 09 unflipped_!      
            if (!isAscending()) {
                flipBytes(valueBytes);
            }

            bout.write(valueBytes, 0, 8);
            bout.write(MID_GUID);
            bout.write(valueBytes, 8, 8);
            bout.write(isAscending() ? ASC_END_GUID : DESC_END_GUID);
        }
    }

    /**
     * ColumnDescriptor for columns which we cannot currently write.
     */
    private static final class ReadOnlyColumnDescriptor extends ColumnDescriptor {
        private ReadOnlyColumnDescriptor(Column column, byte flags) throws IOException {
            super(column, flags);
        }

        @Override
        protected void writeNonNullValue(Object value, ByteStream bout) throws IOException {
            throw new UnsupportedOperationException("should not be called");
        }
    }

    /**
     * A single leaf entry in an index (points to a single row)
     */
    public static class Entry implements Comparable<Entry> {
        /** page/row on which this row is stored */
        private final RowId _rowId;
        /** the entry value */
        private final byte[] _entryBytes;
        /** comparable type for the entry */
        private final EntryType _type;

        /**
         * Create a new entry
         * @param entryBytes encoded bytes for this index entry
         * @param rowId rowId in which the row is stored
         * @param type the type of the entry
         */
        private Entry(byte[] entryBytes, RowId rowId, EntryType type) {
            _rowId = rowId;
            _entryBytes = entryBytes;
            _type = type;
        }

        /**
         * Create a new entry
         * @param entryBytes encoded bytes for this index entry
         * @param rowId rowId in which the row is stored
         */
        private Entry(byte[] entryBytes, RowId rowId) {
            this(entryBytes, rowId, determineEntryType(entryBytes, rowId));
        }

        /**
         * Read an existing entry in from a buffer
         */
        private Entry(ByteBuffer buffer, int entryLen) throws IOException {
            this(buffer, entryLen, 0);
        }

        /**
         * Read an existing entry in from a buffer
         */
        private Entry(ByteBuffer buffer, int entryLen, int extraTrailingLen) throws IOException {
            // we need 4 trailing bytes for the rowId, plus whatever the caller
            // wants
            int colEntryLen = entryLen - (4 + extraTrailingLen);

            // read the entry bytes
            _entryBytes = new byte[colEntryLen];
            buffer.get(_entryBytes);

            // read the rowId
            int page = ByteUtil.get3ByteInt(buffer, ENTRY_BYTE_ORDER);
            int row = ByteUtil.getUnsignedByte(buffer);

            _rowId = new RowId(page, row);
            _type = EntryType.NORMAL;
        }

        public RowId getRowId() {
            return _rowId;
        }

        public EntryType getType() {
            return _type;
        }

        public Integer getSubPageNumber() {
            throw new UnsupportedOperationException();
        }

        public boolean isLeafEntry() {
            return true;
        }

        public boolean isValid() {
            return (_entryBytes != null);
        }

        protected final byte[] getEntryBytes() {
            return _entryBytes;
        }

        /**
         * Size of this entry in the db.
         */
        protected int size() {
            // need 4 trailing bytes for the rowId
            return _entryBytes.length + 4;
        }

        /**
         * Write this entry into a buffer
         */
        protected void write(ByteBuffer buffer, byte[] prefix) throws IOException {
            if (prefix.length <= _entryBytes.length) {

                // write entry bytes, not including prefix
                buffer.put(_entryBytes, prefix.length, (_entryBytes.length - prefix.length));
                ByteUtil.put3ByteInt(buffer, getRowId().getPageNumber(), ENTRY_BYTE_ORDER);

            } else if (prefix.length <= (_entryBytes.length + 3)) {

                // the prefix includes part of the page number, write to temp buffer
                // and copy last bytes to output buffer
                ByteBuffer tmp = ByteBuffer.allocate(3);
                ByteUtil.put3ByteInt(tmp, getRowId().getPageNumber(), ENTRY_BYTE_ORDER);
                tmp.flip();
                tmp.position(prefix.length - _entryBytes.length);
                buffer.put(tmp);

            } else {

                // since the row number would never be the same if the page number is
                // the same, nothing past the page number should ever be included in
                // the prefix.
                // FIXME, this could happen if page has only one row...
                throw new IllegalStateException("prefix should never be this long");
            }

            buffer.put((byte) getRowId().getRowNumber());
        }

        protected final String entryBytesToString() {
            return (isValid()
                    ? ", Bytes = " + ByteUtil.toHexString(ByteBuffer.wrap(_entryBytes), _entryBytes.length)
                    : "");
        }

        @Override
        public String toString() {
            return "RowId = " + _rowId + entryBytesToString() + "\n";
        }

        @Override
        public int hashCode() {
            return _rowId.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass()) && (compareTo((Entry) o) == 0)));
        }

        /**
         * @return {@code true} iff the entryBytes are equal between this
         *         Entry and the given Entry
         */
        public boolean equalsEntryBytes(Entry o) {
            return (BYTE_CODE_COMPARATOR.compare(_entryBytes, o._entryBytes) == 0);
        }

        public int compareTo(Entry other) {
            if (this == other) {
                return 0;
            }

            if (isValid() && other.isValid()) {

                // comparing two valid entries.  first, compare by actual byte values
                int entryCmp = BYTE_CODE_COMPARATOR.compare(_entryBytes, other._entryBytes);
                if (entryCmp != 0) {
                    return entryCmp;
                }

            } else {

                // if the entries are of mixed validity (or both invalid), we defer
                // next to the EntryType
                int typeCmp = _type.compareTo(other._type);
                if (typeCmp != 0) {
                    return typeCmp;
                }
            }

            // at this point we let the RowId decide the final result
            return _rowId.compareTo(other.getRowId());
        }

        /**
         * Returns a copy of this entry as a node Entry with the given
         * subPageNumber.
         */
        protected Entry asNodeEntry(Integer subPageNumber) {
            return new NodeEntry(_entryBytes, _rowId, _type, subPageNumber);
        }

    }

    /**
     * A single node entry in an index (points to a sub-page in the index)
     */
    private static final class NodeEntry extends Entry {

        /** index page number of the page to which this node entry refers */
        private final Integer _subPageNumber;

        /**
         * Create a new node entry
         * @param entryBytes encoded bytes for this index entry
         * @param rowId rowId in which the row is stored
         * @param type the type of the entry
         * @param subPageNumber the sub-page to which this node entry refers
         */
        private NodeEntry(byte[] entryBytes, RowId rowId, EntryType type, Integer subPageNumber) {
            super(entryBytes, rowId, type);
            _subPageNumber = subPageNumber;
        }

        /**
         * Read an existing node entry in from a buffer
         */
        private NodeEntry(ByteBuffer buffer, int entryLen) throws IOException {
            // we need 4 trailing bytes for the sub-page number
            super(buffer, entryLen, 4);

            _subPageNumber = ByteUtil.getInt(buffer, ENTRY_BYTE_ORDER);
        }

        @Override
        public Integer getSubPageNumber() {
            return _subPageNumber;
        }

        @Override
        public boolean isLeafEntry() {
            return false;
        }

        @Override
        protected int size() {
            // need 4 trailing bytes for the sub-page number
            return super.size() + 4;
        }

        @Override
        protected void write(ByteBuffer buffer, byte[] prefix) throws IOException {
            super.write(buffer, prefix);
            ByteUtil.putInt(buffer, _subPageNumber, ENTRY_BYTE_ORDER);
        }

        @Override
        public boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass()) && (compareTo((Entry) o) == 0)
                    && (getSubPageNumber().equals(((Entry) o).getSubPageNumber()))));
        }

        @Override
        public String toString() {
            return ("Node RowId = " + getRowId() + ", SubPage = " + _subPageNumber + entryBytesToString() + "\n");
        }

    }

    /**
     * Utility class to traverse the entries in the Index.  Remains valid in the
     * face of index entry modifications.
     */
    public final class EntryCursor {
        /** handler for moving the page cursor forward */
        private final DirHandler _forwardDirHandler = new ForwardDirHandler();
        /** handler for moving the page cursor backward */
        private final DirHandler _reverseDirHandler = new ReverseDirHandler();
        /** the first (exclusive) row id for this cursor */
        private Position _firstPos;
        /** the last (exclusive) row id for this cursor */
        private Position _lastPos;
        /** the current entry */
        private Position _curPos;
        /** the previous entry */
        private Position _prevPos;
        /** the last read modification count on the Index.  we track this so that
            the cursor can detect updates to the index while traversing and act
            accordingly */
        private int _lastModCount;

        private EntryCursor(Position firstPos, Position lastPos) {
            _firstPos = firstPos;
            _lastPos = lastPos;
            _lastModCount = getIndexModCount();
            reset();
        }

        /**
         * Returns the DirHandler for the given direction
         */
        private DirHandler getDirHandler(boolean moveForward) {
            return (moveForward ? _forwardDirHandler : _reverseDirHandler);
        }

        public IndexData getIndexData() {
            return IndexData.this;
        }

        private int getIndexModCount() {
            return IndexData.this._modCount;
        }

        /**
         * Returns the first entry (exclusive) as defined by this cursor.
         */
        public Entry getFirstEntry() {
            return _firstPos.getEntry();
        }

        /**
         * Returns the last entry (exclusive) as defined by this cursor.
         */
        public Entry getLastEntry() {
            return _lastPos.getEntry();
        }

        /**
         * Returns {@code true} if this cursor is up-to-date with respect to its
         * index.
         */
        public boolean isUpToDate() {
            return (getIndexModCount() == _lastModCount);
        }

        public void reset() {
            beforeFirst();
        }

        public void beforeFirst() {
            reset(Cursor.MOVE_FORWARD);
        }

        public void afterLast() {
            reset(Cursor.MOVE_REVERSE);
        }

        protected void reset(boolean moveForward) {
            _curPos = getDirHandler(moveForward).getBeginningPosition();
            _prevPos = _curPos;
        }

        /**
         * Repositions the cursor so that the next row will be the first entry
         * >= the given row.
         */
        public void beforeEntry(Object[] row) throws IOException {
            restorePosition(new Entry(IndexData.this.createEntryBytes(row), RowId.FIRST_ROW_ID));
        }

        /**
         * Repositions the cursor so that the previous row will be the first
         * entry <= the given row.
         */
        public void afterEntry(Object[] row) throws IOException {
            restorePosition(new Entry(IndexData.this.createEntryBytes(row), RowId.LAST_ROW_ID));
        }

        /**
         * @return valid entry if there was a next entry,
         *         {@code #getLastEntry} otherwise
         */
        public Entry getNextEntry() throws IOException {
            return getAnotherPosition(Cursor.MOVE_FORWARD).getEntry();
        }

        /**
         * @return valid entry if there was a next entry,
         *         {@code #getFirstEntry} otherwise
         */
        public Entry getPreviousEntry() throws IOException {
            return getAnotherPosition(Cursor.MOVE_REVERSE).getEntry();
        }

        /**
         * Restores a current position for the cursor (current position becomes
         * previous position).
         */
        protected void restorePosition(Entry curEntry) throws IOException {
            restorePosition(curEntry, _curPos.getEntry());
        }

        /**
         * Restores a current and previous position for the cursor.
         */
        protected void restorePosition(Entry curEntry, Entry prevEntry) throws IOException {
            if (!_curPos.equalsEntry(curEntry) || !_prevPos.equalsEntry(prevEntry)) {
                if (!isUpToDate()) {
                    updateBounds();
                    _lastModCount = getIndexModCount();
                }
                _prevPos = updatePosition(prevEntry);
                _curPos = updatePosition(curEntry);
            } else {
                checkForModification();
            }
        }

        /**
         * Gets another entry in the given direction, returning the new entry.
         */
        private Position getAnotherPosition(boolean moveForward) throws IOException {
            DirHandler handler = getDirHandler(moveForward);
            if (_curPos.equals(handler.getEndPosition())) {
                if (!isUpToDate()) {
                    restorePosition(_prevPos.getEntry());
                    // drop through and retry moving to another entry
                } else {
                    // at end, no more
                    return _curPos;
                }
            }

            checkForModification();

            _prevPos = _curPos;
            _curPos = handler.getAnotherPosition(_curPos);
            return _curPos;
        }

        /**
         * Checks the index for modifications and updates state accordingly.
         */
        private void checkForModification() throws IOException {
            if (!isUpToDate()) {
                updateBounds();
                _prevPos = updatePosition(_prevPos.getEntry());
                _curPos = updatePosition(_curPos.getEntry());
                _lastModCount = getIndexModCount();
            }
        }

        /**
         * Updates the given position, taking boundaries into account.
         */
        private Position updatePosition(Entry entry) throws IOException {
            if (!entry.isValid()) {
                // no use searching if "updating" the first/last pos
                if (_firstPos.equalsEntry(entry)) {
                    return _firstPos;
                } else if (_lastPos.equalsEntry(entry)) {
                    return _lastPos;
                } else {
                    throw new IllegalArgumentException("Invalid entry given " + entry);
                }
            }

            Position pos = findEntryPosition(entry);
            if (pos.compareTo(_lastPos) >= 0) {
                return _lastPos;
            } else if (pos.compareTo(_firstPos) <= 0) {
                return _firstPos;
            }
            return pos;
        }

        /**
         * Updates any the boundary info (_firstPos/_lastPos).
         */
        private void updateBounds() throws IOException {
            _firstPos = findEntryPosition(_firstPos.getEntry());
            _lastPos = findEntryPosition(_lastPos.getEntry());
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + " CurPosition " + _curPos + ", PrevPosition " + _prevPos;
        }

        /**
         * Handles moving the cursor in a given direction.  Separates cursor
         * logic from value storage.
         */
        private abstract class DirHandler {
            public abstract Position getAnotherPosition(Position curPos) throws IOException;

            public abstract Position getBeginningPosition();

            public abstract Position getEndPosition();
        }

        /**
         * Handles moving the cursor forward.
         */
        private final class ForwardDirHandler extends DirHandler {
            @Override
            public Position getAnotherPosition(Position curPos) throws IOException {
                Position newPos = getNextPosition(curPos);
                if ((newPos == null) || (newPos.compareTo(_lastPos) >= 0)) {
                    newPos = _lastPos;
                }
                return newPos;
            }

            @Override
            public Position getBeginningPosition() {
                return _firstPos;
            }

            @Override
            public Position getEndPosition() {
                return _lastPos;
            }
        }

        /**
         * Handles moving the cursor backward.
         */
        private final class ReverseDirHandler extends DirHandler {
            @Override
            public Position getAnotherPosition(Position curPos) throws IOException {
                Position newPos = getPreviousPosition(curPos);
                if ((newPos == null) || (newPos.compareTo(_firstPos) <= 0)) {
                    newPos = _firstPos;
                }
                return newPos;
            }

            @Override
            public Position getBeginningPosition() {
                return _lastPos;
            }

            @Override
            public Position getEndPosition() {
                return _firstPos;
            }
        }
    }

    /**
     * Simple value object for maintaining some cursor state.
     */
    private static final class Position implements Comparable<Position> {
        /** the last known page of the given entry */
        private final DataPage _dataPage;
        /** the last known index of the given entry */
        private final int _idx;
        /** the entry at the given index */
        private final Entry _entry;
        /** {@code true} if this entry does not currently exist in the entry list,
            {@code false} otherwise (this is equivalent to adding -0.5 to the
            _idx) */
        private final boolean _between;

        private Position(DataPage dataPage, int idx) {
            this(dataPage, idx, dataPage.getEntries().get(idx), false);
        }

        private Position(DataPage dataPage, int idx, Entry entry, boolean between) {
            _dataPage = dataPage;
            _idx = idx;
            _entry = entry;
            _between = between;
        }

        public DataPage getDataPage() {
            return _dataPage;
        }

        public int getIndex() {
            return _idx;
        }

        public int getNextIndex() {
            // note, _idx does not need to be advanced if it was pointing at a
            // between position
            return (_between ? _idx : (_idx + 1));
        }

        public int getPrevIndex() {
            // note, we ignore the between flag here because the index will be
            // pointing at the correct next index in either the between or
            // non-between case
            return (_idx - 1);
        }

        public Entry getEntry() {
            return _entry;
        }

        //public boolean isBetween() {
        //return _between;
        //}

        public boolean equalsEntry(Entry entry) {
            return _entry.equals(entry);
        }

        public int compareTo(Position other) {
            if (this == other) {
                return 0;
            }

            if (_dataPage.equals(other._dataPage)) {
                // "simple" index comparison (handle between-ness)
                int idxCmp = ((_idx < other._idx) ? -1
                        : ((_idx > other._idx) ? 1 : ((_between == other._between) ? 0 : (_between ? -1 : 1))));
                if (idxCmp != 0) {
                    return idxCmp;
                }
            }

            // compare the entries.
            return _entry.compareTo(other._entry);
        }

        @Override
        public int hashCode() {
            return _entry.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass()) && (compareTo((Position) o) == 0)));
        }

        @Override
        public String toString() {
            return "Page = " + _dataPage.getPageNumber() + ", Idx = " + _idx + ", Entry = " + _entry
                    + ", Between = " + _between;
        }
    }

    /**
     * Object used to maintain state about an Index page.
     */
    protected static abstract class DataPage {

        public abstract int getPageNumber();

        public abstract boolean isLeaf();

        public abstract void setLeaf(boolean isLeaf);

        public abstract int getPrevPageNumber();

        public abstract void setPrevPageNumber(int pageNumber);

        public abstract int getNextPageNumber();

        public abstract void setNextPageNumber(int pageNumber);

        public abstract int getChildTailPageNumber();

        public abstract void setChildTailPageNumber(int pageNumber);

        public abstract int getTotalEntrySize();

        public abstract void setTotalEntrySize(int totalSize);

        public abstract byte[] getEntryPrefix();

        public abstract void setEntryPrefix(byte[] entryPrefix);

        public abstract List<Entry> getEntries();

        public abstract void setEntries(List<Entry> entries);

        public abstract void addEntry(int idx, Entry entry) throws IOException;

        public abstract void removeEntry(int idx) throws IOException;

        public final boolean isEmpty() {
            return getEntries().isEmpty();
        }

        public final int getCompressedEntrySize() {
            // when written to the index page, the entryPrefix bytes will only be
            // written for the first entry, so we subtract the entry prefix size
            // from all the other entries to determine the compressed size
            return getTotalEntrySize() - (getEntryPrefix().length * (getEntries().size() - 1));
        }

        public final int findEntry(Entry entry) {
            return Collections.binarySearch(getEntries(), entry);
        }

        @Override
        public final int hashCode() {
            return getPageNumber();
        }

        @Override
        public final boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass())
                    && (getPageNumber() == ((DataPage) o).getPageNumber())));
        }

        @Override
        public final String toString() {
            List<Entry> entries = getEntries();
            return (isLeaf() ? "Leaf" : "Node") + "DataPage[" + getPageNumber() + "] " + getPrevPageNumber() + ", "
                    + getNextPageNumber() + ", (" + getChildTailPageNumber() + "), "
                    + ((isLeaf() && !entries.isEmpty())
                            ? ("[" + entries.get(0) + ", " + entries.get(entries.size() - 1) + "]")
                            : entries);
        }
    }

}