com.healthmarketscience.jackcess.impl.TableImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.healthmarketscience.jackcess.impl.TableImpl.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.impl;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.healthmarketscience.jackcess.BatchUpdateException;
import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.ConstraintViolationException;
import com.healthmarketscience.jackcess.CursorBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.IndexBuilder;
import com.healthmarketscience.jackcess.JackcessException;
import com.healthmarketscience.jackcess.PropertyMap;
import com.healthmarketscience.jackcess.Row;
import com.healthmarketscience.jackcess.RowId;
import com.healthmarketscience.jackcess.Table;
import com.healthmarketscience.jackcess.util.ErrorHandler;
import com.healthmarketscience.jackcess.util.ExportUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A single database table
 * <p>
 * Is not thread-safe.
 * 
 * @author Tim McCune
 * @usage _general_class_
 */
public class TableImpl implements Table {
    private static final Log LOG = LogFactory.getLog(TableImpl.class);

    private static final short OFFSET_MASK = (short) 0x1FFF;

    private static final short DELETED_ROW_MASK = (short) 0x8000;

    private static final short OVERFLOW_ROW_MASK = (short) 0x4000;

    static final int MAGIC_TABLE_NUMBER = 1625;

    private static final int MAX_BYTE = 256;

    /**
     * Table type code for system tables
     * @usage _intermediate_class_
     */
    public static final byte TYPE_SYSTEM = 0x53;
    /**
     * Table type code for user tables
     * @usage _intermediate_class_
     */
    public static final byte TYPE_USER = 0x4e;

    /** comparator which sorts variable length columns based on their index into
        the variable length offset table */
    private static final Comparator<ColumnImpl> VAR_LEN_COLUMN_COMPARATOR = new Comparator<ColumnImpl>() {
        public int compare(ColumnImpl c1, ColumnImpl c2) {
            return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1
                    : ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : 0));
        }
    };

    /** comparator which sorts columns based on their display index */
    private static final Comparator<ColumnImpl> DISPLAY_ORDER_COMPARATOR = new Comparator<ColumnImpl>() {
        public int compare(ColumnImpl c1, ColumnImpl c2) {
            return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1
                    : ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : 0));
        }
    };

    /** owning database */
    private final DatabaseImpl _database;
    /** additional table flags from the catalog entry */
    private final int _flags;
    /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */
    private final byte _tableType;
    /** Number of actual indexes on the table */
    private final int _indexCount;
    /** Number of logical indexes for the table */
    private final int _logicalIndexCount;
    /** page number of the definition of this table */
    private final int _tableDefPageNumber;
    /** max Number of columns in the table (includes previous deletions) */
    private final short _maxColumnCount;
    /** max Number of variable columns in the table */
    private final short _maxVarColumnCount;
    /** List of columns in this table, ordered by column number */
    private final List<ColumnImpl> _columns = new ArrayList<ColumnImpl>();
    /** List of variable length columns in this table, ordered by offset */
    private final List<ColumnImpl> _varColumns = new ArrayList<ColumnImpl>();
    /** List of autonumber columns in this table, ordered by column number */
    private final List<ColumnImpl> _autoNumColumns = new ArrayList<ColumnImpl>(1);
    /** List of indexes on this table (multiple logical indexes may be backed by
        the same index data) */
    private final List<IndexImpl> _indexes = new ArrayList<IndexImpl>();
    /** List of index datas on this table (the actual backing data for an
        index) */
    private final List<IndexData> _indexDatas = new ArrayList<IndexData>();
    /** List of columns in this table which are in one or more indexes */
    private final Set<ColumnImpl> _indexColumns = new LinkedHashSet<ColumnImpl>();
    /** Table name as stored in Database */
    private final String _name;
    /** Usage map of pages that this table owns */
    private final UsageMap _ownedPages;
    /** Usage map of pages that this table owns with free space on them */
    private final UsageMap _freeSpacePages;
    /** Number of rows in the table */
    private int _rowCount;
    /** last long auto number for the table */
    private int _lastLongAutoNumber;
    /** last complex type auto number for the table */
    private int _lastComplexTypeAutoNumber;
    /** modification count for the table, keeps row-states up-to-date */
    private int _modCount;
    /** page buffer used to update data pages when adding rows */
    private final TempPageHolder _addRowBufferH = TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
    /** page buffer used to update the table def page */
    private final TempPageHolder _tableDefBufferH = TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
    /** buffer used to writing rows of data */
    private final TempBufferHolder _writeRowBufferH = TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
    /** page buffer used to write out-of-row "long value" data */
    private final TempPageHolder _longValueBufferH = TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
    /** optional error handler to use when row errors are encountered */
    private ErrorHandler _tableErrorHandler;
    /** properties for this table */
    private PropertyMap _props;
    /** properties group for this table (and columns) */
    private PropertyMaps _propertyMaps;
    /** foreign-key enforcer for this table */
    private final FKEnforcer _fkEnforcer;

    /** default cursor for iterating through the table, kept here for basic
        table traversal */
    private CursorImpl _defaultCursor;

    /**
     * Only used by unit tests
     * @usage _advanced_method_
     */
    protected TableImpl(boolean testing, List<ColumnImpl> columns) throws IOException {
        if (!testing) {
            throw new IllegalArgumentException();
        }
        _database = null;
        _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
        _name = null;

        _columns.addAll(columns);
        for (ColumnImpl col : _columns) {
            if (col.getType().isVariableLength()) {
                _varColumns.add(col);
            }
        }
        _maxColumnCount = (short) _columns.size();
        _maxVarColumnCount = (short) _varColumns.size();
        getAutoNumberColumns();

        _fkEnforcer = null;
        _flags = 0;
        _tableType = TYPE_USER;
        _indexCount = 0;
        _logicalIndexCount = 0;
        _ownedPages = null;
        _freeSpacePages = null;
    }

    /**
     * @param database database which owns this table
     * @param tableBuffer Buffer to read the table with
     * @param pageNumber Page number of the table definition
     * @param name Table name
     */
    protected TableImpl(DatabaseImpl database, ByteBuffer tableBuffer, int pageNumber, String name, int flags)
            throws IOException {
        _database = database;
        _tableDefPageNumber = pageNumber;
        _name = name;
        _flags = flags;

        // read table definition
        tableBuffer = loadCompleteTableDefinitionBuffer(tableBuffer);
        _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS);
        _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER);
        if (getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) {
            _lastComplexTypeAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER);
        }
        _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE);
        _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS);
        _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS);
        short columnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_COLS);
        _logicalIndexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEX_SLOTS);
        _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES);

        tableBuffer.position(getFormat().OFFSET_OWNED_PAGES);
        _ownedPages = UsageMap.read(getDatabase(), tableBuffer, false);
        tableBuffer.position(getFormat().OFFSET_FREE_SPACE_PAGES);
        _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false);

        for (int i = 0; i < _indexCount; i++) {
            _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat()));
        }

        readColumnDefinitions(tableBuffer, columnCount);

        readIndexDefinitions(tableBuffer);

        // read column usage map info
        while (tableBuffer.remaining() >= 2) {

            short umapColNum = tableBuffer.getShort();
            if (umapColNum == IndexData.COLUMN_UNUSED) {
                break;
            }

            int pos = tableBuffer.position();
            UsageMap colOwnedPages = null;
            UsageMap colFreeSpacePages = null;
            try {
                colOwnedPages = UsageMap.read(getDatabase(), tableBuffer, false);
                colFreeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false);
            } catch (IllegalStateException e) {
                // ignore invalid usage map info
                colOwnedPages = null;
                colFreeSpacePages = null;
                tableBuffer.position(pos + 8);
                LOG.warn("Table " + _name + " invalid column " + umapColNum + " usage map definition: " + e);
            }

            for (ColumnImpl col : _columns) {
                if (col.getColumnNumber() == umapColNum) {
                    col.setUsageMaps(colOwnedPages, colFreeSpacePages);
                    break;
                }
            }
        }

        // re-sort columns if necessary
        if (getDatabase().getColumnOrder() != ColumnOrder.DATA) {
            Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
        }

        for (ColumnImpl col : _columns) {
            // some columns need to do extra work after the table is completely
            // loaded
            col.postTableLoadInit();
        }

        _fkEnforcer = new FKEnforcer(this);

        if (!isSystem()) {
            // after fully constructed, allow column validator to be configured (but
            // only for user tables)
            for (ColumnImpl col : _columns) {
                col.setColumnValidator(null);
            }
        }
    }

    public String getName() {
        return _name;
    }

    public boolean isHidden() {
        return ((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0);
    }

    public boolean isSystem() {
        return (_tableType != TYPE_USER);
    }

    /**
     * @usage _advanced_method_
     */
    public int getMaxColumnCount() {
        return _maxColumnCount;
    }

    public int getColumnCount() {
        return _columns.size();
    }

    public DatabaseImpl getDatabase() {
        return _database;
    }

    /**
     * @usage _advanced_method_
     */
    public JetFormat getFormat() {
        return getDatabase().getFormat();
    }

    /**
     * @usage _advanced_method_
     */
    public PageChannel getPageChannel() {
        return getDatabase().getPageChannel();
    }

    public ErrorHandler getErrorHandler() {
        return ((_tableErrorHandler != null) ? _tableErrorHandler : getDatabase().getErrorHandler());
    }

    public void setErrorHandler(ErrorHandler newErrorHandler) {
        _tableErrorHandler = newErrorHandler;
    }

    public int getTableDefPageNumber() {
        return _tableDefPageNumber;
    }

    /**
     * @usage _advanced_method_
     */
    public RowState createRowState() {
        return new RowState(TempBufferHolder.Type.HARD);
    }

    /**
     * @usage _advanced_method_
     */
    public UsageMap.PageCursor getOwnedPagesCursor() {
        return _ownedPages.cursor();
    }

    /**
     * Returns the <i>approximate</i> number of database pages owned by this
     * table and all related indexes (this number does <i>not</i> take into
     * account pages used for large OLE/MEMO fields).
     * <p>
     * To calculate the approximate number of bytes owned by a table:
     * <code>
     * int approxTableBytes = (table.getApproximateOwnedPageCount() *
     *                         table.getFormat().PAGE_SIZE);
     * </code>
     * @usage _intermediate_method_
     */
    public int getApproximateOwnedPageCount() {

        // add a page for the table def (although that might actually be more than
        // one page)
        int count = _ownedPages.getPageCount() + 1;

        for (ColumnImpl col : _columns) {
            count += col.getOwnedPageCount();
        }

        // note, we count owned pages from _physical_ indexes, not logical indexes
        // (otherwise we could double count pages)
        for (IndexData indexData : _indexDatas) {
            count += indexData.getOwnedPageCount();
        }

        return count;
    }

    protected TempPageHolder getLongValueBuffer() {
        return _longValueBufferH;
    }

    public List<ColumnImpl> getColumns() {
        return Collections.unmodifiableList(_columns);
    }

    public ColumnImpl getColumn(String name) {
        for (ColumnImpl column : _columns) {
            if (column.getName().equalsIgnoreCase(name)) {
                return column;
            }
        }
        throw new IllegalArgumentException("Column with name " + name + " does not exist in this table");
    }

    public boolean hasColumn(String name) {
        for (ColumnImpl column : _columns) {
            if (column.getName().equalsIgnoreCase(name)) {
                return true;
            }
        }
        return false;
    }

    public PropertyMap getProperties() throws IOException {
        if (_props == null) {
            _props = getPropertyMaps().getDefault();
        }
        return _props;
    }

    /**
     * @return all PropertyMaps for this table (and columns)
     * @usage _advanced_method_
     */
    public PropertyMaps getPropertyMaps() throws IOException {
        if (_propertyMaps == null) {
            _propertyMaps = getDatabase().getPropertiesForObject(_tableDefPageNumber);
        }
        return _propertyMaps;
    }

    public List<IndexImpl> getIndexes() {
        return Collections.unmodifiableList(_indexes);
    }

    public IndexImpl getIndex(String name) {
        for (IndexImpl index : _indexes) {
            if (index.getName().equalsIgnoreCase(name)) {
                return index;
            }
        }
        throw new IllegalArgumentException("Index with name " + name + " does not exist on this table");
    }

    public IndexImpl getPrimaryKeyIndex() {
        for (IndexImpl index : _indexes) {
            if (index.isPrimaryKey()) {
                return index;
            }
        }
        throw new IllegalArgumentException("Table " + getName() + " does not have a primary key index");
    }

    public IndexImpl getForeignKeyIndex(Table otherTable) {
        for (IndexImpl index : _indexes) {
            if (index.isForeignKey() && (index.getReference() != null) && (index.getReference()
                    .getOtherTablePageNumber() == ((TableImpl) otherTable).getTableDefPageNumber())) {
                return index;
            }
        }
        throw new IllegalArgumentException(
                "Table " + getName() + " does not have a foreign key reference to " + otherTable.getName());
    }

    /**
     * @return All of the IndexData on this table (unmodifiable List)
     * @usage _advanced_method_
     */
    public List<IndexData> getIndexDatas() {
        return Collections.unmodifiableList(_indexDatas);
    }

    /**
     * Only called by unit tests
     * @usage _advanced_method_
     */
    public int getLogicalIndexCount() {
        return _logicalIndexCount;
    }

    public CursorImpl getDefaultCursor() {
        if (_defaultCursor == null) {
            _defaultCursor = CursorImpl.createCursor(this);
        }
        return _defaultCursor;
    }

    public CursorBuilder newCursor() {
        return new CursorBuilder(this);
    }

    public void reset() {
        getDefaultCursor().reset();
    }

    public Row deleteRow(Row row) throws IOException {
        deleteRow(row.getId());
        return row;
    }

    /**
     * Delete the row with the given id.  Provided RowId must have previously
     * been returned from this Table.
     * @return the given rowId
     * @throws IllegalStateException if the given row is not valid
     * @usage _intermediate_method_
     */
    public RowId deleteRow(RowId rowId) throws IOException {
        deleteRow(getDefaultCursor().getRowState(), (RowIdImpl) rowId);
        return rowId;
    }

    /**
     * Delete the row for the given rowId.
     * @usage _advanced_method_
     */
    public void deleteRow(RowState rowState, RowIdImpl rowId) throws IOException {
        requireValidRowId(rowId);

        getPageChannel().startWrite();
        try {

            // ensure that the relevant row state is up-to-date
            ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId);

            if (rowState.isDeleted()) {
                // don't care about duplicate deletion
                return;
            }
            requireNonDeletedRow(rowState, rowId);

            // delete flag always gets set in the "header" row (even if data is on
            // overflow row)
            int pageNumber = rowState.getHeaderRowId().getPageNumber();
            int rowNumber = rowState.getHeaderRowId().getRowNumber();

            // attempt to fill in index column values
            Object[] rowValues = null;
            if (!_indexDatas.isEmpty()) {

                // move to row data to get index values
                rowBuffer = positionAtRowData(rowState, rowId);

                for (ColumnImpl idxCol : _indexColumns) {
                    getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null);
                }

                // use any read rowValues to help update the indexes
                rowValues = rowState.getRowCacheValues();

                // check foreign keys before proceeding w/ deletion
                _fkEnforcer.deleteRow(rowValues);

                // move back to the header
                rowBuffer = positionAtRowHeader(rowState, rowId);
            }

            // finally, pull the trigger
            int rowIndex = getRowStartOffset(rowNumber, getFormat());
            rowBuffer.putShort(rowIndex,
                    (short) (rowBuffer.getShort(rowIndex) | DELETED_ROW_MASK | OVERFLOW_ROW_MASK));
            writeDataPage(rowBuffer, pageNumber);

            // update the indexes
            for (IndexData indexData : _indexDatas) {
                indexData.deleteRow(rowValues, rowId);
            }

            // make sure table def gets updated
            updateTableDefinition(-1);

        } finally {
            getPageChannel().finishWrite();
        }
    }

    public Row getNextRow() throws IOException {
        return getDefaultCursor().getNextRow();
    }

    /**
     * Reads a single column from the given row.
     * @usage _advanced_method_
     */
    public Object getRowValue(RowState rowState, RowIdImpl rowId, ColumnImpl column) throws IOException {
        if (this != column.getTable()) {
            throw new IllegalArgumentException("Given column " + column + " is not from this table");
        }
        requireValidRowId(rowId);

        // position at correct row
        ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
        requireNonDeletedRow(rowState, rowId);

        return getRowColumn(getFormat(), rowBuffer, column, rowState, null);
    }

    /**
     * Reads some columns from the given row.
     * @param columnNames Only column names in this collection will be returned
     * @usage _advanced_method_
     */
    public RowImpl getRow(RowState rowState, RowIdImpl rowId, Collection<String> columnNames) throws IOException {
        requireValidRowId(rowId);

        // position at correct row
        ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
        requireNonDeletedRow(rowState, rowId);

        return getRow(getFormat(), rowState, rowBuffer, _columns, columnNames);
    }

    /**
     * Reads the row data from the given row buffer.  Leaves limit unchanged.
     * Saves parsed row values to the given rowState.
     */
    private static RowImpl getRow(JetFormat format, RowState rowState, ByteBuffer rowBuffer,
            Collection<ColumnImpl> columns, Collection<String> columnNames) throws IOException {
        RowImpl rtn = new RowImpl(rowState.getHeaderRowId(), columns.size());
        for (ColumnImpl column : columns) {

            if ((columnNames == null) || (columnNames.contains(column.getName()))) {
                // Add the value to the row data
                column.setRowValue(rtn, getRowColumn(format, rowBuffer, column, rowState, null));
            }
        }
        return rtn;
    }

    /**
     * Reads the column data from the given row buffer.  Leaves limit unchanged.
     * Caches the returned value in the rowState.
     */
    private static Object getRowColumn(JetFormat format, ByteBuffer rowBuffer, ColumnImpl column, RowState rowState,
            Map<ColumnImpl, byte[]> rawVarValues) throws IOException {
        byte[] columnData = null;
        try {

            NullMask nullMask = rowState.getNullMask(rowBuffer);
            boolean isNull = nullMask.isNull(column);
            if (column.storeInNullMask()) {
                // Boolean values are stored in the null mask.  see note about
                // caching below
                return rowState.setRowCacheValue(column.getColumnIndex(), column.readFromNullMask(isNull));
            } else if (isNull) {
                // well, that's easy! (no need to update cache w/ null)
                return null;
            }

            Object cachedValue = rowState.getRowCacheValue(column.getColumnIndex());
            if (cachedValue != null) {
                // we already have it, use it
                return cachedValue;
            }

            // reset position to row start
            rowBuffer.reset();

            // locate the column data bytes
            int rowStart = rowBuffer.position();
            int colDataPos = 0;
            int colDataLen = 0;
            if (!column.isVariableLength()) {

                // read fixed length value (non-boolean at this point)
                int dataStart = rowStart + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET;
                colDataPos = dataStart + column.getFixedDataOffset();
                colDataLen = column.getType().getFixedSize(column.getLength());

            } else {
                int varDataStart;
                int varDataEnd;

                if (format.SIZE_ROW_VAR_COL_OFFSET == 2) {

                    // read simple var length value
                    int varColumnOffsetPos = (rowBuffer.limit() - nullMask.byteSize() - 4)
                            - (column.getVarLenTableIndex() * 2);

                    varDataStart = rowBuffer.getShort(varColumnOffsetPos);
                    varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);

                } else {

                    // read jump-table based var length values
                    short[] varColumnOffsets = readJumpTableVarColOffsets(rowState, rowBuffer, rowStart, nullMask);

                    varDataStart = varColumnOffsets[column.getVarLenTableIndex()];
                    varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1];
                }

                colDataPos = rowStart + varDataStart;
                colDataLen = varDataEnd - varDataStart;
            }

            // grab the column data
            rowBuffer.position(colDataPos);
            columnData = ByteUtil.getBytes(rowBuffer, colDataLen);

            if ((rawVarValues != null) && column.isVariableLength()) {
                // caller wants raw value as well
                rawVarValues.put(column, columnData);
            }

            // parse the column data.  we cache the row values in order to be able
            // to update the index on row deletion.  note, most of the returned
            // values are immutable, except for binary data (returned as byte[]),
            // but binary data shouldn't be indexed anyway.
            return rowState.setRowCacheValue(column.getColumnIndex(), column.read(columnData));

        } catch (Exception e) {

            // cache "raw" row value.  see note about caching above
            rowState.setRowCacheValue(column.getColumnIndex(), ColumnImpl.rawDataWrapper(columnData));

            return rowState.handleRowError(column, columnData, e);
        }
    }

    private static short[] readJumpTableVarColOffsets(RowState rowState, ByteBuffer rowBuffer, int rowStart,
            NullMask nullMask) {
        short[] varColOffsets = rowState.getVarColOffsets();
        if (varColOffsets != null) {
            return varColOffsets;
        }

        // calculate offsets using jump-table info
        int nullMaskSize = nullMask.byteSize();
        int rowEnd = rowStart + rowBuffer.remaining() - 1;
        int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, rowEnd - nullMaskSize);
        varColOffsets = new short[numVarCols + 1];

        int rowLen = rowEnd - rowStart + 1;
        int numJumps = (rowLen - 1) / MAX_BYTE;
        int colOffset = rowEnd - nullMaskSize - numJumps - 1;

        // If last jump is a dummy value, ignore it
        if (((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) {
            numJumps--;
        }

        int jumpsUsed = 0;
        for (int i = 0; i < numVarCols + 1; i++) {

            while ((jumpsUsed < numJumps)
                    && (i == ByteUtil.getUnsignedByte(rowBuffer, rowEnd - nullMaskSize - jumpsUsed - 1))) {
                jumpsUsed++;
            }

            varColOffsets[i] = (short) (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i)
                    + (jumpsUsed * MAX_BYTE));
        }

        rowState.setVarColOffsets(varColOffsets);
        return varColOffsets;
    }

    /**
     * Reads the null mask from the given row buffer.  Leaves limit unchanged.
     */
    private NullMask getRowNullMask(ByteBuffer rowBuffer) throws IOException {
        // reset position to row start
        rowBuffer.reset();

        // Number of columns in this row
        int columnCount = ByteUtil.getUnsignedVarInt(rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT);

        // read null mask
        NullMask nullMask = new NullMask(columnCount);
        rowBuffer.position(rowBuffer.limit() - nullMask.byteSize()); //Null mask at end
        nullMask.read(rowBuffer);

        return nullMask;
    }

    /**
     * Sets a new buffer to the correct row header page using the given rowState
     * according to the given rowId.  Deleted state is
     * determined, but overflow row pointers are not followed.
     * 
     * @return a ByteBuffer of the relevant page, or null if row was invalid
     * @usage _advanced_method_
     */
    public static ByteBuffer positionAtRowHeader(RowState rowState, RowIdImpl rowId) throws IOException {
        ByteBuffer rowBuffer = rowState.setHeaderRow(rowId);

        if (rowState.isAtHeaderRow()) {
            // this task has already been accomplished
            return rowBuffer;
        }

        if (!rowState.isValid()) {
            // this was an invalid page/row
            rowState.setStatus(RowStateStatus.AT_HEADER);
            return null;
        }

        // note, we don't use findRowStart here cause we need the unmasked value
        short rowStart = rowBuffer
                .getShort(getRowStartOffset(rowId.getRowNumber(), rowState.getTable().getFormat()));

        // check the deleted, overflow flags for the row (the "real" flags are
        // always set on the header row)
        RowStatus rowStatus = RowStatus.NORMAL;
        if (isDeletedRow(rowStart)) {
            rowStatus = RowStatus.DELETED;
        } else if (isOverflowRow(rowStart)) {
            rowStatus = RowStatus.OVERFLOW;
        }

        rowState.setRowStatus(rowStatus);
        rowState.setStatus(RowStateStatus.AT_HEADER);
        return rowBuffer;
    }

    /**
     * Sets the position and limit in a new buffer using the given rowState
     * according to the given row number and row end, following overflow row
     * pointers as necessary.
     * 
     * @return a ByteBuffer narrowed to the actual row data, or null if row was
     *         invalid or deleted
     * @usage _advanced_method_
     */
    public static ByteBuffer positionAtRowData(RowState rowState, RowIdImpl rowId) throws IOException {
        positionAtRowHeader(rowState, rowId);
        if (!rowState.isValid() || rowState.isDeleted()) {
            // row is invalid or deleted
            rowState.setStatus(RowStateStatus.AT_FINAL);
            return null;
        }

        ByteBuffer rowBuffer = rowState.getFinalPage();
        int rowNum = rowState.getFinalRowId().getRowNumber();
        JetFormat format = rowState.getTable().getFormat();

        if (rowState.isAtFinalRow()) {
            // we've already found the final row data
            return PageChannel.narrowBuffer(rowBuffer, findRowStart(rowBuffer, rowNum, format),
                    findRowEnd(rowBuffer, rowNum, format));
        }

        while (true) {

            // note, we don't use findRowStart here cause we need the unmasked value
            short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format));
            short rowEnd = findRowEnd(rowBuffer, rowNum, format);

            // note, at this point we know the row is not deleted, so ignore any
            // subsequent deleted flags (as overflow rows are always marked deleted
            // anyway)
            boolean overflowRow = isOverflowRow(rowStart);

            // now, strip flags from rowStart offset
            rowStart = (short) (rowStart & OFFSET_MASK);

            if (overflowRow) {

                if ((rowEnd - rowStart) < 4) {
                    throw new IOException("invalid overflow row info");
                }

                // Overflow page.  the "row" data in the current page points to
                // another page/row
                int overflowRowNum = ByteUtil.getUnsignedByte(rowBuffer, rowStart);
                int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1);
                rowBuffer = rowState.setOverflowRow(new RowIdImpl(overflowPageNum, overflowRowNum));
                rowNum = overflowRowNum;

            } else {

                rowState.setStatus(RowStateStatus.AT_FINAL);
                return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd);
            }
        }
    }

    public Iterator<Row> iterator() {
        return getDefaultCursor().iterator();
    }

    /**
     * Writes a new table defined by the given TableCreator to the database.
     * @usage _advanced_method_
     */
    protected static void writeTableDefinition(TableCreator creator) throws IOException {
        // first, create the usage map page
        createUsageMapDefinitionBuffer(creator);

        // next, determine how big the table def will be (in case it will be more
        // than one page)
        JetFormat format = creator.getFormat();
        int idxDataLen = (creator.getIndexCount() * (format.SIZE_INDEX_DEFINITION + format.SIZE_INDEX_COLUMN_BLOCK))
                + (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK);
        int colUmapLen = creator.getLongValueColumns().size() * 10;
        int totalTableDefSize = format.SIZE_TDEF_HEADER
                + (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + idxDataLen + colUmapLen
                + format.SIZE_TDEF_TRAILER;

        // total up the amount of space used by the column and index names (2
        // bytes per char + 2 bytes for the length)
        for (ColumnBuilder col : creator.getColumns()) {
            int nameByteLen = (col.getName().length() * JetFormat.TEXT_FIELD_UNIT_SIZE);
            totalTableDefSize += nameByteLen + 2;
        }

        for (IndexBuilder idx : creator.getIndexes()) {
            int nameByteLen = (idx.getName().length() * JetFormat.TEXT_FIELD_UNIT_SIZE);
            totalTableDefSize += nameByteLen + 2;
        }

        // now, create the table definition
        PageChannel pageChannel = creator.getPageChannel();
        ByteBuffer buffer = PageChannel.createBuffer(Math.max(totalTableDefSize, format.PAGE_SIZE));
        writeTableDefinitionHeader(creator, buffer, totalTableDefSize);

        if (creator.hasIndexes()) {
            // index row counts
            IndexData.writeRowCountDefinitions(creator, buffer);
        }

        // column definitions
        ColumnImpl.writeDefinitions(creator, buffer);

        if (creator.hasIndexes()) {
            // index and index data definitions
            IndexData.writeDefinitions(creator, buffer);
            IndexImpl.writeDefinitions(creator, buffer);
        }

        // write long value column usage map references
        for (ColumnBuilder lvalCol : creator.getLongValueColumns()) {
            buffer.putShort(lvalCol.getColumnNumber());
            TableCreator.ColumnState colState = creator.getColumnState(lvalCol);

            // owned pages umap (both are on same page)
            buffer.put(colState.getUmapOwnedRowNumber());
            ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
            // free space pages umap
            buffer.put(colState.getUmapFreeRowNumber());
            ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
        }

        //End of tabledef
        buffer.put((byte) 0xff);
        buffer.put((byte) 0xff);

        // write table buffer to database
        if (totalTableDefSize <= format.PAGE_SIZE) {

            // easy case, fits on one page
            buffer.putShort(format.OFFSET_FREE_SPACE, (short) (buffer.remaining() - 8)); // overwrite page free space
            // Write the tdef page to disk.
            pageChannel.writePage(buffer, creator.getTdefPageNumber());

        } else {

            // need to split across multiple pages
            ByteBuffer partialTdef = pageChannel.createPageBuffer();
            buffer.rewind();
            int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
            while (buffer.hasRemaining()) {

                // reset for next write
                partialTdef.clear();

                if (nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) {

                    // this is the first page.  note, the first page already has the
                    // page header, so no need to write it here
                    nextTdefPageNumber = creator.getTdefPageNumber();

                } else {

                    // write page header
                    writeTablePageHeader(partialTdef);
                }

                // copy the next page of tdef bytes
                int curTdefPageNumber = nextTdefPageNumber;
                int writeLen = Math.min(partialTdef.remaining(), buffer.remaining());
                partialTdef.put(buffer.array(), buffer.position(), writeLen);
                ByteUtil.forward(buffer, writeLen);

                if (buffer.hasRemaining()) {
                    // need a next page
                    nextTdefPageNumber = pageChannel.allocateNewPage();
                    partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, nextTdefPageNumber);
                }

                // update page free space
                partialTdef.putShort(format.OFFSET_FREE_SPACE, (short) (partialTdef.remaining() - 8)); // overwrite page free space

                // write partial page to disk
                pageChannel.writePage(partialTdef, curTdefPageNumber);
            }

        }
    }

    /**
     * @param buffer Buffer to write to
     * @param columns List of Columns in the table
     */
    private static void writeTableDefinitionHeader(TableCreator creator, ByteBuffer buffer, int totalTableDefSize)
            throws IOException {
        List<ColumnBuilder> columns = creator.getColumns();

        //Start writing the tdef
        writeTablePageHeader(buffer);
        buffer.putInt(totalTableDefSize); //Length of table def
        buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value
        buffer.putInt(0); //Number of rows
        buffer.putInt(0); //Last Autonumber
        buffer.put((byte) 1); // this makes autonumbering work in access
        for (int i = 0; i < 15; i++) { //Unknown
            buffer.put((byte) 0);
        }
        buffer.put(TYPE_USER); //Table type
        buffer.putShort((short) columns.size()); //Max columns a row will have
        buffer.putShort(ColumnImpl.countVariableLength(columns)); //Number of variable columns in table
        buffer.putShort((short) columns.size()); //Number of columns in table
        buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table
        buffer.putInt(creator.getIndexCount()); //Number of indexes in table
        buffer.put((byte) 0); //Usage map row number
        ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Usage map page number
        buffer.put((byte) 1); //Free map row number
        ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Free map page number
    }

    /**
     * Writes the page header for a table definition page
     * @param buffer Buffer to write to
     */
    private static void writeTablePageHeader(ByteBuffer buffer) {
        buffer.put(PageTypes.TABLE_DEF); //Page type
        buffer.put((byte) 0x01); //Unknown
        buffer.put((byte) 0); //Unknown
        buffer.put((byte) 0); //Unknown
        buffer.putInt(0); //Next TDEF page pointer
    }

    /**
     * Writes the given name into the given buffer in the format as expected by
     * {@link #readName}.
     */
    static void writeName(ByteBuffer buffer, String name, Charset charset) {
        ByteBuffer encName = ColumnImpl.encodeUncompressedText(name, charset);
        buffer.putShort((short) encName.remaining());
        buffer.put(encName);
    }

    /**
     * Create the usage map definition page buffer.  The "used pages" map is in
     * row 0, the "pages with free space" map is in row 1.  Index usage maps are
     * in subsequent rows.
     */
    private static void createUsageMapDefinitionBuffer(TableCreator creator) throws IOException {
        List<ColumnBuilder> lvalCols = creator.getLongValueColumns();

        // 2 table usage maps plus 1 for each index and 2 for each lval col
        int indexUmapEnd = 2 + creator.getIndexCount();
        int umapNum = indexUmapEnd + (lvalCols.size() * 2);

        JetFormat format = creator.getFormat();
        int umapRowLength = format.OFFSET_USAGE_MAP_START + format.USAGE_MAP_TABLE_BYTE_LENGTH;
        int umapSpaceUsage = getRowSpaceUsage(umapRowLength, format);
        PageChannel pageChannel = creator.getPageChannel();
        int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER;
        ByteBuffer umapBuf = null;
        int freeSpace = 0;
        int rowStart = 0;
        int umapRowNum = 0;

        for (int i = 0; i < umapNum; ++i) {

            if (umapBuf == null) {

                // need new page for usage maps
                if (umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) {
                    // first umap page has already been reserved
                    umapPageNumber = creator.getUmapPageNumber();
                } else {
                    // need another umap page
                    umapPageNumber = creator.reservePageNumber();
                }

                freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE;

                umapBuf = pageChannel.createPageBuffer();
                umapBuf.put(PageTypes.DATA);
                umapBuf.put((byte) 0x1); //Unknown
                umapBuf.putShort((short) freeSpace); //Free space in page
                umapBuf.putInt(0); //Table definition
                umapBuf.putInt(0); //Unknown
                umapBuf.putShort((short) 0); //Number of records on this page

                rowStart = findRowEnd(umapBuf, 0, format) - umapRowLength;
                umapRowNum = 0;
            }

            umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short) rowStart);

            if (i == 0) {

                // table "owned pages" map definition
                umapBuf.put(rowStart, UsageMap.MAP_TYPE_REFERENCE);

            } else if (i == 1) {

                // table "free space pages" map definition
                umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);

            } else if (i < indexUmapEnd) {

                // index umap
                int indexIdx = i - 2;
                IndexBuilder idx = creator.getIndexes().get(indexIdx);

                // allocate root page for the index
                int rootPageNumber = pageChannel.allocateNewPage();

                // stash info for later use
                TableCreator.IndexState idxState = creator.getIndexState(idx);
                idxState.setRootPageNumber(rootPageNumber);
                idxState.setUmapRowNumber((byte) umapRowNum);
                idxState.setUmapPageNumber(umapPageNumber);

                // index map definition, including initial root page
                umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);
                umapBuf.putInt(rowStart + 1, rootPageNumber);
                umapBuf.put(rowStart + 5, (byte) 1);

            } else {

                // long value column umaps
                int lvalColIdx = i - indexUmapEnd;
                int umapType = lvalColIdx % 2;
                lvalColIdx /= 2;

                ColumnBuilder lvalCol = lvalCols.get(lvalColIdx);
                TableCreator.ColumnState colState = creator.getColumnState(lvalCol);

                umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);

                if ((umapType == 1) && (umapPageNumber != colState.getUmapPageNumber())) {
                    // we want to force both usage maps for a column to be on the same
                    // data page, so just discard the previous one we wrote
                    --i;
                    umapType = 0;
                }

                if (umapType == 0) {
                    // lval column "owned pages" usage map
                    colState.setUmapOwnedRowNumber((byte) umapRowNum);
                    colState.setUmapPageNumber(umapPageNumber);
                } else {
                    // lval column "free space pages" usage map (always on same page)
                    colState.setUmapFreeRowNumber((byte) umapRowNum);
                }
            }

            rowStart -= umapRowLength;
            freeSpace -= umapSpaceUsage;
            ++umapRowNum;

            if ((freeSpace <= umapSpaceUsage) || (i == (umapNum - 1))) {
                // finish current page
                umapBuf.putShort(format.OFFSET_FREE_SPACE, (short) freeSpace);
                umapBuf.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, (short) umapRowNum);
                pageChannel.writePage(umapBuf, umapPageNumber);
                umapBuf = null;
            }
        }
    }

    /**
     * Returns a single ByteBuffer which contains the entire table definition
     * (which may span multiple database pages).
     */
    private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) throws IOException {
        int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE);
        ByteBuffer nextPageBuffer = null;
        while (nextPage != 0) {
            if (nextPageBuffer == null) {
                nextPageBuffer = getPageChannel().createPageBuffer();
            }
            getPageChannel().readPage(nextPageBuffer, nextPage);
            nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE);
            ByteBuffer newBuffer = PageChannel.createBuffer(tableBuffer.capacity() + getFormat().PAGE_SIZE - 8);
            newBuffer.put(tableBuffer);
            newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8);
            tableBuffer = newBuffer;
            tableBuffer.flip();
        }
        return tableBuffer;
    }

    private void readColumnDefinitions(ByteBuffer tableBuffer, short columnCount) throws IOException {
        int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + _indexCount * getFormat().SIZE_INDEX_DEFINITION;

        tableBuffer.position(colOffset + (columnCount * getFormat().SIZE_COLUMN_HEADER));
        List<String> colNames = new ArrayList<String>(columnCount);
        for (int i = 0; i < columnCount; i++) {
            colNames.add(readName(tableBuffer));
        }

        int dispIndex = 0;
        for (int i = 0; i < columnCount; i++) {
            ColumnImpl column = ColumnImpl.create(this, tableBuffer,
                    colOffset + (i * getFormat().SIZE_COLUMN_HEADER), colNames.get(i), dispIndex++);
            _columns.add(column);
            if (column.isVariableLength()) {
                // also shove it in the variable columns list, which is ordered
                // differently from the _columns list
                _varColumns.add(column);
            }
        }

        Collections.sort(_columns);
        getAutoNumberColumns();

        // setup the data index for the columns
        int colIdx = 0;
        for (ColumnImpl col : _columns) {
            col.setColumnIndex(colIdx++);
        }

        // sort variable length columns based on their index into the variable
        // length offset table, because we will write the columns in this order
        Collections.sort(_varColumns, VAR_LEN_COLUMN_COMPARATOR);
    }

    private void readIndexDefinitions(ByteBuffer tableBuffer) throws IOException {
        // read index column information
        for (int i = 0; i < _indexCount; i++) {
            IndexData idxData = _indexDatas.get(i);
            idxData.read(tableBuffer, _columns);
            // keep track of all columns involved in indexes
            for (IndexData.ColumnDescriptor iCol : idxData.getColumns()) {
                _indexColumns.add(iCol.getColumn());
            }
        }

        // read logical index info (may be more logical indexes than index datas)
        for (int i = 0; i < _logicalIndexCount; i++) {
            _indexes.add(new IndexImpl(tableBuffer, _indexDatas, getFormat()));
        }

        // read logical index names
        for (int i = 0; i < _logicalIndexCount; i++) {
            _indexes.get(i).setName(readName(tableBuffer));
        }

        Collections.sort(_indexes);
    }

    /**
     * Writes the given page data to the given page number, clears any other
     * relevant buffers.
     */
    private void writeDataPage(ByteBuffer pageBuffer, int pageNumber) throws IOException {
        // write the page data
        getPageChannel().writePage(pageBuffer, pageNumber);

        // possibly invalidate the add row buffer if a different data buffer is
        // being written (e.g. this happens during deleteRow)
        _addRowBufferH.possiblyInvalidate(pageNumber, pageBuffer);

        // update modification count so any active RowStates can keep themselves
        // up-to-date
        ++_modCount;
    }

    /**
     * Returns a name read from the buffer at the current position. The
     * expected name format is the name length followed by the name 
     * encoded using the {@link JetFormat#CHARSET}
     */
    private String readName(ByteBuffer buffer) {
        int nameLength = readNameLength(buffer);
        byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength);
        return ColumnImpl.decodeUncompressedText(nameBytes, getDatabase().getCharset());
    }

    /**
     * Returns a name length read from the buffer at the current position.
     */
    private int readNameLength(ByteBuffer buffer) {
        return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH);
    }

    public Object[] asRow(Map<String, ?> rowMap) {
        return asRow(rowMap, null, false);
    }

    /**
     * Converts a map of columnName -> columnValue to an array of row values
     * appropriate for a call to {@link #addRow(Object...)}, where the generated
     * RowId will be an extra value at the end of the array.
     * @see ColumnImpl#RETURN_ROW_ID
     * @usage _intermediate_method_
     */
    public Object[] asRowWithRowId(Map<String, ?> rowMap) {
        return asRow(rowMap, null, true);
    }

    public Object[] asUpdateRow(Map<String, ?> rowMap) {
        return asRow(rowMap, Column.KEEP_VALUE, false);
    }

    /**
     * @return the generated RowId added to a row of values created via {@link
     *         #asRowWithRowId}
     * @usage _intermediate_method_
     */
    public RowId getRowId(Object[] row) {
        return (RowId) row[_columns.size()];
    }

    /**
     * Converts a map of columnName -> columnValue to an array of row values.
     */
    private Object[] asRow(Map<String, ?> rowMap, Object defaultValue, boolean returnRowId) {
        int len = _columns.size();
        if (returnRowId) {
            ++len;
        }
        Object[] row = new Object[len];
        if (defaultValue != null) {
            Arrays.fill(row, defaultValue);
        }
        if (returnRowId) {
            row[len - 1] = ColumnImpl.RETURN_ROW_ID;
        }
        if (rowMap == null) {
            return row;
        }
        for (ColumnImpl col : _columns) {
            if (rowMap.containsKey(col.getName())) {
                col.setRowValue(row, col.getRowValue(rowMap));
            }
        }
        return row;
    }

    public Object[] addRow(Object... row) throws IOException {
        return addRows(Collections.singletonList(row), false).get(0);
    }

    public <M extends Map<String, Object>> M addRowFromMap(M row) throws IOException {
        Object[] rowValues = asRow(row);

        addRow(rowValues);

        returnRowValues(row, rowValues, _autoNumColumns);
        return row;
    }

    public List<? extends Object[]> addRows(List<? extends Object[]> rows) throws IOException {
        return addRows(rows, true);
    }

    public <M extends Map<String, Object>> List<M> addRowsFromMaps(List<M> rows) throws IOException {
        List<Object[]> rowValuesList = new ArrayList<Object[]>(rows.size());
        for (Map<String, Object> row : rows) {
            rowValuesList.add(asRow(row));
        }

        addRows(rowValuesList);

        if (!_autoNumColumns.isEmpty()) {
            for (int i = 0; i < rowValuesList.size(); ++i) {
                Map<String, Object> row = rows.get(i);
                Object[] rowValues = rowValuesList.get(i);
                returnRowValues(row, rowValues, _autoNumColumns);
            }
        }
        return rows;
    }

    private static void returnRowValues(Map<String, Object> row, Object[] rowValues, List<ColumnImpl> cols) {
        for (ColumnImpl col : cols) {
            col.setRowValue(row, col.getRowValue(rowValues));
        }
    }

    /**
     * Add multiple rows to this table, only writing to disk after all
     * rows have been written, and every time a data page is filled.
     * @param inRows List of Object[] row values
     */
    private List<? extends Object[]> addRows(List<? extends Object[]> rows, final boolean isBatchWrite)
            throws IOException {
        if (rows.isEmpty()) {
            return rows;
        }

        getPageChannel().startWrite();
        try {

            ByteBuffer dataPage = null;
            int pageNumber = PageChannel.INVALID_PAGE_NUMBER;
            int updateCount = 0;
            int autoNumAssignCount = 0;
            try {

                List<Object[]> dupeRows = null;
                final int numCols = _columns.size();
                for (int i = 0; i < rows.size(); i++) {

                    // we need to make sure the row is the right length and is an
                    // Object[] (fill with null if too short).  note, if the row is
                    // copied the caller will not be able to access any generated
                    // auto-number value, but if they need that info they should use a
                    // row array of the right size/type!
                    Object[] row = rows.get(i);
                    if ((row.length < numCols) || (row.getClass() != Object[].class)) {
                        row = dupeRow(row, numCols);
                        // copy the input rows to a modifiable list so we can update the
                        // elements
                        if (dupeRows == null) {
                            dupeRows = new ArrayList<Object[]>(rows);
                            rows = dupeRows;
                        }
                        // we copied the row, so put the copy back into the rows list
                        dupeRows.set(i, row);
                    }

                    // handle various value massaging activities
                    for (ColumnImpl column : _columns) {
                        if (!column.isAutoNumber()) {
                            // pass input value through column validator
                            column.setRowValue(row, column.validate(column.getRowValue(row)));
                        }
                    }

                    // fill in autonumbers
                    handleAutoNumbersForAdd(row);
                    ++autoNumAssignCount;

                    // write the row of data to a temporary buffer
                    ByteBuffer rowData = createRow(row, _writeRowBufferH.getPageBuffer(getPageChannel()));

                    int rowSize = rowData.remaining();
                    if (rowSize > getFormat().MAX_ROW_SIZE) {
                        throw new IOException("Row size " + rowSize + " is too large");
                    }

                    // get page with space
                    dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber);
                    pageNumber = _addRowBufferH.getPageNumber();

                    // determine where this row will end up on the page
                    int rowNum = getRowsOnDataPage(dataPage, getFormat());

                    RowIdImpl rowId = new RowIdImpl(pageNumber, rowNum);

                    // before we actually write the row data, we verify all the database
                    // constraints.
                    if (!_indexDatas.isEmpty()) {

                        IndexData.PendingChange idxChange = null;
                        try {

                            // handle foreign keys before adding to table
                            _fkEnforcer.addRow(row);

                            // prepare index updates
                            for (IndexData indexData : _indexDatas) {
                                idxChange = indexData.prepareAddRow(row, rowId, idxChange);
                            }

                            // complete index updates
                            IndexData.commitAll(idxChange);

                        } catch (ConstraintViolationException ce) {
                            IndexData.rollbackAll(idxChange);
                            throw ce;
                        }
                    }

                    // we have satisfied all the constraints, write the row
                    addDataPageRow(dataPage, rowSize, getFormat(), 0);
                    dataPage.put(rowData);

                    // return rowTd if desired
                    if ((row.length > numCols) && (row[numCols] == ColumnImpl.RETURN_ROW_ID)) {
                        row[numCols] = rowId;
                    }

                    ++updateCount;
                }

                writeDataPage(dataPage, pageNumber);

                // Update tdef page
                updateTableDefinition(rows.size());

            } catch (Exception rowWriteFailure) {

                boolean isWriteFailure = isWriteFailure(rowWriteFailure);

                if (!isWriteFailure && (autoNumAssignCount > updateCount)) {
                    // we assigned some autonumbers which won't get written.  attempt to
                    // recover them so we don't get ugly "holes"
                    restoreAutoNumbersFromAdd(rows.get(autoNumAssignCount - 1));
                }

                if (!isBatchWrite) {
                    // just re-throw the original exception
                    if (rowWriteFailure instanceof IOException) {
                        throw (IOException) rowWriteFailure;
                    }
                    throw (RuntimeException) rowWriteFailure;
                }

                // attempt to resolve a partial batch write
                if (isWriteFailure) {

                    // we don't really know the status of any of the rows, so clear the
                    // update count
                    updateCount = 0;

                } else if (updateCount > 0) {

                    // attempt to flush the rows already written to disk
                    try {

                        writeDataPage(dataPage, pageNumber);

                        // Update tdef page
                        updateTableDefinition(updateCount);

                    } catch (Exception flushFailure) {
                        // the flush failure is "worse" as it implies possible database
                        // corruption (failed write vs. a row failure which was not a
                        // write failure).  we don't know the status of any rows at this
                        // point (and the original failure is probably irrelevant)
                        LOG.warn("Secondary row failure which preceded the write failure", rowWriteFailure);
                        updateCount = 0;
                        rowWriteFailure = flushFailure;
                    }
                }

                throw new BatchUpdateException(updateCount, rowWriteFailure);
            }

        } finally {
            getPageChannel().finishWrite();
        }

        return rows;
    }

    private static boolean isWriteFailure(Throwable t) {
        while (t != null) {
            if ((t instanceof IOException) && !(t instanceof JackcessException)) {
                return true;
            }
            t = t.getCause();
        }
        // some other sort of exception which is not a write failure
        return false;
    }

    public Row updateRow(Row row) throws IOException {
        return updateRowFromMap(getDefaultCursor().getRowState(), (RowIdImpl) row.getId(), row);
    }

    /**
     * Update the row with the given id.  Provided RowId must have previously
     * been returned from this Table.
     * @return the given row, updated with the current row values
     * @throws IllegalStateException if the given row is not valid, or deleted.
     * @usage _intermediate_method_
     */
    public Object[] updateRow(RowId rowId, Object... row) throws IOException {
        return updateRow(getDefaultCursor().getRowState(), (RowIdImpl) rowId, row);
    }

    /**
     * Update the given column's value for the given row id.  Provided RowId
     * must have previously been returned from this Table.
     * @throws IllegalStateException if the given row is not valid, or deleted.
     * @usage _intermediate_method_
     */
    public void updateValue(Column column, RowId rowId, Object value) throws IOException {
        Object[] row = new Object[_columns.size()];
        Arrays.fill(row, Column.KEEP_VALUE);
        column.setRowValue(row, value);

        updateRow(rowId, row);
    }

    public <M extends Map<String, Object>> M updateRowFromMap(RowState rowState, RowIdImpl rowId, M row)
            throws IOException {
        Object[] rowValues = updateRow(rowState, rowId, asUpdateRow(row));
        returnRowValues(row, rowValues, _columns);
        return row;
    }

    /**
     * Update the row for the given rowId.
     * @usage _advanced_method_
     */
    public Object[] updateRow(RowState rowState, RowIdImpl rowId, Object... row) throws IOException {
        requireValidRowId(rowId);

        getPageChannel().startWrite();
        try {

            // ensure that the relevant row state is up-to-date
            ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
            int oldRowSize = rowBuffer.remaining();

            requireNonDeletedRow(rowState, rowId);

            // we need to make sure the row is the right length & type (fill with
            // null if too short).
            if ((row.length < _columns.size()) || (row.getClass() != Object[].class)) {
                row = dupeRow(row, _columns.size());
            }

            // hang on to the raw values of var length columns we are "keeping".  this
            // will allow us to re-use pre-written var length data, which can save
            // space for things like long value columns.
            Map<ColumnImpl, byte[]> keepRawVarValues = (!_varColumns.isEmpty() ? new HashMap<ColumnImpl, byte[]>()
                    : null);

            // handle various value massaging activities
            for (ColumnImpl column : _columns) {

                Object rowValue = null;
                if (column.isAutoNumber()) {

                    // fill in any auto-numbers (we don't allow autonumber values to be
                    // modified)
                    rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null);

                } else {

                    rowValue = column.getRowValue(row);
                    if (rowValue == Column.KEEP_VALUE) {

                        // fill in any "keep value" fields (restore old value)
                        rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, keepRawVarValues);

                    } else {

                        // set oldValue to something that could not possibly be a real value
                        Object oldValue = Column.KEEP_VALUE;
                        if (_indexColumns.contains(column)) {
                            // read (old) row value to help update indexes
                            oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null);
                        } else {
                            oldValue = rowState.getRowCacheValue(column.getColumnIndex());
                        }

                        // if the old value was passed back in, we don't need to validate
                        if (oldValue != rowValue) {
                            // pass input value through column validator
                            rowValue = column.validate(rowValue);
                        }
                    }
                }

                column.setRowValue(row, rowValue);
            }

            // generate new row bytes
            ByteBuffer newRowData = createRow(row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize,
                    keepRawVarValues);

            if (newRowData.limit() > getFormat().MAX_ROW_SIZE) {
                throw new IOException("Row size " + newRowData.limit() + " is too large");
            }

            if (!_indexDatas.isEmpty()) {

                IndexData.PendingChange idxChange = null;
                try {

                    Object[] oldRowValues = rowState.getRowCacheValues();

                    // check foreign keys before actually updating
                    _fkEnforcer.updateRow(oldRowValues, row);

                    // prepare index updates
                    for (IndexData indexData : _indexDatas) {
                        idxChange = indexData.prepareUpdateRow(oldRowValues, rowId, row, idxChange);
                    }

                    // complete index updates
                    IndexData.commitAll(idxChange);

                } catch (ConstraintViolationException ce) {
                    IndexData.rollbackAll(idxChange);
                    throw ce;
                }
            }

            // see if we can squeeze the new row data into the existing row
            rowBuffer.reset();
            int rowSize = newRowData.remaining();

            ByteBuffer dataPage = null;
            int pageNumber = PageChannel.INVALID_PAGE_NUMBER;

            if (oldRowSize >= rowSize) {

                // awesome, slap it in!
                rowBuffer.put(newRowData);

                // grab the page we just updated
                dataPage = rowState.getFinalPage();
                pageNumber = rowState.getFinalRowId().getPageNumber();

            } else {

                // bummer, need to find a new page for the data
                dataPage = findFreeRowSpace(rowSize, null, PageChannel.INVALID_PAGE_NUMBER);
                pageNumber = _addRowBufferH.getPageNumber();

                RowIdImpl headerRowId = rowState.getHeaderRowId();
                ByteBuffer headerPage = rowState.getHeaderPage();
                if (pageNumber == headerRowId.getPageNumber()) {
                    // new row is on the same page as header row, share page
                    dataPage = headerPage;
                }

                // write out the new row data (set the deleted flag on the new data row
                // so that it is ignored during normal table traversal)
                int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), DELETED_ROW_MASK);
                dataPage.put(newRowData);

                // write the overflow info into the header row and clear out the
                // remaining header data
                rowBuffer = PageChannel.narrowBuffer(headerPage,
                        findRowStart(headerPage, headerRowId.getRowNumber(), getFormat()),
                        findRowEnd(headerPage, headerRowId.getRowNumber(), getFormat()));
                rowBuffer.put((byte) rowNum);
                ByteUtil.put3ByteInt(rowBuffer, pageNumber);
                ByteUtil.clearRemaining(rowBuffer);

                // set the overflow flag on the header row
                int headerRowIndex = getRowStartOffset(headerRowId.getRowNumber(), getFormat());
                headerPage.putShort(headerRowIndex,
                        (short) (headerPage.getShort(headerRowIndex) | OVERFLOW_ROW_MASK));
                if (pageNumber != headerRowId.getPageNumber()) {
                    writeDataPage(headerPage, headerRowId.getPageNumber());
                }
            }

            writeDataPage(dataPage, pageNumber);

            updateTableDefinition(0);

        } finally {
            getPageChannel().finishWrite();
        }

        return row;
    }

    private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, int pageNumber) throws IOException {
        // assume incoming page is modified
        boolean modifiedPage = true;

        if (dataPage == null) {

            // find owned page w/ free space
            dataPage = findFreeRowSpace(_ownedPages, _freeSpacePages, _addRowBufferH);

            if (dataPage == null) {
                // No data pages exist (with free space).  Create a new one.
                return newDataPage();
            }

            // found a page, see if it will work
            pageNumber = _addRowBufferH.getPageNumber();
            // since we just loaded this page, it is not yet modified
            modifiedPage = false;
        }

        if (!rowFitsOnDataPage(rowSize, dataPage, getFormat())) {

            // Last data page is full.  Write old one and create a new one.
            if (modifiedPage) {
                writeDataPage(dataPage, pageNumber);
            }
            _freeSpacePages.removePageNumber(pageNumber, true);

            dataPage = newDataPage();
        }

        return dataPage;
    }

    static ByteBuffer findFreeRowSpace(UsageMap ownedPages, UsageMap freeSpacePages, TempPageHolder rowBufferH)
            throws IOException {
        // find last data page (Not bothering to check other pages for free
        // space.)
        UsageMap.PageCursor revPageCursor = ownedPages.cursor();
        revPageCursor.afterLast();
        while (true) {
            int tmpPageNumber = revPageCursor.getPreviousPage();
            if (tmpPageNumber < 0) {
                break;
            }
            // only use if actually listed in free space pages
            if (!freeSpacePages.containsPageNumber(tmpPageNumber)) {
                continue;
            }
            ByteBuffer dataPage = rowBufferH.setPage(ownedPages.getPageChannel(), tmpPageNumber);
            if (dataPage.get() == PageTypes.DATA) {
                // found last data page with free space
                return dataPage;
            }
        }

        return null;
    }

    /**
     * Updates the table definition after rows are modified.
     */
    private void updateTableDefinition(int rowCountInc) throws IOException {
        // load table definition
        ByteBuffer tdefPage = _tableDefBufferH.setPage(getPageChannel(), _tableDefPageNumber);

        // make sure rowcount and autonumber are up-to-date
        _rowCount += rowCountInc;
        tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount);
        tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber);
        int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER;
        if (ctypeOff >= 0) {
            tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber);
        }

        // write any index changes
        for (IndexData indexData : _indexDatas) {
            // write the unique entry count for the index to the table definition
            // page
            tdefPage.putInt(indexData.getUniqueEntryCountOffset(), indexData.getUniqueEntryCount());
            // write the entry page for the index
            indexData.update();
        }

        // write modified table definition
        getPageChannel().writePage(tdefPage, _tableDefPageNumber);
    }

    /**
     * Create a new data page
     * @return Page number of the new page
     */
    private ByteBuffer newDataPage() throws IOException {
        ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel());
        dataPage.put(PageTypes.DATA); //Page type
        dataPage.put((byte) 1); //Unknown
        dataPage.putShort((short) getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page
        dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition
        dataPage.putInt(0); //Unknown
        dataPage.putShort((short) 0); //Number of rows on this page
        int pageNumber = _addRowBufferH.getPageNumber();
        getPageChannel().writePage(dataPage, pageNumber);
        _ownedPages.addPageNumber(pageNumber);
        _freeSpacePages.addPageNumber(pageNumber);
        return dataPage;
    }

    protected ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) throws IOException {
        return createRow(rowArray, buffer, 0, Collections.<ColumnImpl, byte[]>emptyMap());
    }

    /**
     * Serialize a row of Objects into a byte buffer.
     * 
     * @param rowArray row data, expected to be correct length for this table
     * @param buffer buffer to which to write the row data
     * @param minRowSize min size for result row
     * @param rawVarValues optional, pre-written values for var length columns
     *                     (enables re-use of previously written values).
     * @return the given buffer, filled with the row data
     */
    private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, int minRowSize,
            Map<ColumnImpl, byte[]> rawVarValues) throws IOException {
        buffer.putShort(_maxColumnCount);
        NullMask nullMask = new NullMask(_maxColumnCount);

        //Fixed length column data comes first
        int fixedDataStart = buffer.position();
        int fixedDataEnd = fixedDataStart;
        for (ColumnImpl col : _columns) {

            if (col.isVariableLength()) {
                continue;
            }

            Object rowValue = col.getRowValue(rowArray);

            if (col.storeInNullMask()) {

                if (col.writeToNullMask(rowValue)) {
                    nullMask.markNotNull(col);
                }
                rowValue = null;
            }

            if (rowValue != null) {

                // we have a value to write
                nullMask.markNotNull(col);

                // remainingRowLength is ignored when writing fixed length data
                buffer.position(fixedDataStart + col.getFixedDataOffset());
                buffer.put(col.write(rowValue, 0));
            }

            // always insert space for the entire fixed data column length
            // (including null values), access expects the row to always be at least
            // big enough to hold all fixed values
            buffer.position(fixedDataStart + col.getFixedDataOffset() + col.getLength());

            // keep track of the end of fixed data
            if (buffer.position() > fixedDataEnd) {
                fixedDataEnd = buffer.position();
            }

        }

        // reposition at end of fixed data
        buffer.position(fixedDataEnd);

        // only need this info if this table contains any var length data
        if (_maxVarColumnCount > 0) {

            int maxRowSize = getFormat().MAX_ROW_SIZE;

            // figure out how much space remains for var length data.  first,
            // account for already written space
            maxRowSize -= buffer.position();
            // now, account for trailer space
            int trailerSize = (nullMask.byteSize() + 4 + (_maxVarColumnCount * 2));
            maxRowSize -= trailerSize;

            // for each non-null long value column we need to reserve a small
            // amount of space so that we don't end up running out of row space
            // later by being too greedy
            for (ColumnImpl varCol : _varColumns) {
                if ((varCol.getType().isLongValue()) && (varCol.getRowValue(rowArray) != null)) {
                    maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF;
                }
            }

            //Now write out variable length column data
            short[] varColumnOffsets = new short[_maxVarColumnCount];
            int varColumnOffsetsIndex = 0;
            for (ColumnImpl varCol : _varColumns) {
                short offset = (short) buffer.position();
                Object rowValue = varCol.getRowValue(rowArray);
                if (rowValue != null) {
                    // we have a value
                    nullMask.markNotNull(varCol);

                    byte[] rawValue = null;
                    ByteBuffer varDataBuf = null;
                    if (((rawValue = rawVarValues.get(varCol)) != null) && (rawValue.length <= maxRowSize)) {
                        // save time and potentially db space, re-use raw value
                        varDataBuf = ByteBuffer.wrap(rawValue);
                    } else {
                        // write column value
                        varDataBuf = varCol.write(rowValue, maxRowSize);
                    }

                    maxRowSize -= varDataBuf.remaining();
                    if (varCol.getType().isLongValue()) {
                        // we already accounted for some amount of the long value data
                        // above.  add that space back so we don't double count
                        maxRowSize += getFormat().SIZE_LONG_VALUE_DEF;
                    }
                    buffer.put(varDataBuf);
                }

                // we do a loop here so that we fill in offsets for deleted columns
                while (varColumnOffsetsIndex <= varCol.getVarLenTableIndex()) {
                    varColumnOffsets[varColumnOffsetsIndex++] = offset;
                }
            }

            // fill in offsets for any remaining deleted columns
            while (varColumnOffsetsIndex < varColumnOffsets.length) {
                varColumnOffsets[varColumnOffsetsIndex++] = (short) buffer.position();
            }

            // record where we stopped writing
            int eod = buffer.position();

            // insert padding if necessary
            padRowBuffer(buffer, minRowSize, trailerSize);

            buffer.putShort((short) eod); //EOD marker

            //Now write out variable length offsets
            //Offsets are stored in reverse order
            for (int i = _maxVarColumnCount - 1; i >= 0; i--) {
                buffer.putShort(varColumnOffsets[i]);
            }
            buffer.putShort(_maxVarColumnCount); //Number of var length columns

        } else {

            // insert padding for row w/ no var cols
            padRowBuffer(buffer, minRowSize, nullMask.byteSize());
        }

        nullMask.write(buffer); //Null mask
        buffer.flip();
        return buffer;
    }

    /**
     * Fill in all autonumber column values.
     */
    private void handleAutoNumbersForAdd(Object[] row) throws IOException {
        if (_autoNumColumns.isEmpty()) {
            return;
        }

        Object complexAutoNumber = null;
        for (ColumnImpl col : _autoNumColumns) {
            // ignore given row value, use next autonumber
            ColumnImpl.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator();
            Object rowValue = null;
            if (autoNumGen.getType() != DataType.COMPLEX_TYPE) {
                rowValue = autoNumGen.getNext(null);
            } else {
                // complex type auto numbers are shared across all complex columns
                // in the row
                complexAutoNumber = autoNumGen.getNext(complexAutoNumber);
                rowValue = complexAutoNumber;
            }
            col.setRowValue(row, rowValue);
        }
    }

    /**
     * Restores all autonumber column values from a failed add row.
     */
    private void restoreAutoNumbersFromAdd(Object[] row) throws IOException {
        if (_autoNumColumns.isEmpty()) {
            return;
        }

        for (ColumnImpl col : _autoNumColumns) {
            // restore the last value from the row
            col.getAutoNumberGenerator().restoreLast(col.getRowValue(row));
        }
    }

    private static void padRowBuffer(ByteBuffer buffer, int minRowSize, int trailerSize) {
        int pos = buffer.position();
        if ((pos + trailerSize) < minRowSize) {
            // pad the row to get to the min byte size
            int padSize = minRowSize - (pos + trailerSize);
            ByteUtil.clearRange(buffer, pos, pos + padSize);
            ByteUtil.forward(buffer, padSize);
        }
    }

    public int getRowCount() {
        return _rowCount;
    }

    int getNextLongAutoNumber() {
        // note, the saved value is the last one handed out, so pre-increment
        return ++_lastLongAutoNumber;
    }

    int getLastLongAutoNumber() {
        // gets the last used auto number (does not modify)
        return _lastLongAutoNumber;
    }

    void restoreLastLongAutoNumber(int lastLongAutoNumber) {
        // restores the last used auto number
        _lastLongAutoNumber = lastLongAutoNumber - 1;
    }

    int getNextComplexTypeAutoNumber() {
        // note, the saved value is the last one handed out, so pre-increment
        return ++_lastComplexTypeAutoNumber;
    }

    int getLastComplexTypeAutoNumber() {
        // gets the last used auto number (does not modify)
        return _lastComplexTypeAutoNumber;
    }

    void restoreLastComplexTypeAutoNumber(int lastComplexTypeAutoNumber) {
        // restores the last used auto number
        _lastComplexTypeAutoNumber = lastComplexTypeAutoNumber - 1;
    }

    @Override
    public String toString() {
        return CustomToStringStyle.builder(this)
                .append("type", (_tableType + (!isSystem() ? " (USER)" : " (SYSTEM)"))).append("name", _name)
                .append("rowCount", _rowCount).append("columnCount", _columns.size())
                .append("indexCount(data)", _indexCount).append("logicalIndexCount", _logicalIndexCount)
                .append("columns", _columns).append("indexes", _indexes).append("ownedPages", _ownedPages)
                .toString();
    }

    /**
     * @return A simple String representation of the entire table in
     *         tab-delimited format
     * @usage _general_method_
     */
    public String display() throws IOException {
        return display(Long.MAX_VALUE);
    }

    /**
     * @param limit Maximum number of rows to display
     * @return A simple String representation of the entire table in
     *         tab-delimited format
     * @usage _general_method_
     */
    public String display(long limit) throws IOException {
        reset();
        StringWriter rtn = new StringWriter();
        new ExportUtil.Builder(getDefaultCursor()).setDelimiter("\t").setHeader(true)
                .exportWriter(new BufferedWriter(rtn));
        return rtn.toString();
    }

    /**
     * Updates free space and row info for a new row of the given size in the
     * given data page.  Positions the page for writing the row data.
     * @return the row number of the new row
     * @usage _advanced_method_
     */
    public static int addDataPageRow(ByteBuffer dataPage, int rowSize, JetFormat format, int rowFlags) {
        int rowSpaceUsage = getRowSpaceUsage(rowSize, format);

        // Decrease free space record.
        short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE);
        dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - rowSpaceUsage));

        // Increment row count record.
        short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
        dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, (short) (rowCount + 1));

        // determine row position
        short rowLocation = findRowEnd(dataPage, rowCount, format);
        rowLocation -= rowSize;

        // write row position
        dataPage.putShort(getRowStartOffset(rowCount, format), (short) (rowLocation | rowFlags));

        // set position for row data
        dataPage.position(rowLocation);

        return rowCount;
    }

    /**
     * Returns the row count for the current page.  If the page is invalid
     * ({@code null}) or the page is not a DATA page, 0 is returned.
     */
    static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format) throws IOException {
        int rowsOnPage = 0;
        if ((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) {
            rowsOnPage = rowBuffer.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
        }
        return rowsOnPage;
    }

    /**
     * @throws IllegalStateException if the given rowId is invalid
     */
    private static void requireValidRowId(RowIdImpl rowId) {
        if (!rowId.isValid()) {
            throw new IllegalArgumentException("Given rowId is invalid: " + rowId);
        }
    }

    /**
     * @throws IllegalStateException if the given row is invalid or deleted
     */
    private static void requireNonDeletedRow(RowState rowState, RowIdImpl rowId) {
        if (!rowState.isValid()) {
            throw new IllegalArgumentException("Given rowId is invalid for this table: " + rowId);
        }
        if (rowState.isDeleted()) {
            throw new IllegalStateException("Row is deleted: " + rowId);
        }
    }

    /**
     * @usage _advanced_method_
     */
    public static boolean isDeletedRow(short rowStart) {
        return ((rowStart & DELETED_ROW_MASK) != 0);
    }

    /**
     * @usage _advanced_method_
     */
    public static boolean isOverflowRow(short rowStart) {
        return ((rowStart & OVERFLOW_ROW_MASK) != 0);
    }

    /**
     * @usage _advanced_method_
     */
    public static short cleanRowStart(short rowStart) {
        return (short) (rowStart & OFFSET_MASK);
    }

    /**
     * @usage _advanced_method_
     */
    public static short findRowStart(ByteBuffer buffer, int rowNum, JetFormat format) {
        return cleanRowStart(buffer.getShort(getRowStartOffset(rowNum, format)));
    }

    /**
     * @usage _advanced_method_
     */
    public static int getRowStartOffset(int rowNum, JetFormat format) {
        return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum);
    }

    /**
     * @usage _advanced_method_
     */
    public static short findRowEnd(ByteBuffer buffer, int rowNum, JetFormat format) {
        return (short) ((rowNum == 0) ? format.PAGE_SIZE
                : cleanRowStart(buffer.getShort(getRowEndOffset(rowNum, format))));
    }

    /**
     * @usage _advanced_method_
     */
    public static int getRowEndOffset(int rowNum, JetFormat format) {
        return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1));
    }

    /**
     * @usage _advanced_method_
     */
    public static int getRowSpaceUsage(int rowSize, JetFormat format) {
        return rowSize + format.SIZE_ROW_LOCATION;
    }

    private void getAutoNumberColumns() {
        for (ColumnImpl c : _columns) {
            if (c.isAutoNumber()) {
                _autoNumColumns.add(c);
            }
        }
    }

    /**
     * Returns {@code true} if a row of the given size will fit on the given
     * data page, {@code false} otherwise.
     * @usage _advanced_method_
     */
    public static boolean rowFitsOnDataPage(int rowLength, ByteBuffer dataPage, JetFormat format)
            throws IOException {
        int rowSpaceUsage = getRowSpaceUsage(rowLength, format);
        short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE);
        int rowsOnPage = getRowsOnDataPage(dataPage, format);
        return ((rowSpaceUsage <= freeSpaceInPage) && (rowsOnPage < format.MAX_NUM_ROWS_ON_DATA_PAGE));
    }

    /**
     * Duplicates and returns a row of data, optionally with a longer length
     * filled with {@code null}.
     */
    static Object[] dupeRow(Object[] row, int newRowLength) {
        Object[] copy = new Object[newRowLength];
        System.arraycopy(row, 0, copy, 0, Math.min(row.length, newRowLength));
        return copy;
    }

    /** various statuses for the row data */
    private enum RowStatus {
        INIT, INVALID_PAGE, INVALID_ROW, VALID, DELETED, NORMAL, OVERFLOW;
    }

    /** the phases the RowState moves through as the data is parsed */
    private enum RowStateStatus {
        INIT, AT_HEADER, AT_FINAL;
    }

    /**
     * Maintains the state of reading a row of data.
     * @usage _advanced_class_
     */
    public final class RowState implements ErrorHandler.Location {
        /** Buffer used for reading the header row data pages */
        private final TempPageHolder _headerRowBufferH;
        /** the header rowId */
        private RowIdImpl _headerRowId = RowIdImpl.FIRST_ROW_ID;
        /** the number of rows on the header page */
        private int _rowsOnHeaderPage;
        /** the rowState status */
        private RowStateStatus _status = RowStateStatus.INIT;
        /** the row status */
        private RowStatus _rowStatus = RowStatus.INIT;
        /** buffer used for reading overflow pages */
        private final TempPageHolder _overflowRowBufferH = TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
        /** the row buffer which contains the final data (after following any
            overflow pointers) */
        private ByteBuffer _finalRowBuffer;
        /** the rowId which contains the final data (after following any overflow
            pointers) */
        private RowIdImpl _finalRowId = null;
        /** true if the row values array has data */
        private boolean _haveRowValues;
        /** values read from the last row */
        private final Object[] _rowValues;
        /** null mask for the last row */
        private NullMask _nullMask;
        /** last modification count seen on the table we track this so that the
            rowState can detect updates to the table and re-read any buffered
            data */
        private int _lastModCount;
        /** optional error handler to use when row errors are encountered */
        private ErrorHandler _errorHandler;
        /** cached variable column offsets for jump-table based rows */
        private short[] _varColOffsets;

        private RowState(TempBufferHolder.Type headerType) {
            _headerRowBufferH = TempPageHolder.newHolder(headerType);
            _rowValues = new Object[TableImpl.this.getColumnCount()];
            _lastModCount = TableImpl.this._modCount;
        }

        public TableImpl getTable() {
            return TableImpl.this;
        }

        public ErrorHandler getErrorHandler() {
            return ((_errorHandler != null) ? _errorHandler : getTable().getErrorHandler());
        }

        public void setErrorHandler(ErrorHandler newErrorHandler) {
            _errorHandler = newErrorHandler;
        }

        public void reset() {
            _finalRowId = null;
            _finalRowBuffer = null;
            _rowsOnHeaderPage = 0;
            _status = RowStateStatus.INIT;
            _rowStatus = RowStatus.INIT;
            _varColOffsets = null;
            _nullMask = null;
            if (_haveRowValues) {
                Arrays.fill(_rowValues, null);
                _haveRowValues = false;
            }
        }

        public boolean isUpToDate() {
            return (TableImpl.this._modCount == _lastModCount);
        }

        private void checkForModification() {
            if (!isUpToDate()) {
                reset();
                _headerRowBufferH.invalidate();
                _overflowRowBufferH.invalidate();
                _lastModCount = TableImpl.this._modCount;
            }
        }

        private ByteBuffer getFinalPage() throws IOException {
            if (_finalRowBuffer == null) {
                // (re)load current page
                _finalRowBuffer = getHeaderPage();
            }
            return _finalRowBuffer;
        }

        public RowIdImpl getFinalRowId() {
            if (_finalRowId == null) {
                _finalRowId = getHeaderRowId();
            }
            return _finalRowId;
        }

        private void setRowStatus(RowStatus rowStatus) {
            _rowStatus = rowStatus;
        }

        public boolean isValid() {
            return (_rowStatus.ordinal() >= RowStatus.VALID.ordinal());
        }

        public boolean isDeleted() {
            return (_rowStatus == RowStatus.DELETED);
        }

        public boolean isOverflow() {
            return (_rowStatus == RowStatus.OVERFLOW);
        }

        public boolean isHeaderPageNumberValid() {
            return (_rowStatus.ordinal() > RowStatus.INVALID_PAGE.ordinal());
        }

        public boolean isHeaderRowNumberValid() {
            return (_rowStatus.ordinal() > RowStatus.INVALID_ROW.ordinal());
        }

        private void setStatus(RowStateStatus status) {
            _status = status;
        }

        public boolean isAtHeaderRow() {
            return (_status.ordinal() >= RowStateStatus.AT_HEADER.ordinal());
        }

        public boolean isAtFinalRow() {
            return (_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal());
        }

        private Object setRowCacheValue(int idx, Object value) {
            _haveRowValues = true;
            _rowValues[idx] = value;
            return value;
        }

        private Object getRowCacheValue(int idx) {
            Object value = _rowValues[idx];
            // only return immutable values.  mutable values could have been
            // modified externally and therefore could return an incorrect value
            return (ColumnImpl.isImmutableValue(value) ? value : null);
        }

        public Object[] getRowCacheValues() {
            return dupeRow(_rowValues, _rowValues.length);
        }

        public NullMask getNullMask(ByteBuffer rowBuffer) throws IOException {
            if (_nullMask == null) {
                _nullMask = getRowNullMask(rowBuffer);
            }
            return _nullMask;
        }

        private short[] getVarColOffsets() {
            return _varColOffsets;
        }

        private void setVarColOffsets(short[] varColOffsets) {
            _varColOffsets = varColOffsets;
        }

        public RowIdImpl getHeaderRowId() {
            return _headerRowId;
        }

        public int getRowsOnHeaderPage() {
            return _rowsOnHeaderPage;
        }

        private ByteBuffer getHeaderPage() throws IOException {
            checkForModification();
            return _headerRowBufferH.getPage(getPageChannel());
        }

        private ByteBuffer setHeaderRow(RowIdImpl rowId) throws IOException {
            checkForModification();

            // don't do any work if we are already positioned correctly
            if (isAtHeaderRow() && (getHeaderRowId().equals(rowId))) {
                return (isValid() ? getHeaderPage() : null);
            }

            // rejigger everything
            reset();
            _headerRowId = rowId;
            _finalRowId = rowId;

            int pageNumber = rowId.getPageNumber();
            int rowNumber = rowId.getRowNumber();
            if ((pageNumber < 0) || !_ownedPages.containsPageNumber(pageNumber)) {
                setRowStatus(RowStatus.INVALID_PAGE);
                return null;
            }

            _finalRowBuffer = _headerRowBufferH.setPage(getPageChannel(), pageNumber);
            _rowsOnHeaderPage = getRowsOnDataPage(_finalRowBuffer, getFormat());

            if ((rowNumber < 0) || (rowNumber >= _rowsOnHeaderPage)) {
                setRowStatus(RowStatus.INVALID_ROW);
                return null;
            }

            setRowStatus(RowStatus.VALID);
            return _finalRowBuffer;
        }

        private ByteBuffer setOverflowRow(RowIdImpl rowId) throws IOException {
            // this should never see modifications because it only happens within
            // the positionAtRowData method
            if (!isUpToDate()) {
                throw new IllegalStateException("Table modified while searching?");
            }
            if (_rowStatus != RowStatus.OVERFLOW) {
                throw new IllegalStateException("Row is not an overflow row?");
            }
            _finalRowId = rowId;
            _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(), rowId.getPageNumber());
            return _finalRowBuffer;
        }

        private Object handleRowError(ColumnImpl column, byte[] columnData, Exception error) throws IOException {
            return getErrorHandler().handleRowError(column, columnData, this, error);
        }

        @Override
        public String toString() {
            return CustomToStringStyle.valueBuilder(this).append("headerRowId", _headerRowId)
                    .append("finalRowId", _finalRowId).toString();
        }
    }

}