]> source.dussan.org Git - jackcess.git/commitdiff
move Table internals to TableImpl
authorJames Ahlborn <jtahlborn@yahoo.com>
Sun, 3 Mar 2013 03:43:37 +0000 (03:43 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Sun, 3 Mar 2013 03:43:37 +0000 (03:43 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@669 f203690c-595d-4dc9-a70b-905162fa7fd2

21 files changed:
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/Cursor.java
src/java/com/healthmarketscience/jackcess/CursorBuilder.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/DatabaseImpl.java
src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java
src/java/com/healthmarketscience/jackcess/ErrorHandler.java
src/java/com/healthmarketscience/jackcess/FKEnforcer.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/IndexCursor.java
src/java/com/healthmarketscience/jackcess/IndexData.java
src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TableCreator.java
src/java/com/healthmarketscience/jackcess/TableImpl.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/UsageMap.java
src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java
src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java

index d6f71c3463e2c336e55fb0e617d0660e8763db6a..4a8ac4765da3c808ed4e76abf211bd4605f212ed 100644 (file)
@@ -187,7 +187,7 @@ public class Column implements Comparable<Column> {
 
   
   /** owning table */
-  private final Table _table;
+  private final TableImpl _table;
   /** Whether or not the column is of variable length */
   private boolean _variableLength;
   /** Whether or not the column is an autonumber column */
@@ -236,7 +236,7 @@ public class Column implements Comparable<Column> {
   /**
    * Only used by unit tests
    */
-  Column(boolean testing, Table table) {
+  Column(boolean testing, TableImpl table) {
     if(!testing) {
       throw new IllegalArgumentException();
     }
@@ -250,7 +250,7 @@ public class Column implements Comparable<Column> {
    * @param offset Offset in the buffer at which the column definition starts
    * @usage _advanced_method_
    */
-  public Column(Table table, ByteBuffer buffer, int offset, int displayIndex)
+  public Column(TableImpl table, ByteBuffer buffer, int offset, int displayIndex)
     throws IOException
   {
     _table = table;
@@ -327,7 +327,7 @@ public class Column implements Comparable<Column> {
   /**
    * @usage _general_method_
    */
-  public Table getTable() {
+  public TableImpl getTable() {
     return _table;
   }
 
@@ -933,8 +933,8 @@ public class Column implements Comparable<Column> {
         {
           getPageChannel().readPage(lvalPage, pageNum);
 
-          short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat());
-          short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat());
+          short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat());
+          short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat());
 
           if((rowEnd - rowStart) != length) {
             throw new IOException("Unexpected lval row length");
@@ -953,8 +953,8 @@ public class Column implements Comparable<Column> {
           lvalPage.clear();
           getPageChannel().readPage(lvalPage, pageNum);
 
-          short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat());
-          short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat());
+          short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat());
+          short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat());
           
           // read next page information
           lvalPage.position(rowStart);
@@ -1325,7 +1325,7 @@ public class Column implements Comparable<Column> {
       case LONG_VALUE_TYPE_OTHER_PAGE:
         lvalPage = getLongValuePage(value.length, lvalBufferH);
         firstLvalPageNum = lvalBufferH.getPageNumber();
-        firstLvalRow = (byte)Table.addDataPageRow(lvalPage, value.length,
+        firstLvalRow = (byte)TableImpl.addDataPageRow(lvalPage, value.length,
                                                   getFormat(), 0);
         lvalPage.put(value);
         getPageChannel().writePage(lvalPage, firstLvalPageNum);
@@ -1363,7 +1363,7 @@ public class Column implements Comparable<Column> {
           }
 
           // add row to this page
-          byte lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4,
+          byte lvalRow = (byte)TableImpl.addDataPageRow(lvalPage, chunkLength + 4,
                                                     getFormat(), 0);
           
           // write next page info (we'll always be writing into row 0 for
@@ -1437,7 +1437,7 @@ public class Column implements Comparable<Column> {
     ByteBuffer lvalPage = null;
     if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) {
       lvalPage = lvalBufferH.getPage(getPageChannel());
-      if(Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
+      if(TableImpl.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
         // the current page has space
         return lvalPage;
       }
@@ -2066,7 +2066,7 @@ public class Column implements Comparable<Column> {
 
       int position = buffer.position();
       buffer.put(col.getType().getValue());
-      buffer.putInt(Table.MAGIC_TABLE_NUMBER);  //constant magic number
+      buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER);  //constant magic number
       buffer.putShort(columnNumber);  //Column Number
       if (col.isVariableLength()) {
         if(!col.getType().isLongValue()) {
@@ -2118,7 +2118,7 @@ public class Column implements Comparable<Column> {
       }
     }
     for (Column col : columns) {
-      Table.writeName(buffer, col.getName(), creator.getCharset());
+      TableImpl.writeName(buffer, col.getName(), creator.getCharset());
     }
   }
 
index 042241a8c5109744bd990ec389b0db62044ae089..162e7993bf656f84150d5168c0b4362cad297701 100644 (file)
@@ -34,7 +34,7 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
-import com.healthmarketscience.jackcess.Table.RowState;
+import com.healthmarketscience.jackcess.TableImpl.RowState;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -75,7 +75,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   /** identifier for this cursor */
   private final Id _id;
   /** owning table */
-  private final Table _table;
+  private final TableImpl _table;
   /** State used for reading the table rows */
   private final RowState _rowState;
   /** the first (exclusive) row id for this cursor */
@@ -89,7 +89,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   /** ColumnMatcher to be used when matching column values */
   protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE;
 
-  protected Cursor(Id id, Table table, Position firstPos, Position lastPos) {
+  protected Cursor(Id id, TableImpl table, Position firstPos, Position lastPos) {
     _id = id;
     _table = table;
     _rowState = _table.createRowState();
@@ -103,7 +103,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    * Creates a normal, un-indexed cursor for the given table.
    * @param table the table over which this cursor will traverse
    */
-  public static Cursor createCursor(Table table) {
+  public static Cursor createCursor(TableImpl table) {
     return new TableScanCursor(table);
   }
 
@@ -118,7 +118,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    * @param index index for the table which will define traversal order as
    *              well as enhance certain lookups
    */
-  public static Cursor createIndexCursor(Table table, Index index)
+  public static Cursor createIndexCursor(TableImpl table, Index index)
     throws IOException
   {
     return IndexCursor.createCursor(table, index);
@@ -140,7 +140,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    * @param endRow the last row of data for the cursor (inclusive), or
    *               {@code null} for the last entry
    */
-  public static Cursor createIndexCursor(Table table, Index index,
+  public static Cursor createIndexCursor(TableImpl table, Index index,
                                          Object[] startRow, Object[] endRow)
     throws IOException
   {
@@ -165,7 +165,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    *               the last entry
    * @param endInclusive whether or not endRow is inclusive or exclusive
    */
-  public static Cursor createIndexCursor(Table table, Index index,
+  public static Cursor createIndexCursor(TableImpl table, Index index,
                                          Object[] startRow,
                                          boolean startInclusive,
                                          Object[] endRow,
@@ -188,7 +188,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    * @param rowPattern pattern to be used to find the row
    * @return the matching row or {@code null} if a match could not be found.
    */
-  public static Map<String,Object> findRow(Table table,
+  public static Map<String,Object> findRow(TableImpl table,
                                            Map<String,?> rowPattern)
     throws IOException
   {
@@ -216,7 +216,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    *                     desired row
    * @return the matching row or {@code null} if a match could not be found.
    */
-  public static Object findValue(Table table, Column column,
+  public static Object findValue(TableImpl table, Column column,
                                  Column columnPattern, Object valuePattern)
     throws IOException
   {
@@ -240,7 +240,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    * @param rowPattern pattern to be used to find the row
    * @return the matching row or {@code null} if a match could not be found.
    */
-  public static Map<String,Object> findRow(Table table, Index index,
+  public static Map<String,Object> findRow(TableImpl table, Index index,
                                            Map<String,?> rowPattern)
     throws IOException
   {
@@ -269,7 +269,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    *                     desired row
    * @return the matching row or {@code null} if a match could not be found.
    */
-  public static Object findValue(Table table, Index index, Column column,
+  public static Object findValue(TableImpl table, Index index, Column column,
                                  Column columnPattern, Object valuePattern)
     throws IOException
   {
@@ -284,7 +284,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     return _id;
   }
 
-  public Table getTable() {
+  public TableImpl getTable() {
     return _table;
   }
 
@@ -438,7 +438,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   {
     // we need to ensure that the "deleted" flag has been read for this row
     // (or re-read if the table has been recently modified)
-    Table.positionAtRowData(_rowState, _curPos.getRowId());
+    TableImpl.positionAtRowData(_rowState, _curPos.getRowId());
     return _rowState.isDeleted();
   }
   
@@ -826,7 +826,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     _rowState.reset();
     _prevPos = _curPos;
     _curPos = findAnotherPosition(_rowState, _curPos, moveForward);
-    Table.positionAtRowHeader(_rowState, _curPos.getRowId());
+    TableImpl.positionAtRowHeader(_rowState, _curPos.getRowId());
     return(!_curPos.equals(getDirHandler(moveForward).getEndPosition()));
   }
   
@@ -1330,7 +1330,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     /** Cursor over the pages that this table owns */
     private final UsageMap.PageCursor _ownedPagesCursor;
     
-    private TableScanCursor(Table table) {
+    private TableScanCursor(TableImpl table) {
       super(new Id(table, null), table,
             FIRST_SCAN_POSITION, LAST_SCAN_POSITION);
       _ownedPagesCursor = table.getOwnedPagesCursor();
@@ -1376,7 +1376,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
       // figure out how many rows are left on this page so we can find the
       // next row
       RowId curRowId = curPos.getRowId();
-      Table.positionAtRowHeader(rowState, curRowId);
+      TableImpl.positionAtRowHeader(rowState, curRowId);
       int currentRowNumber = curRowId.getRowNumber();
     
       // loop until we find the next valid row or run out of pages
@@ -1384,14 +1384,14 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
 
         currentRowNumber = handler.getAnotherRowNumber(currentRowNumber);
         curRowId = new RowId(curRowId.getPageNumber(), currentRowNumber);
-        Table.positionAtRowHeader(rowState, curRowId);
+        TableImpl.positionAtRowHeader(rowState, curRowId);
         
         if(!rowState.isValid()) {
           
           // load next page
           curRowId = new RowId(handler.getAnotherPageNumber(),
                                RowId.INVALID_ROW_NUMBER);
-          Table.positionAtRowHeader(rowState, curRowId);
+          TableImpl.positionAtRowHeader(rowState, curRowId);
           
           if(!rowState.isHeaderPageNumberValid()) {
             //No more owned pages.  No more rows.
@@ -1486,7 +1486,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     private final String _tableName;
     private final String _indexName;
 
-    protected Id(Table table, Index index) {
+    protected Id(TableImpl table, Index index) {
       _tableName = table.getName();
       _indexName = ((index != null) ? index.getName() : null);
     }
index 4e955d0535bcb563618eac481f291af849189618..f0d7348fdf558170fa35c8b4b006392dbcfb8f08 100644 (file)
@@ -278,9 +278,9 @@ public class CursorBuilder {
   {
     Cursor cursor = null;
     if(_index == null) {
-      cursor = Cursor.createCursor(_table);
+      cursor = Cursor.createCursor((TableImpl)_table);
     } else {
-      cursor = Cursor.createIndexCursor(_table, _index,
+      cursor = Cursor.createIndexCursor((TableImpl)_table, _index,
                                         _startRow, _startRowInclusive,
                                         _endRow, _endRowInclusive);
     }
index ca16047ad7461c66bbaff212810d7943c1c1bbcc..7857a879ad2617912846c86d26ef8bea80224547 100644 (file)
@@ -40,8 +40,13 @@ import com.healthmarketscience.jackcess.query.Query;
  * the data via the relevant {@link Table}.  When a Database instance is no
  * longer useful, it should <b>always</b> be closed ({@link #close}) to avoid
  * corruption.
+ * <p>
+ * Note, Database instances (and all the related objects) are <i>not</i>
+ * thread-safe.  However, separate Database instances (and their respective
+ * objects) can be used by separate threads without a problem.
  *
  * @author James Ahlborn
+ * @usage _general_class_
  */
 public abstract class Database implements Iterable<Table>, Closeable, Flushable
 {
@@ -109,7 +114,7 @@ public abstract class Database implements Iterable<Table>, Closeable, Flushable
    */
   public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() {
       public Object handleRowError(Column column, byte[] columnData,
-                                   Table.RowState rowState, Exception error)
+                                   Location location, Exception error)
         throws IOException
       {
         // really can only be RuntimeException or IOException
index 2aa0208086008fe3989159af6532ef6405b0b617..cbb5c7f57a7fe854d41fe1ba231cb05cba54844b 100644 (file)
@@ -300,17 +300,17 @@ public class DatabaseImpl extends Database
   /** Reads and writes database pages */
   private final PageChannel _pageChannel;
   /** System catalog table */
-  private Table _systemCatalog;
+  private TableImpl _systemCatalog;
   /** utility table finder */
   private TableFinder _tableFinder;
   /** System access control entries table (initialized on first use) */
-  private Table _accessControlEntries;
+  private TableImpl _accessControlEntries;
   /** System relationships table (initialized on first use) */
-  private Table _relationships;
+  private TableImpl _relationships;
   /** System queries table (initialized on first use) */
-  private Table _queries;
+  private TableImpl _queries;
   /** System complex columns table (initialized on first use) */
-  private Table _complexCols;
+  private TableImpl _complexCols;
   /** SIDs to use for the ACEs added for new tables */
   private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>();
   /** optional error handler to use when row errors are encountered */
@@ -566,12 +566,12 @@ public class DatabaseImpl extends Database
   }
   
   @Override
-  public Table getSystemCatalog() {
+  public TableImpl getSystemCatalog() {
     return _systemCatalog;
   }
   
   @Override
-  public Table getAccessControlEntries() throws IOException {
+  public TableImpl getAccessControlEntries() throws IOException {
     if(_accessControlEntries == null) {
       _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES);
       if(_accessControlEntries == null) {
@@ -587,7 +587,7 @@ public class DatabaseImpl extends Database
    * @return the complex column system table (loaded on demand)
    * @usage _advanced_method_
    */
-  public Table getSystemComplexColumns() throws IOException {
+  public TableImpl getSystemComplexColumns() throws IOException {
     if(_complexCols == null) {
       _complexCols = getSystemTable(TABLE_SYSTEM_COMPLEX_COLS);
       if(_complexCols == null) {
@@ -881,7 +881,7 @@ public class DatabaseImpl extends Database
   }
 
   @Override
-  public Table getTable(String name) throws IOException {
+  public TableImpl getTable(String name) throws IOException {
     return getTable(name, false);
   }
 
@@ -890,10 +890,10 @@ public class DatabaseImpl extends Database
    * @return The table, or null if it doesn't exist
    * @usage _advanced_method_
    */
-  public Table getTable(int tableDefPageNumber) throws IOException {
+  public TableImpl getTable(int tableDefPageNumber) throws IOException {
 
     // first, check for existing table
-    Table table = _tableCache.get(tableDefPageNumber);
+    TableImpl table = _tableCache.get(tableDefPageNumber);
     if(table != null) {
       return table;
     }
@@ -916,7 +916,7 @@ public class DatabaseImpl extends Database
    * @param includeSystemTables whether to consider returning a system table
    * @return The table, or null if it doesn't exist
    */
-  private Table getTable(String name, boolean includeSystemTables) 
+  private TableImpl getTable(String name, boolean includeSystemTables) 
     throws IOException 
   {
     TableInfo tableInfo = lookupTable(name);
@@ -1115,7 +1115,7 @@ public class DatabaseImpl extends Database
   }
 
   @Override
-  public Table getSystemTable(String tableName) throws IOException
+  public TableImpl getSystemTable(String tableName) throws IOException
   {
     return getTable(tableName, true);
   }
@@ -1382,11 +1382,11 @@ public class DatabaseImpl extends Database
   /**
    * Reads a table with the given name from the given pageNumber.
    */
-  private Table readTable(String name, int pageNumber, int flags)
+  private TableImpl readTable(String name, int pageNumber, int flags)
     throws IOException
   {
     // first, check for existing table
-    Table table = _tableCache.get(pageNumber);
+    TableImpl table = _tableCache.get(pageNumber);
     if(table != null) {
       return table;
     }
@@ -1402,7 +1402,7 @@ public class DatabaseImpl extends Database
             ", but page type is " + pageType);
       }
       return _tableCache.put(
-          new Table(this, buffer, pageNumber, name, flags));
+          new TableImpl(this, buffer, pageNumber, name, flags));
     } finally {
       releaseSharedBuffer(buffer);
     }
@@ -1413,7 +1413,7 @@ public class DatabaseImpl extends Database
    * an existing index), otherwise a simple table cursor.
    */
   private static Cursor createCursorWithOptionalIndex(
-      Table table, String colName, Object colValue)
+      TableImpl table, String colName, Object colValue)
     throws IOException
   {
     try {
@@ -2041,12 +2041,12 @@ public class DatabaseImpl extends Database
    * WeakReference for a Table which holds the table pageNumber (for later
    * cache purging).
    */
-  private static final class WeakTableReference extends WeakReference<Table>
+  private static final class WeakTableReference extends WeakReference<TableImpl>
   {
     private final Integer _pageNumber;
 
-    private WeakTableReference(Integer pageNumber, Table table, 
-                               ReferenceQueue<Table> queue) {
+    private WeakTableReference(Integer pageNumber, TableImpl table, 
+                               ReferenceQueue<TableImpl> queue) {
       super(table, queue);
       _pageNumber = pageNumber;
     }
@@ -2063,14 +2063,15 @@ public class DatabaseImpl extends Database
   {
     private final Map<Integer,WeakTableReference> _tables = 
       new HashMap<Integer,WeakTableReference>();
-    private final ReferenceQueue<Table> _queue = new ReferenceQueue<Table>();
+    private final ReferenceQueue<TableImpl> _queue = 
+      new ReferenceQueue<TableImpl>();
 
-    public Table get(Integer pageNumber) {
+    public TableImpl get(Integer pageNumber) {
       WeakTableReference ref = _tables.get(pageNumber);
       return ((ref != null) ? ref.get() : null);
     }
 
-    public Table put(Table table) {
+    public TableImpl put(TableImpl table) {
       purgeOldRefs();
   
       Integer pageNumber = table.getTableDefPageNumber();
index 2fbd478b284495c7629132c612910a16515fe484..55ea72977a57aa05b7806dc689468bc852fa76fc 100644 (file)
@@ -60,21 +60,19 @@ public class DebugErrorHandler extends ReplacementErrorHandler
   }
 
   @Override
-  public Object handleRowError(Column column,
-                               byte[] columnData,
-                               Table.RowState rowState,
-                               Exception error)
+  public Object handleRowError(Column column, byte[] columnData, 
+                               Location location, Exception error)
     throws IOException
   {
     if(LOG.isDebugEnabled()) {
       LOG.debug("Failed reading column " + column + ", row " +
-                rowState + ", bytes " +
+                location + ", bytes " +
                 ((columnData != null) ?
                  ByteUtil.toHexString(columnData) : "null"),
                 error);
     }
 
-    return super.handleRowError(column, columnData, rowState, error);
+    return super.handleRowError(column, columnData, location, error);
   }
 
 }
index 25c4d9d046b60c55b9b2fdd22f7e86ce4996e975..052dac152756782adb205b4849e715e329a19389 100644 (file)
@@ -51,15 +51,30 @@ public interface ErrorHandler
    * @param columnData the actual column data for the column being read (which
    *                   may be {@code null} depending on when the exception
    *                   was thrown during the reading process)
-   * @param rowState the current row state for the caller
+   * @param location the current location of the error
    * @param error the error that was encountered
    *
    * @return replacement for this row's column
    */
   public Object handleRowError(Column column,
                                byte[] columnData,
-                               Table.RowState rowState,
+                               Location location,
                                Exception error)
     throws IOException;
 
+  /**
+   * Provides location information for an error.
+   */
+  public interface Location 
+  {
+    /**
+     * @return the table in which the error occurred
+     */
+    public Table getTable();
+
+    /**
+     * Contains details about the errored row, useful for debugging.
+     */
+    public String toString();
+  }
 }
index b5ce3ec58f2c7ede63896350d4599d47dfc76745..8aa9f01686591c920291020a277804e596ae05f6 100644 (file)
@@ -44,7 +44,7 @@ final class FKEnforcer
   private static final ColumnMatcher MATCHER =
     CaseInsensitiveColumnMatcher.INSTANCE;
 
-  private final Table _table;
+  private final TableImpl _table;
   private final List<Column> _cols;
   private List<Joiner> _primaryJoinersChkUp;
   private List<Joiner> _primaryJoinersChkDel;
@@ -52,7 +52,7 @@ final class FKEnforcer
   private List<Joiner> _primaryJoinersDoDel;
   private List<Joiner> _secondaryJoiners;
 
-  FKEnforcer(Table table) {
+  FKEnforcer(TableImpl table) {
     _table = table;
 
     // at this point, only init the index columns
index 9fc24c3399b7272eed69bf33c5024493a933c233..fec1bd9ef5fbc797e985cef5ecd38470ba6d7918 100644 (file)
@@ -115,7 +115,7 @@ public class Index implements Comparable<Index> {
     return _data;
   }
 
-  public Table getTable() {
+  public TableImpl getTable() {
     return getIndexData().getTable();
   }
   
@@ -173,7 +173,7 @@ public class Index implements Comparable<Index> {
       return null;
     }
 
-    Table refTable = getTable().getDatabase().getTable(
+    TableImpl refTable = getTable().getDatabase().getTable(
         _reference.getOtherTablePageNumber());
 
     if(refTable == null) {
@@ -399,7 +399,7 @@ public class Index implements Comparable<Index> {
     // write logical index information
     for(IndexBuilder idx : creator.getIndexes()) {
       TableCreator.IndexState idxState = creator.getIndexState(idx);
-      buffer.putInt(Table.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
+      buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
       buffer.putInt(idxState.getIndexNumber()); // index num
       buffer.putInt(idxState.getIndexDataNumber()); // index data num
       buffer.put((byte)0); // related table type
@@ -413,7 +413,7 @@ public class Index implements Comparable<Index> {
 
     // write index names
     for(IndexBuilder idx : creator.getIndexes()) {
-      Table.writeName(buffer, idx.getName(), creator.getCharset());
+      TableImpl.writeName(buffer, idx.getName(), creator.getCharset());
     }
   }
 
index aa77b652fe9010b258c196c3ab2c9b7a5326c5ac..016a9c64f55294a74aa0eba1f92f1421b4f9be9b 100644 (file)
@@ -27,7 +27,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
-import com.healthmarketscience.jackcess.Table.RowState;
+import com.healthmarketscience.jackcess.TableImpl.RowState;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -53,7 +53,7 @@ public class IndexCursor extends Cursor
   /** column names for the index entry columns */
   private Set<String> _indexEntryPattern;
 
-  private IndexCursor(Table table, Index index,
+  private IndexCursor(TableImpl table, Index index,
                       IndexData.EntryCursor entryCursor)
     throws IOException
   {
@@ -76,7 +76,7 @@ public class IndexCursor extends Cursor
    * @param index index for the table which will define traversal order as
    *              well as enhance certain lookups
    */
-  public static IndexCursor createCursor(Table table, Index index)
+  public static IndexCursor createCursor(TableImpl table, Index index)
     throws IOException
   {
     return createCursor(table, index, null, null);
@@ -99,7 +99,7 @@ public class IndexCursor extends Cursor
    *               {@code null} for the last entry
    */
   public static IndexCursor createCursor(
-      Table table, Index index, Object[] startRow, Object[] endRow)
+      TableImpl table, Index index, Object[] startRow, Object[] endRow)
     throws IOException
   {
     return createCursor(table, index, startRow, true, endRow, true);
@@ -123,7 +123,7 @@ public class IndexCursor extends Cursor
    *               the last entry
    * @param endInclusive whether or not endRow is inclusive or exclusive
    */
-  public static IndexCursor createCursor(Table table, Index index,
+  public static IndexCursor createCursor(TableImpl table, Index index,
                                          Object[] startRow,
                                          boolean startInclusive,
                                          Object[] endRow,
index e3d508c9beeddb37467767948e455ecc9fd1f8e9..ca22be4b387c4a7d21291bec8f3aec142ba7f209 100644 (file)
@@ -141,7 +141,7 @@ public class IndexData {
         
   
   /** owning table */
-  private final Table _table;
+  private final TableImpl _table;
   /** 0-based index data number */
   private final int _number;
   /** Page number of the root index data */
@@ -181,7 +181,7 @@ public class IndexData {
   /** Cache which manages the index pages */
   private final IndexPageCache _pageCache;
   
-  protected IndexData(Table table, int number, int uniqueEntryCount,
+  protected IndexData(TableImpl table, int number, int uniqueEntryCount,
                       int uniqueEntryCountOffset)
   {
     _table  = table;
@@ -196,7 +196,7 @@ public class IndexData {
    * Creates an IndexData appropriate for the given table, using information
    * from the given table definition buffer.
    */
-  public static IndexData create(Table table, ByteBuffer tableBuffer,
+  public static IndexData create(TableImpl table, ByteBuffer tableBuffer,
                                  int number, JetFormat format)
     throws IOException
   {
@@ -208,7 +208,7 @@ public class IndexData {
     return new IndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset);
   }
 
-  public Table getTable() {
+  public TableImpl getTable() {
     return _table;
   }
   
index bdb003cb02a709bc76843fcbcf721a5603dc1500..443de4e2c7d0bba996f8fa87c358d44a0c17c7ec 100644 (file)
@@ -56,10 +56,8 @@ public class ReplacementErrorHandler implements ErrorHandler
     _replacement = replacement;
   }
 
-  public Object handleRowError(Column column,
-                               byte[] columnData,
-                               Table.RowState rowState,
-                               Exception error)
+  public Object handleRowError(Column column, byte[] columnData,
+                               Location location, Exception error)
     throws IOException
   {
     return _replacement;
index 18303df56311fd234eece31666cb94ae8fcd76b4..2c0c3fde067d5f9c8cd97a5a3263bdb17061516d 100644 (file)
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2005 Health Market Science, Inc.
+Copyright (c) 2013 James Ahlborn
 
 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Lesser General Public
@@ -15,72 +15,21 @@ You should have received a copy of the GNU Lesser General Public
 License along with this library; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 USA
-
-You can contact Health Market Science at info@healthmarketscience.com
-or at the following address:
-
-Health Market Science
-2700 Horizon Drive
-Suite 200
-King of Prussia, PA 19406
 */
 
 package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.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.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
 
 /**
- * A single database table
- * <p>
- * Is not thread-safe.
- * 
- * @author Tim McCune
+ *
+ * @author James Ahlborn
  * @usage _general_class_
  */
-public class Table
-  implements Iterable<Map<String, Object>>
+public abstract class Table implements Iterable<Map<String, Object>>
 {
-  
-  private static final Log LOG = LogFactory.getLog(Table.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;
-
   /**
    * enum which controls the ordering of the columns in a table.
    * @usage _intermediate_class_
@@ -94,183 +43,27 @@ public class Table
     DISPLAY;
   }
 
-  /** comparator which sorts variable length columns based on their index into
-      the variable length offset table */
-  private static final Comparator<Column> VAR_LEN_COLUMN_COMPARATOR =
-    new Comparator<Column>() {
-      public int compare(Column c1, Column 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<Column> DISPLAY_ORDER_COMPARATOR =
-    new Comparator<Column>() {
-      public int compare(Column c1, Column 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 int _flags;
-  /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */
-  private byte _tableType;
-  /** Number of actual indexes on the table */
-  private int _indexCount;
-  /** Number of logical indexes for the table */
-  private int _logicalIndexCount;
-  /** 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;
-  /** page number of the definition of this table */
-  private final int _tableDefPageNumber;
-  /** max Number of columns in the table (includes previous deletions) */
-  private short _maxColumnCount;
-  /** max Number of variable columns in the table */
-  private short _maxVarColumnCount;
-  /** List of columns in this table, ordered by column number */
-  private List<Column> _columns = new ArrayList<Column>();
-  /** List of variable length columns in this table, ordered by offset */
-  private final List<Column> _varColumns = new ArrayList<Column>();
-  /** List of autonumber columns in this table, ordered by column number */
-  private List<Column> _autoNumColumns;
-  /** List of indexes on this table (multiple logical indexes may be backed by
-      the same index data) */
-  private final List<Index> _indexes = new ArrayList<Index>();
-  /** 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<Column> _indexColumns = new LinkedHashSet<Column>();
-  /** Table name as stored in Database */
-  private final String _name;
-  /** Usage map of pages that this table owns */
-  private UsageMap _ownedPages;
-  /** Usage map of pages that this table owns with free space on them */
-  private UsageMap _freeSpacePages;
-  /** 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 single rows of data */
-  private final TempBufferHolder _singleRowBufferH =
-    TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
-  /** "buffer" used to writing multi rows of data (will create new buffer on
-      every call) */
-  private final TempBufferHolder _multiRowBufferH =
-    TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true);
-  /** page buffer used to write out-of-line "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;
-
-  /** common cursor for iterating through the table, kept here for historic
-      reasons */
-  private Cursor _cursor;
-  
-  /**
-   * Only used by unit tests
-
-   */
-  Table(boolean testing, List<Column> columns) throws IOException {
-    if(!testing) {
-      throw new IllegalArgumentException();
-    }
-    _database = null;
-    _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
-    _name = null;
-    setColumns(columns);
-    _fkEnforcer = 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 Table(DatabaseImpl database, ByteBuffer tableBuffer,
-                  int pageNumber, String name, int flags)
-    throws IOException
-  {
-    _database = database;
-    _tableDefPageNumber = pageNumber;
-    _name = name;
-    _flags = flags;
-    readTableDefinition(loadCompleteTableDefinitionBuffer(tableBuffer));
-    _fkEnforcer = new FKEnforcer(this);
-  }
-
   /**
    * @return The name of the table
    * @usage _general_method_
    */
-  public String getName() {
-    return _name;
-  }
+  public abstract String getName();
 
   /**
    * Whether or not this table has been marked as hidden.
    * @usage _general_method_
    */
-  public boolean isHidden() {
-    return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0);
-  }
+  public abstract boolean isHidden();
 
-  /**
-   * @usage _advanced_method_
-   */
-  public int getMaxColumnCount() {
-    return _maxColumnCount;
-  }
-  
-  /**
-   * @usage _general_method_
-   */
-  public int getColumnCount() {
-    return _columns.size();
-  }
-  
   /**
    * @usage _general_method_
    */
-  public DatabaseImpl getDatabase() {
-    return _database;
-  }
-  
-  /**
-   * @usage _advanced_method_
-   */
-  public JetFormat getFormat() {
-    return getDatabase().getFormat();
-  }
+  public abstract int getColumnCount();
 
   /**
-   * @usage _advanced_method_
+   * @usage _general_method_
    */
-  public PageChannel getPageChannel() {
-    return getDatabase().getPageChannel();
-  }
+  public abstract Database getDatabase();
 
   /**
    * Gets the currently configured ErrorHandler (always non-{@code null}).
@@ -278,154 +71,45 @@ public class Table
    * level.
    * @usage _intermediate_method_
    */
-  public ErrorHandler getErrorHandler() {
-    return((_tableErrorHandler != null) ? _tableErrorHandler :
-           getDatabase().getErrorHandler());
-  }
+  public abstract ErrorHandler getErrorHandler();
 
   /**
    * Sets a new ErrorHandler.  If {@code null}, resets to using the
    * ErrorHandler configured at the Database level.
    * @usage _intermediate_method_
    */
-  public void setErrorHandler(ErrorHandler newErrorHandler) {
-    _tableErrorHandler = newErrorHandler;
-  }    
-
-  public int getTableDefPageNumber() {
-    return _tableDefPageNumber;
-  }
-
-  /**
-   * @usage _advanced_method_
-   */
-  public RowState createRowState() {
-    return new RowState(TempBufferHolder.Type.HARD);
-  }
-
-  protected 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;
-    // 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 abstract void setErrorHandler(ErrorHandler newErrorHandler);
 
   /**
    * @return All of the columns in this table (unmodifiable List)
    * @usage _general_method_
    */
-  public List<Column> getColumns() {
-    return Collections.unmodifiableList(_columns);
-  }
+  public abstract List<Column> getColumns();
 
   /**
    * @return the column with the given name
    * @usage _general_method_
    */
-  public Column getColumn(String name) {
-    for(Column column : _columns) {
-      if(column.getName().equalsIgnoreCase(name)) {
-        return column;
-      }
-    }
-    throw new IllegalArgumentException("Column with name " + name +
-                                       " does not exist in this table");
-  }
-    
-  /**
-   * Only called by unit tests
-   */
-  private void setColumns(List<Column> columns) {
-    _columns = columns;
-    int colIdx = 0;
-    int varLenIdx = 0;
-    int fixedOffset = 0;
-    for(Column col : _columns) {
-      col.setColumnNumber((short)colIdx);
-      col.setColumnIndex(colIdx++);
-      if(col.isVariableLength()) {
-        col.setVarLenTableIndex(varLenIdx++);
-        _varColumns.add(col);
-      } else {
-        col.setFixedDataOffset(fixedOffset);
-        fixedOffset += col.getType().getFixedSize();
-      }
-    }
-    _maxColumnCount = (short)_columns.size();
-    _maxVarColumnCount = (short)_varColumns.size();
-    _autoNumColumns = getAutoNumberColumns(columns);
-  }
+  public abstract Column getColumn(String name);
 
   /**
    * @return the properties for this table
    * @usage _general_method_
    */
-  public PropertyMap getProperties() throws IOException {
-    if(_props == null) {
-      _props = getPropertyMaps().getDefault();
-    }
-    return _props;
-  }
+  public abstract PropertyMap getProperties() throws IOException;
 
-  /**
-   * @return all PropertyMaps for this table (and columns)
-   * @usage _general_method_
-   */
-  protected PropertyMaps getPropertyMaps() throws IOException {
-    if(_propertyMaps == null) {
-      _propertyMaps = getDatabase().getPropertiesForObject(
-          _tableDefPageNumber);
-    }
-    return _propertyMaps;
-  }
-  
   /**
    * @return All of the Indexes on this table (unmodifiable List)
    * @usage _intermediate_method_
    */
-  public List<Index> getIndexes() {
-    return Collections.unmodifiableList(_indexes);
-  }
+  public abstract List<Index> getIndexes();
 
   /**
    * @return the index with the given name
    * @throws IllegalArgumentException if there is no index with the given name
    * @usage _intermediate_method_
    */
-  public Index getIndex(String name) {
-    for(Index index : _indexes) {
-      if(index.getName().equalsIgnoreCase(name)) {
-        return index;
-      }
-    }
-    throw new IllegalArgumentException("Index with name " + name +
-                                       " does not exist on this table");
-  }
+  public abstract Index getIndex(String name);
 
   /**
    * @return the primary key index for this table
@@ -433,971 +117,30 @@ public class Table
    *         table
    * @usage _intermediate_method_
    */
-  public Index getPrimaryKeyIndex() {
-    for(Index index : _indexes) {
-      if(index.isPrimaryKey()) {
-        return index;
-      }
-    }
-    throw new IllegalArgumentException("Table " + getName() +
-                                       " does not have a primary key index");
-  }
-  
+  public abstract Index getPrimaryKeyIndex();
+
   /**
    * @return the foreign key index joining this table to the given other table
    * @throws IllegalArgumentException if there is no relationship between this
    *         table and the given table
    * @usage _intermediate_method_
    */
-  public Index getForeignKeyIndex(Table otherTable) {
-    for(Index index : _indexes) {
-      if(index.isForeignKey() && (index.getReference() != null) &&
-         (index.getReference().getOtherTablePageNumber() ==
-          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)
-   */
-  List<IndexData> getIndexDatas() {
-    return Collections.unmodifiableList(_indexDatas);
-  }
-
-  /**
-   * Only called by unit tests
-   */
-  int getLogicalIndexCount() {
-    return _logicalIndexCount;
-  }
-
-  private Cursor getInternalCursor() {
-    if(_cursor == null) {
-      _cursor = Cursor.createCursor(this);
-    }
-    return _cursor;
-  }
-  
-  /**
-   * After calling this method, getNextRow will return the first row in the
-   * table, see {@link Cursor#reset}.
-   * @usage _general_method_
-   */
-  public void reset() {
-    getInternalCursor().reset();
-  }
-  
-  /**
-   * Delete the current row (retrieved by a call to {@link #getNextRow()}).
-   * @usage _general_method_
-   */
-  public void deleteCurrentRow() throws IOException {
-    getInternalCursor().deleteCurrentRow();
-  }
-
-  /**
-   * Delete the row on which the given rowState is currently positioned.
-   * <p>
-   * Note, this method is not generally meant to be used directly.  You should
-   * use the {@link #deleteCurrentRow} method or use the Cursor class, which
-   * allows for more complex table interactions.
-   * @usage _advanced_method_
-   */
-  public void deleteRow(RowState rowState, RowId rowId) throws IOException {
-    requireValidRowId(rowId);
-    
-    // ensure that the relevant row state is up-to-date
-    ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId);
-
-    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(Column idxCol : _indexColumns) {
-        getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null);
-      }
-
-      // use any read rowValues to help update the indexes
-      rowValues = rowState.getRowValues();
-
-      // 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);
-  }
-  
-  /**
-   * @return The next row in this table (Column name -> Column value)
-   * @usage _general_method_
-   */
-  public Map<String, Object> getNextRow() throws IOException {
-    return getNextRow(null);
-  }
-
-  /**
-   * @param columnNames Only column names in this collection will be returned
-   * @return The next row in this table (Column name -> Column value)
-   * @usage _general_method_
-   */
-  public Map<String, Object> getNextRow(Collection<String> columnNames) 
-    throws IOException
-  {
-    return getInternalCursor().getNextRow(columnNames);
-  }
-  
-  /**
-   * Reads a single column from the given row.
-   * <p>
-   * Note, this method is not generally meant to be used directly.  Instead
-   * use the Cursor class, which allows for more complex table interactions,
-   * e.g. {@link Cursor#getCurrentRowValue}.
-   * @usage _advanced_method_
-   */
-  public Object getRowValue(RowState rowState, RowId rowId, Column 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 Map<String, Object> getRow(
-      RowState rowState, RowId 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 Map<String, Object> getRow(
-         JetFormat format,
-      RowState rowState,
-      ByteBuffer rowBuffer,
-      Collection<Column> columns,
-      Collection<String> columnNames)
-    throws IOException
-  {
-    Map<String, Object> rtn = new LinkedHashMap<String, Object>(
-        columns.size());
-    for(Column 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,
-                                     Column column,
-                                     RowState rowState,
-                                     Map<Column,byte[]> rawVarValues)
-    throws IOException
-  {
-    byte[] columnData = null;
-    try {
-
-      NullMask nullMask = rowState.getNullMask(rowBuffer);
-      boolean isNull = nullMask.isNull(column);
-      if(column.getType() == DataType.BOOLEAN) {
-          // Boolean values are stored in the null mask.  see note about
-          // caching below
-        return rowState.setRowValue(column.getColumnIndex(),
-                                    Boolean.valueOf(!isNull));
-      } else if(isNull) {
-        // well, that's easy! (no need to update cache w/ null)
-        return null;
-      }
-
-      // 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.setRowValue(column.getColumnIndex(), 
-                                  column.read(columnData));
-
-    } catch(Exception e) {
-
-      // cache "raw" row value.  see note about caching above
-      rowState.setRowValue(column.getColumnIndex(), 
-                           Column.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, RowId 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, RowId 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 RowId(overflowPageNum, overflowRowNum));
-        rowNum = overflowRowNum;
-      
-      } else {
-
-        rowState.setStatus(RowStateStatus.AT_FINAL);
-        return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd);
-      }
-    }
-  }
-
-  
-  /**
-   * Calls <code>reset</code> on this table and returns a modifiable
-   * Iterator which will iterate through all the rows of this table.  Use of
-   * the Iterator follows the same restrictions as a call to
-   * <code>getNextRow</code>.
-   * @throws IllegalStateException if an IOException is thrown by one of the
-   *         operations, the actual exception will be contained within
-   * @usage _general_method_
-   */
-  public Iterator<Map<String, Object>> iterator()
-  {
-    return iterator(null);
-  }
-  
-  /**
-   * Calls <code>reset</code> on this table and returns a modifiable
-   * Iterator which will iterate through all the rows of this table, returning
-   * only the given columns.  Use of the Iterator follows the same
-   * restrictions as a call to <code>getNextRow</code>.
-   * @throws IllegalStateException if an IOException is thrown by one of the
-   *         operations, the actual exception will be contained within
-   * @usage _general_method_
-   */
-  public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
-  {
-    reset();
-    return getInternalCursor().iterator(columnNames);
-  }
-
-  /**
-   * 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 totalTableDefSize = format.SIZE_TDEF_HEADER +
-      (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + 
-      idxDataLen + 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(Column 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
-    Column.writeDefinitions(creator, buffer); 
-    
-    if(creator.hasIndexes()) {
-      // index and index data definitions
-      IndexData.writeDefinitions(creator, buffer);
-      Index.writeDefinitions(creator, buffer);
-    }
-
-    //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<Column> 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(Table.TYPE_USER); //Table type
-    buffer.putShort((short) columns.size()); //Max columns a row will have
-    buffer.putShort(Column.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
-    if (LOG.isDebugEnabled()) {
-      int position = buffer.position();
-      buffer.rewind();
-      LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString(
-                buffer, creator.getFormat().SIZE_TDEF_HEADER));
-      buffer.position(position);
-    }
-  }
+  public abstract Index getForeignKeyIndex(Table otherTable);
 
-  /**
-   * 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 = Column.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
-  {
-    // 2 table usage maps plus 1 for each index
-    int umapNum = 2 + creator.getIndexCount();
-
-    JetFormat format = creator.getFormat();
-    int usageMapRowLength = format.OFFSET_USAGE_MAP_START +
-      format.USAGE_MAP_TABLE_BYTE_LENGTH;
-    int freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE
-      - (umapNum * getRowSpaceUsage(usageMapRowLength, format));
-    
-    // for now, don't handle writing that many indexes
-    if(freeSpace < 0) {
-      throw new IOException("FIXME attempting to write too many indexes");
-    }
-
-    int umapPageNumber = creator.getUmapPageNumber();
-
-    PageChannel pageChannel = creator.getPageChannel();
-    ByteBuffer rtn = pageChannel.createPageBuffer();
-    rtn.put(PageTypes.DATA);
-    rtn.put((byte) 0x1);  //Unknown
-    rtn.putShort((short)freeSpace);  //Free space in page
-    rtn.putInt(0); //Table definition
-    rtn.putInt(0); //Unknown
-    rtn.putShort((short) umapNum); //Number of records on this page
-
-    // write two rows of usage map definitions for the table
-    int rowStart = findRowEnd(rtn, 0, format) - usageMapRowLength;
-    for(int i = 0; i < 2; ++i) {
-      rtn.putShort(getRowStartOffset(i, format), (short)rowStart);
-      if(i == 0) {
-        // initial "usage pages" map definition
-        rtn.put(rowStart, UsageMap.MAP_TYPE_REFERENCE);
-      } else {
-        // initial "pages with free space" map definition
-        rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE);
-      }
-      rowStart -= usageMapRowLength;
-    }
-
-    if(creator.hasIndexes()) {
-      
-      for(int i = 0; i < creator.getIndexes().size(); ++i) {
-        IndexBuilder idx = creator.getIndexes().get(i);
-
-        // allocate root page for the index
-        int rootPageNumber = pageChannel.allocateNewPage();
-        int umapRowNum = i + 2;
-
-        // 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
-        rtn.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart);
-        rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE);
-        rtn.putInt(rowStart + 1, rootPageNumber);
-        rtn.put(rowStart + 5, (byte)1);
-
-        rowStart -= usageMapRowLength;
-      }
-    }
-
-    pageChannel.writePage(rtn, umapPageNumber);
-  }
-
-  /**
-   * 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 = getPageChannel().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;
-  }
-    
-  /**
-   * Read the table definition
-   */
-  private void readTableDefinition(ByteBuffer tableBuffer) throws IOException
-  {
-    if (LOG.isDebugEnabled()) {
-      tableBuffer.rewind();
-      LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer,
-          getFormat().SIZE_TDEF_HEADER));
-    }
-    _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);
-
-    int rowNum = ByteUtil.getUnsignedByte(
-        tableBuffer, getFormat().OFFSET_OWNED_PAGES);
-    int pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_OWNED_PAGES + 1);
-    _ownedPages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
-    rowNum = ByteUtil.getUnsignedByte(
-        tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES);
-    pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES + 1);
-    _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
-    
-    for (int i = 0; i < _indexCount; i++) {
-      _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat()));
-    }
-    
-    int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK +
-        _indexCount * getFormat().SIZE_INDEX_DEFINITION;
-    int dispIndex = 0;
-    for (int i = 0; i < columnCount; i++) {
-      Column column = new Column(this, tableBuffer,
-          colOffset + (i * getFormat().SIZE_COLUMN_HEADER), 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);
-      }
-    }
-    tableBuffer.position(colOffset +
-                         (columnCount * getFormat().SIZE_COLUMN_HEADER));
-    for (int i = 0; i < columnCount; i++) {
-      Column column = _columns.get(i);
-      column.setName(readName(tableBuffer));
-    }    
-    Collections.sort(_columns);
-    _autoNumColumns = getAutoNumberColumns(_columns);
-
-    // setup the data index for the columns
-    int colIdx = 0;
-    for(Column 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);
-
-    // 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 Index(tableBuffer, _indexDatas, getFormat()));
-    }
-
-    // read logical index names
-    for (int i = 0; i < _logicalIndexCount; i++) {
-      _indexes.get(i).setName(readName(tableBuffer));
-    }
-    
-    Collections.sort(_indexes);
-
-    // re-sort columns if necessary
-    if(getDatabase().getColumnOrder() != ColumnOrder.DATA) {
-      Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
-    }
-
-    for(Column col : _columns) {
-      // some columns need to do extra work after the table is completely
-      // loaded
-      col.postTableLoadInit();
-    }
-  }
-  
-  /**
-   * 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 Column.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);
-  }
-  
   /**
    * Converts a map of columnName -> columnValue to an array of row values
    * appropriate for a call to {@link #addRow(Object...)}.
    * @usage _general_method_
    */
-  public Object[] asRow(Map<String,?> rowMap) {
-    return asRow(rowMap, null);
-  }
-  
+  public abstract Object[] asRow(Map<String,?> rowMap);
+
   /**
    * Converts a map of columnName -> columnValue to an array of row values
    * appropriate for a call to {@link #updateCurrentRow(Object...)}.
    * @usage _general_method_
    */
-  public Object[] asUpdateRow(Map<String,?> rowMap) {
-    return asRow(rowMap, Column.KEEP_VALUE);
-  }
+  public abstract Object[] asUpdateRow(Map<String,?> rowMap);
 
-  /**
-   * Converts a map of columnName -> columnValue to an array of row values.
-   */
-  private Object[] asRow(Map<String,?> rowMap, Object defaultValue)
-  {
-    Object[] row = new Object[_columns.size()];
-    if(defaultValue != null) {
-      Arrays.fill(row, defaultValue);
-    }
-    if(rowMap == null) {
-      return row;
-    }
-    for(Column col : _columns) {
-      if(rowMap.containsKey(col.getName())) {
-        col.setRowValue(row, col.getRowValue(rowMap));
-      }
-    }
-    return row;
-  }
-  
   /**
    * Adds a single row to this table and writes it to disk.  The values are
    * expected to be given in the order that the Columns are listed by the
@@ -1416,10 +159,8 @@ public class Table
    *            otherwise it will not be modified.
    * @usage _general_method_
    */
-  public void addRow(Object... row) throws IOException {
-    addRows(Collections.singletonList(row), _singleRowBufferH);
-  }
-  
+  public abstract void addRow(Object... row) throws IOException;
+
   /**
    * Add multiple rows to this table, only writing to disk after all
    * rows have been written, and every time a data page is filled.  This
@@ -1436,1072 +177,10 @@ public class Table
    *             will not be modified.
    * @usage _general_method_
    */
-  public void addRows(List<? extends Object[]> rows) throws IOException {
-    addRows(rows, _multiRowBufferH);
-  }
-  
-  /**
-   * 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
-   * @param writeRowBufferH TempBufferHolder used to generate buffers for
-   *                        writing the row data
-   */
-  private void addRows(List<? extends Object[]> inRows,
-                       TempBufferHolder writeRowBufferH)
-    throws IOException
-  {
-    if(inRows.isEmpty()) {
-      return;
-    }
-
-    // copy the input rows to a modifiable list so we can update the elements
-    List<Object[]> rows = new ArrayList<Object[]>(inRows);
-    ByteBuffer[] rowData = new ByteBuffer[rows.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 < _columns.size()) || (row.getClass() != Object[].class)) {
-        row = dupeRow(row, _columns.size());
-        // we copied the row, so put the copy back into the rows list
-        rows.set(i, row);
-      }
-
-      // fill in autonumbers
-      handleAutoNumbersForAdd(row);
-      
-      // write the row of data to a temporary buffer
-      rowData[i] = createRow(row, writeRowBufferH.getPageBuffer(getPageChannel()));
-      
-      if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) {
-        throw new IOException("Row size " + rowData[i].limit() +
-                              " is too large");
-      }
-    }
-
-    ByteBuffer dataPage = null;
-    int pageNumber = PageChannel.INVALID_PAGE_NUMBER;
-    
-    for (int i = 0; i < rowData.length; i++) {
-      int rowSize = rowData[i].remaining();
-      Object[] row = rows.get(i);
-
-      // handle foreign keys before adding to table
-      _fkEnforcer.addRow(row);
-
-      // get page with space
-      dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber);
-      pageNumber = _addRowBufferH.getPageNumber();
-
-      // write out the row data
-      int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0);
-      dataPage.put(rowData[i]);
-
-      // update the indexes
-      RowId rowId = new RowId(pageNumber, rowNum);
-      for(IndexData indexData : _indexDatas) {
-        indexData.addRow(row, rowId);
-      }
-    }
-
-    writeDataPage(dataPage, pageNumber);
-    
-    // Update tdef page
-    updateTableDefinition(rows.size());
-  }
+  public abstract void addRows(List<? extends Object[]> rows) throws IOException;
 
   /**
-   * Updates the current row to the new values.
-   * <p>
-   * Note, if this table has an auto-number column(s), the existing value(s)
-   * will be maintained, unchanged.
-   *
-   * @param row new row values for the current row.
    * @usage _general_method_
    */
-  public void updateCurrentRow(Object... row) throws IOException {
-     getInternalCursor().updateCurrentRow(row);
-  }
-  
-  /**
-   * Update the row on which the given rowState is currently positioned.
-   * <p>
-   * Note, this method is not generally meant to be used directly.  You should
-   * use the {@link #updateCurrentRow} method or use the Cursor class, which
-   * allows for more complex table interactions, e.g.
-   * {@link Cursor#setCurrentRowValue} and {@link Cursor#updateCurrentRow}.
-   * @usage _advanced_method_
-   */
-  public void updateRow(RowState rowState, RowId rowId, Object... row) 
-    throws IOException 
-  {
-    requireValidRowId(rowId);
-    
-    // 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<Column,byte[]> keepRawVarValues = 
-      (!_varColumns.isEmpty() ? new HashMap<Column,byte[]>() : null);
-
-    for(Column column : _columns) {
-      if(_autoNumColumns.contains(column)) {
-        // fill in any auto-numbers (we don't allow autonumber values to be
-        // modified)
-        column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, 
-                                             rowState, null));
-      } else if(column.getRowValue(row) == Column.KEEP_VALUE) {
-        // fill in any "keep value" fields
-        column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column,
-                                             rowState, keepRawVarValues));
-      } else if(_indexColumns.contains(column)) {
-        // read row value to help update indexes
-        getRowColumn(getFormat(), rowBuffer, column, rowState, null);
-      }
-    }
-
-    // generate new row bytes
-    ByteBuffer newRowData = createRow(
-        row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize,
-        keepRawVarValues);
-
-    if (newRowData.limit() > getFormat().MAX_ROW_SIZE) {
-      throw new IOException("Row size " + newRowData.limit() + 
-                            " is too large");
-    }
-
-    if(!_indexDatas.isEmpty()) {
-
-      Object[] oldRowValues = rowState.getRowValues();
-
-      // check foreign keys before actually updating
-      _fkEnforcer.updateRow(oldRowValues, row);
-
-      // delete old values from indexes
-      for(IndexData indexData : _indexDatas) {
-        indexData.deleteRow(oldRowValues, rowId);
-      }
-    }
-    
-    // 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();
-
-      RowId 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());
-      }
-    }
-
-    // update the indexes
-    for(IndexData indexData : _indexDatas) {
-      indexData.addRow(row, rowId);
-    }
-
-    writeDataPage(dataPage, pageNumber);
-
-    updateTableDefinition(0);
-  }
-   
-  private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, 
-                                      int pageNumber)
-    throws IOException
-  {
-    if(dataPage == null) {
-
-      // 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;
-        }
-        dataPage = _addRowBufferH.setPage(getPageChannel(), tmpPageNumber);
-        if(dataPage.get() == PageTypes.DATA) {
-          // found last data page, only use if actually listed in free space
-          // pages
-          if(_freeSpacePages.containsPageNumber(tmpPageNumber)) {
-            pageNumber = tmpPageNumber;
-          }
-          break;
-        }
-      }
-
-      if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) {
-        // No data pages exist (with free space).  Create a new one.
-        return newDataPage();
-      }
-    
-    }
-
-    if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) {
-
-      // Last data page is full.  Create a new one.
-      writeDataPage(dataPage, pageNumber);
-      _freeSpacePages.removePageNumber(pageNumber);
-
-      dataPage = newDataPage();
-    }
-
-    return dataPage;
-  }
-  /**
-   * 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 {
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("Creating new data page");
-    }
-    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;
-  }
-  
-  ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer)
-    throws IOException
-  {
-    return createRow(rowArray, buffer, 0, Collections.<Column,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<Column,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 (Column col : _columns) {
-
-      if(col.isVariableLength()) {
-        continue;
-      }
-        
-      Object rowValue = col.getRowValue(rowArray);
-
-      if (col.getType() == DataType.BOOLEAN) {
-        
-        if(Column.toBooleanValue(rowValue)) {
-          //Booleans are stored in the null mask
-          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 (Column 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 (Column 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();
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit()));
-    }
-    return buffer;
-  }
-
-  /**
-   * Fill in all autonumber column values.
-   */
-  private void handleAutoNumbersForAdd(Object[] row)
-    throws IOException
-  {
-    if(_autoNumColumns.isEmpty()) {
-      return;
-    }
-
-    Object complexAutoNumber = null;
-    for(Column col : _autoNumColumns) {
-      // ignore given row value, use next autonumber
-      Column.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);
-    }
-  }
-
-  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);
-    }
-  }
-
-  /**
-   * @usage _general_method_
-   */
-  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;
-  }
-  
-  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;
-  }
-  
-  @Override
-  public String toString() {
-    StringBuilder rtn = new StringBuilder();
-    rtn.append("Type: " + _tableType + 
-               ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)"));
-    rtn.append("\nName: " + _name);
-    rtn.append("\nRow count: " + _rowCount);
-    rtn.append("\nColumn count: " + _columns.size());
-    rtn.append("\nIndex (data) count: " + _indexCount);
-    rtn.append("\nLogical Index count: " + _logicalIndexCount);
-    rtn.append("\nColumns:\n");
-    for(Column col : _columns) {
-      rtn.append(col);
-    }
-    rtn.append("\nIndexes:\n");
-    for(Index index : _indexes) {
-      rtn.append(index);
-    }
-    rtn.append("\nOwned pages: " + _ownedPages + "\n");
-    return rtn.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();
-    StringBuilder rtn = new StringBuilder();
-    for(Iterator<Column> iter = _columns.iterator(); iter.hasNext(); ) {
-      Column col = iter.next();
-      rtn.append(col.getName());
-      if (iter.hasNext()) {
-        rtn.append("\t");
-      }
-    }
-    rtn.append("\n");
-    Map<String, Object> row;
-    int rowCount = 0;
-    while ((rowCount++ < limit) && (row = getNextRow()) != null) {
-      for(Iterator<Object> iter = row.values().iterator(); iter.hasNext(); ) {
-        Object obj = iter.next();
-        if (obj instanceof byte[]) {
-          byte[] b = (byte[]) obj;
-          rtn.append(ByteUtil.toHexString(b));
-          //This block can be used to easily dump a binary column to a file
-          /*java.io.File f = java.io.File.createTempFile("ole", ".bin");
-            java.io.FileOutputStream out = new java.io.FileOutputStream(f);
-            out.write(b);
-            out.flush();
-            out.close();*/
-        } else {
-          rtn.append(String.valueOf(obj));
-        }
-        if (iter.hasNext()) {
-          rtn.append("\t");
-        }
-      }
-      rtn.append("\n");
-    }
-    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.
-   */
-  private 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(RowId 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, RowId 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;
-  }
-
-  /**
-   * @return the "AutoNumber" columns in the given collection of columns.
-   * @usage _advanced_method_
-   */
-  public static List<Column> getAutoNumberColumns(Collection<Column> columns) {
-    List<Column> autoCols = new ArrayList<Column>(1);
-    for(Column c : columns) {
-      if(c.isAutoNumber()) {
-        autoCols.add(c);
-      }
-    }
-    return (!autoCols.isEmpty() ? autoCols : Collections.<Column>emptyList());
-  }
-
-  /**
-   * 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
-  {
-    /** Buffer used for reading the header row data pages */
-    private final TempPageHolder _headerRowBufferH;
-    /** the header rowId */
-    private RowId _headerRowId = RowId.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 RowId _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[Table.this.getColumnCount()];
-      _lastModCount = Table.this._modCount;
-    }
-
-    public Table getTable() {
-      return Table.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(Table.this._modCount == _lastModCount);
-    }
-    
-    private void checkForModification() {
-      if(!isUpToDate()) {
-        reset();
-        _headerRowBufferH.invalidate();
-        _overflowRowBufferH.invalidate();
-        _lastModCount = Table.this._modCount;
-      }
-    }
-    
-    private ByteBuffer getFinalPage()
-      throws IOException
-    {
-      if(_finalRowBuffer == null) {
-        // (re)load current page
-        _finalRowBuffer = getHeaderPage();
-      }
-      return _finalRowBuffer;
-    }
-
-    public RowId 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 setRowValue(int idx, Object value) {
-      _haveRowValues = true;
-      _rowValues[idx] = value;
-      return value;
-    }
-    
-    public Object[] getRowValues() {
-      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 RowId getHeaderRowId() {
-      return _headerRowId;
-    }
-
-    public int getRowsOnHeaderPage() {
-      return _rowsOnHeaderPage;
-    }
-    
-    private ByteBuffer getHeaderPage()
-      throws IOException
-    {
-      checkForModification();
-      return _headerRowBufferH.getPage(getPageChannel());
-    }
-
-    private ByteBuffer setHeaderRow(RowId 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(RowId 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(Column column,
-                                  byte[] columnData,
-                                  Exception error)
-      throws IOException
-    {
-      return getErrorHandler().handleRowError(column, columnData,
-                                              this, error);
-    }  
-
-    @Override
-    public String toString()
-    {
-      return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " +
-        _finalRowId;
-    }
-  }
-  
+  public abstract int getRowCount();
 }
index bc458dee614c71e1598a3f46833a54bbd4430e14..bdf793efe58f982ab87ba44b0e4d57818e2161bb 100644 (file)
@@ -129,7 +129,7 @@ class TableCreator
     _umapPageNumber = reservePageNumber();
     
     //Write the tdef page to disk.
-    Table.writeTableDefinition(this);
+    TableImpl.writeTableDefinition(this);
 
     // update the database with the new table info
     _database.addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, null, null);
@@ -182,7 +182,7 @@ class TableCreator
       }
     }
 
-    List<Column> autoCols = Table.getAutoNumberColumns(_columns);
+    List<Column> autoCols = TableImpl.getAutoNumberColumns(_columns);
     if(autoCols.size() > 1) {
       // for most autonumber types, we can only have one of each type
       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
diff --git a/src/java/com/healthmarketscience/jackcess/TableImpl.java b/src/java/com/healthmarketscience/jackcess/TableImpl.java
new file mode 100644 (file)
index 0000000..35a5067
--- /dev/null
@@ -0,0 +1,2412 @@
+/*
+Copyright (c) 2005 Health Market Science, Inc.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+
+You can contact Health Market Science at info@healthmarketscience.com
+or at the following address:
+
+Health Market Science
+2700 Horizon Drive
+Suite 200
+King of Prussia, PA 19406
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.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.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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 extends 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<Column> VAR_LEN_COLUMN_COMPARATOR =
+    new Comparator<Column>() {
+      public int compare(Column c1, Column 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<Column> DISPLAY_ORDER_COMPARATOR =
+    new Comparator<Column>() {
+      public int compare(Column c1, Column 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 int _flags;
+  /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */
+  private byte _tableType;
+  /** Number of actual indexes on the table */
+  private int _indexCount;
+  /** Number of logical indexes for the table */
+  private int _logicalIndexCount;
+  /** 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;
+  /** page number of the definition of this table */
+  private final int _tableDefPageNumber;
+  /** max Number of columns in the table (includes previous deletions) */
+  private short _maxColumnCount;
+  /** max Number of variable columns in the table */
+  private short _maxVarColumnCount;
+  /** List of columns in this table, ordered by column number */
+  private List<Column> _columns = new ArrayList<Column>();
+  /** List of variable length columns in this table, ordered by offset */
+  private final List<Column> _varColumns = new ArrayList<Column>();
+  /** List of autonumber columns in this table, ordered by column number */
+  private List<Column> _autoNumColumns;
+  /** List of indexes on this table (multiple logical indexes may be backed by
+      the same index data) */
+  private final List<Index> _indexes = new ArrayList<Index>();
+  /** 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<Column> _indexColumns = new LinkedHashSet<Column>();
+  /** Table name as stored in Database */
+  private final String _name;
+  /** Usage map of pages that this table owns */
+  private UsageMap _ownedPages;
+  /** Usage map of pages that this table owns with free space on them */
+  private UsageMap _freeSpacePages;
+  /** 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 single rows of data */
+  private final TempBufferHolder _singleRowBufferH =
+    TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
+  /** "buffer" used to writing multi rows of data (will create new buffer on
+      every call) */
+  private final TempBufferHolder _multiRowBufferH =
+    TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true);
+  /** page buffer used to write out-of-line "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;
+
+  /** common cursor for iterating through the table, kept here for historic
+      reasons */
+  private Cursor _cursor;
+  
+  /**
+   * Only used by unit tests
+
+   */
+  TableImpl(boolean testing, List<Column> columns) throws IOException {
+    if(!testing) {
+      throw new IllegalArgumentException();
+    }
+    _database = null;
+    _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
+    _name = null;
+    setColumns(columns);
+    _fkEnforcer = 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;
+    readTableDefinition(loadCompleteTableDefinitionBuffer(tableBuffer));
+    _fkEnforcer = new FKEnforcer(this);
+  }
+
+  @Override
+  public String getName() {
+    return _name;
+  }
+
+  @Override
+  public boolean isHidden() {
+    return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0);
+  }
+
+  /**
+   * @usage _advanced_method_
+   */
+  public int getMaxColumnCount() {
+    return _maxColumnCount;
+  }
+  
+  @Override
+  public int getColumnCount() {
+    return _columns.size();
+  }
+  
+  @Override
+  public DatabaseImpl getDatabase() {
+    return _database;
+  }
+  
+  /**
+   * @usage _advanced_method_
+   */
+  public JetFormat getFormat() {
+    return getDatabase().getFormat();
+  }
+
+  /**
+   * @usage _advanced_method_
+   */
+  public PageChannel getPageChannel() {
+    return getDatabase().getPageChannel();
+  }
+
+  @Override
+  public ErrorHandler getErrorHandler() {
+    return((_tableErrorHandler != null) ? _tableErrorHandler :
+           getDatabase().getErrorHandler());
+  }
+
+  @Override
+  public void setErrorHandler(ErrorHandler newErrorHandler) {
+    _tableErrorHandler = newErrorHandler;
+  }    
+
+  public int getTableDefPageNumber() {
+    return _tableDefPageNumber;
+  }
+
+  /**
+   * @usage _advanced_method_
+   */
+  public RowState createRowState() {
+    return new RowState(TempBufferHolder.Type.HARD);
+  }
+
+  protected 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;
+    // 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;
+  }
+
+  @Override
+  public List<Column> getColumns() {
+    return Collections.unmodifiableList(_columns);
+  }
+
+  @Override
+  public Column getColumn(String name) {
+    for(Column column : _columns) {
+      if(column.getName().equalsIgnoreCase(name)) {
+        return column;
+      }
+    }
+    throw new IllegalArgumentException("Column with name " + name +
+                                       " does not exist in this table");
+  }
+    
+  /**
+   * Only called by unit tests
+   */
+  private void setColumns(List<Column> columns) {
+    _columns = columns;
+    int colIdx = 0;
+    int varLenIdx = 0;
+    int fixedOffset = 0;
+    for(Column col : _columns) {
+      col.setColumnNumber((short)colIdx);
+      col.setColumnIndex(colIdx++);
+      if(col.isVariableLength()) {
+        col.setVarLenTableIndex(varLenIdx++);
+        _varColumns.add(col);
+      } else {
+        col.setFixedDataOffset(fixedOffset);
+        fixedOffset += col.getType().getFixedSize();
+      }
+    }
+    _maxColumnCount = (short)_columns.size();
+    _maxVarColumnCount = (short)_varColumns.size();
+    _autoNumColumns = getAutoNumberColumns(columns);
+  }
+
+  @Override
+  public PropertyMap getProperties() throws IOException {
+    if(_props == null) {
+      _props = getPropertyMaps().getDefault();
+    }
+    return _props;
+  }
+
+  /**
+   * @return all PropertyMaps for this table (and columns)
+   * @usage _general_method_
+   */
+  protected PropertyMaps getPropertyMaps() throws IOException {
+    if(_propertyMaps == null) {
+      _propertyMaps = getDatabase().getPropertiesForObject(
+          _tableDefPageNumber);
+    }
+    return _propertyMaps;
+  }
+  
+  @Override
+  public List<Index> getIndexes() {
+    return Collections.unmodifiableList(_indexes);
+  }
+
+  @Override
+  public Index getIndex(String name) {
+    for(Index index : _indexes) {
+      if(index.getName().equalsIgnoreCase(name)) {
+        return index;
+      }
+    }
+    throw new IllegalArgumentException("Index with name " + name +
+                                       " does not exist on this table");
+  }
+
+  @Override
+  public Index getPrimaryKeyIndex() {
+    for(Index index : _indexes) {
+      if(index.isPrimaryKey()) {
+        return index;
+      }
+    }
+    throw new IllegalArgumentException("Table " + getName() +
+                                       " does not have a primary key index");
+  }
+  
+  @Override
+  public Index getForeignKeyIndex(Table otherTable) {
+    for(Index 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)
+   */
+  List<IndexData> getIndexDatas() {
+    return Collections.unmodifiableList(_indexDatas);
+  }
+
+  /**
+   * Only called by unit tests
+   */
+  int getLogicalIndexCount() {
+    return _logicalIndexCount;
+  }
+
+  private Cursor getInternalCursor() {
+    if(_cursor == null) {
+      _cursor = Cursor.createCursor(this);
+    }
+    return _cursor;
+  }
+  
+  /**
+   * After calling this method, getNextRow will return the first row in the
+   * table, see {@link Cursor#reset}.
+   * @usage _general_method_
+   */
+  public void reset() {
+    // FIXME remove internal cursor?
+    getInternalCursor().reset();
+  }
+  
+  /**
+   * Delete the current row (retrieved by a call to {@link #getNextRow()}).
+   * @usage _general_method_
+   */
+  public void deleteCurrentRow() throws IOException {
+    // FIXME remove internal cursor?
+    getInternalCursor().deleteCurrentRow();
+  }
+
+  /**
+   * Delete the row on which the given rowState is currently positioned.
+   * <p>
+   * Note, this method is not generally meant to be used directly.  You should
+   * use the {@link #deleteCurrentRow} method or use the Cursor class, which
+   * allows for more complex table interactions.
+   * @usage _advanced_method_
+   */
+  public void deleteRow(RowState rowState, RowId rowId) throws IOException {
+    requireValidRowId(rowId);
+    
+    // ensure that the relevant row state is up-to-date
+    ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId);
+
+    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(Column idxCol : _indexColumns) {
+        getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null);
+      }
+
+      // use any read rowValues to help update the indexes
+      rowValues = rowState.getRowValues();
+
+      // 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);
+  }
+  
+  /**
+   * @return The next row in this table (Column name -> Column value)
+   * @usage _general_method_
+   */
+  public Map<String, Object> getNextRow() throws IOException {
+    // FIXME remove internal cursor?
+    return getNextRow(null);
+  }
+
+  /**
+   * @param columnNames Only column names in this collection will be returned
+   * @return The next row in this table (Column name -> Column value)
+   * @usage _general_method_
+   */
+  public Map<String, Object> getNextRow(Collection<String> columnNames) 
+    throws IOException
+  {
+    // FIXME remove internal cursor?
+    return getInternalCursor().getNextRow(columnNames);
+  }
+  
+  /**
+   * Reads a single column from the given row.
+   * <p>
+   * Note, this method is not generally meant to be used directly.  Instead
+   * use the Cursor class, which allows for more complex table interactions,
+   * e.g. {@link Cursor#getCurrentRowValue}.
+   * @usage _advanced_method_
+   */
+  public Object getRowValue(RowState rowState, RowId rowId, Column 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 Map<String, Object> getRow(
+      RowState rowState, RowId 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 Map<String, Object> getRow(
+         JetFormat format,
+      RowState rowState,
+      ByteBuffer rowBuffer,
+      Collection<Column> columns,
+      Collection<String> columnNames)
+    throws IOException
+  {
+    Map<String, Object> rtn = new LinkedHashMap<String, Object>(
+        columns.size());
+    for(Column 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,
+                                     Column column,
+                                     RowState rowState,
+                                     Map<Column,byte[]> rawVarValues)
+    throws IOException
+  {
+    byte[] columnData = null;
+    try {
+
+      NullMask nullMask = rowState.getNullMask(rowBuffer);
+      boolean isNull = nullMask.isNull(column);
+      if(column.getType() == DataType.BOOLEAN) {
+          // Boolean values are stored in the null mask.  see note about
+          // caching below
+        return rowState.setRowValue(column.getColumnIndex(),
+                                    Boolean.valueOf(!isNull));
+      } else if(isNull) {
+        // well, that's easy! (no need to update cache w/ null)
+        return null;
+      }
+
+      // 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.setRowValue(column.getColumnIndex(), 
+                                  column.read(columnData));
+
+    } catch(Exception e) {
+
+      // cache "raw" row value.  see note about caching above
+      rowState.setRowValue(column.getColumnIndex(), 
+                           Column.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, RowId 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, RowId 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 RowId(overflowPageNum, overflowRowNum));
+        rowNum = overflowRowNum;
+      
+      } else {
+
+        rowState.setStatus(RowStateStatus.AT_FINAL);
+        return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd);
+      }
+    }
+  }
+
+  
+  /**
+   * Calls <code>reset</code> on this table and returns a modifiable
+   * Iterator which will iterate through all the rows of this table.  Use of
+   * the Iterator follows the same restrictions as a call to
+   * <code>getNextRow</code>.
+   * @throws IllegalStateException if an IOException is thrown by one of the
+   *         operations, the actual exception will be contained within
+   * @usage _general_method_
+   */
+  public Iterator<Map<String, Object>> iterator()
+  {
+    // FIXME remove internal cursor?
+    return iterator(null);
+  }
+  
+  /**
+   * Calls <code>reset</code> on this table and returns a modifiable
+   * Iterator which will iterate through all the rows of this table, returning
+   * only the given columns.  Use of the Iterator follows the same
+   * restrictions as a call to <code>getNextRow</code>.
+   * @throws IllegalStateException if an IOException is thrown by one of the
+   *         operations, the actual exception will be contained within
+   * @usage _general_method_
+   */
+  public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
+  {
+    // FIXME remove internal cursor?
+    reset();
+    return getInternalCursor().iterator(columnNames);
+  }
+
+  /**
+   * 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 totalTableDefSize = format.SIZE_TDEF_HEADER +
+      (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + 
+      idxDataLen + 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(Column 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
+    Column.writeDefinitions(creator, buffer); 
+    
+    if(creator.hasIndexes()) {
+      // index and index data definitions
+      IndexData.writeDefinitions(creator, buffer);
+      Index.writeDefinitions(creator, buffer);
+    }
+
+    //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<Column> 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(Column.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
+    if (LOG.isDebugEnabled()) {
+      int position = buffer.position();
+      buffer.rewind();
+      LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString(
+                buffer, creator.getFormat().SIZE_TDEF_HEADER));
+      buffer.position(position);
+    }
+  }
+
+  /**
+   * 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 = Column.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
+  {
+    // 2 table usage maps plus 1 for each index
+    int umapNum = 2 + creator.getIndexCount();
+
+    JetFormat format = creator.getFormat();
+    int usageMapRowLength = format.OFFSET_USAGE_MAP_START +
+      format.USAGE_MAP_TABLE_BYTE_LENGTH;
+    int freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE
+      - (umapNum * getRowSpaceUsage(usageMapRowLength, format));
+    
+    // for now, don't handle writing that many indexes
+    if(freeSpace < 0) {
+      throw new IOException("FIXME attempting to write too many indexes");
+    }
+
+    int umapPageNumber = creator.getUmapPageNumber();
+
+    PageChannel pageChannel = creator.getPageChannel();
+    ByteBuffer rtn = pageChannel.createPageBuffer();
+    rtn.put(PageTypes.DATA);
+    rtn.put((byte) 0x1);  //Unknown
+    rtn.putShort((short)freeSpace);  //Free space in page
+    rtn.putInt(0); //Table definition
+    rtn.putInt(0); //Unknown
+    rtn.putShort((short) umapNum); //Number of records on this page
+
+    // write two rows of usage map definitions for the table
+    int rowStart = findRowEnd(rtn, 0, format) - usageMapRowLength;
+    for(int i = 0; i < 2; ++i) {
+      rtn.putShort(getRowStartOffset(i, format), (short)rowStart);
+      if(i == 0) {
+        // initial "usage pages" map definition
+        rtn.put(rowStart, UsageMap.MAP_TYPE_REFERENCE);
+      } else {
+        // initial "pages with free space" map definition
+        rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+      }
+      rowStart -= usageMapRowLength;
+    }
+
+    if(creator.hasIndexes()) {
+      
+      for(int i = 0; i < creator.getIndexes().size(); ++i) {
+        IndexBuilder idx = creator.getIndexes().get(i);
+
+        // allocate root page for the index
+        int rootPageNumber = pageChannel.allocateNewPage();
+        int umapRowNum = i + 2;
+
+        // 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
+        rtn.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart);
+        rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+        rtn.putInt(rowStart + 1, rootPageNumber);
+        rtn.put(rowStart + 5, (byte)1);
+
+        rowStart -= usageMapRowLength;
+      }
+    }
+
+    pageChannel.writePage(rtn, umapPageNumber);
+  }
+
+  /**
+   * 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 = getPageChannel().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;
+  }
+    
+  /**
+   * Read the table definition
+   */
+  private void readTableDefinition(ByteBuffer tableBuffer) throws IOException
+  {
+    if (LOG.isDebugEnabled()) {
+      tableBuffer.rewind();
+      LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer,
+          getFormat().SIZE_TDEF_HEADER));
+    }
+    _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);
+
+    int rowNum = ByteUtil.getUnsignedByte(
+        tableBuffer, getFormat().OFFSET_OWNED_PAGES);
+    int pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_OWNED_PAGES + 1);
+    _ownedPages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
+    rowNum = ByteUtil.getUnsignedByte(
+        tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES);
+    pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES + 1);
+    _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
+    
+    for (int i = 0; i < _indexCount; i++) {
+      _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat()));
+    }
+    
+    int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK +
+        _indexCount * getFormat().SIZE_INDEX_DEFINITION;
+    int dispIndex = 0;
+    for (int i = 0; i < columnCount; i++) {
+      Column column = new Column(this, tableBuffer,
+          colOffset + (i * getFormat().SIZE_COLUMN_HEADER), 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);
+      }
+    }
+    tableBuffer.position(colOffset +
+                         (columnCount * getFormat().SIZE_COLUMN_HEADER));
+    for (int i = 0; i < columnCount; i++) {
+      Column column = _columns.get(i);
+      column.setName(readName(tableBuffer));
+    }    
+    Collections.sort(_columns);
+    _autoNumColumns = getAutoNumberColumns(_columns);
+
+    // setup the data index for the columns
+    int colIdx = 0;
+    for(Column 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);
+
+    // 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 Index(tableBuffer, _indexDatas, getFormat()));
+    }
+
+    // read logical index names
+    for (int i = 0; i < _logicalIndexCount; i++) {
+      _indexes.get(i).setName(readName(tableBuffer));
+    }
+    
+    Collections.sort(_indexes);
+
+    // re-sort columns if necessary
+    if(getDatabase().getColumnOrder() != ColumnOrder.DATA) {
+      Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
+    }
+
+    for(Column col : _columns) {
+      // some columns need to do extra work after the table is completely
+      // loaded
+      col.postTableLoadInit();
+    }
+  }
+  
+  /**
+   * 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 Column.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);
+  }
+  
+  @Override
+  public Object[] asRow(Map<String,?> rowMap) {
+    return asRow(rowMap, null);
+  }
+  
+  @Override
+  public Object[] asUpdateRow(Map<String,?> rowMap) {
+    return asRow(rowMap, Column.KEEP_VALUE);
+  }
+
+  /**
+   * Converts a map of columnName -> columnValue to an array of row values.
+   */
+  private Object[] asRow(Map<String,?> rowMap, Object defaultValue)
+  {
+    Object[] row = new Object[_columns.size()];
+    if(defaultValue != null) {
+      Arrays.fill(row, defaultValue);
+    }
+    if(rowMap == null) {
+      return row;
+    }
+    for(Column col : _columns) {
+      if(rowMap.containsKey(col.getName())) {
+        col.setRowValue(row, col.getRowValue(rowMap));
+      }
+    }
+    return row;
+  }
+  
+  @Override
+  public void addRow(Object... row) throws IOException {
+    addRows(Collections.singletonList(row), _singleRowBufferH);
+  }
+  
+  @Override
+  public void addRows(List<? extends Object[]> rows) throws IOException {
+    addRows(rows, _multiRowBufferH);
+  }
+  
+  /**
+   * 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
+   * @param writeRowBufferH TempBufferHolder used to generate buffers for
+   *                        writing the row data
+   */
+  private void addRows(List<? extends Object[]> inRows,
+                       TempBufferHolder writeRowBufferH)
+    throws IOException
+  {
+    if(inRows.isEmpty()) {
+      return;
+    }
+
+    // copy the input rows to a modifiable list so we can update the elements
+    List<Object[]> rows = new ArrayList<Object[]>(inRows);
+    ByteBuffer[] rowData = new ByteBuffer[rows.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 < _columns.size()) || (row.getClass() != Object[].class)) {
+        row = dupeRow(row, _columns.size());
+        // we copied the row, so put the copy back into the rows list
+        rows.set(i, row);
+      }
+
+      // fill in autonumbers
+      handleAutoNumbersForAdd(row);
+      
+      // write the row of data to a temporary buffer
+      rowData[i] = createRow(row, writeRowBufferH.getPageBuffer(getPageChannel()));
+      
+      if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) {
+        throw new IOException("Row size " + rowData[i].limit() +
+                              " is too large");
+      }
+    }
+
+    ByteBuffer dataPage = null;
+    int pageNumber = PageChannel.INVALID_PAGE_NUMBER;
+    
+    for (int i = 0; i < rowData.length; i++) {
+      int rowSize = rowData[i].remaining();
+      Object[] row = rows.get(i);
+
+      // handle foreign keys before adding to table
+      _fkEnforcer.addRow(row);
+
+      // get page with space
+      dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber);
+      pageNumber = _addRowBufferH.getPageNumber();
+
+      // write out the row data
+      int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0);
+      dataPage.put(rowData[i]);
+
+      // update the indexes
+      RowId rowId = new RowId(pageNumber, rowNum);
+      for(IndexData indexData : _indexDatas) {
+        indexData.addRow(row, rowId);
+      }
+    }
+
+    writeDataPage(dataPage, pageNumber);
+    
+    // Update tdef page
+    updateTableDefinition(rows.size());
+  }
+
+  /**
+   * Updates the current row to the new values.
+   * <p>
+   * Note, if this table has an auto-number column(s), the existing value(s)
+   * will be maintained, unchanged.
+   *
+   * @param row new row values for the current row.
+   * @usage _general_method_
+   */
+  public void updateCurrentRow(Object... row) throws IOException {
+    // FIXME remove internal cursor?
+     getInternalCursor().updateCurrentRow(row);
+  }
+  
+  /**
+   * Update the row on which the given rowState is currently positioned.
+   * <p>
+   * Note, this method is not generally meant to be used directly.  You should
+   * use the {@link #updateCurrentRow} method or use the Cursor class, which
+   * allows for more complex table interactions, e.g.
+   * {@link Cursor#setCurrentRowValue} and {@link Cursor#updateCurrentRow}.
+   * @usage _advanced_method_
+   */
+  public void updateRow(RowState rowState, RowId rowId, Object... row) 
+    throws IOException 
+  {
+    requireValidRowId(rowId);
+    
+    // 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<Column,byte[]> keepRawVarValues = 
+      (!_varColumns.isEmpty() ? new HashMap<Column,byte[]>() : null);
+
+    for(Column column : _columns) {
+      if(_autoNumColumns.contains(column)) {
+        // fill in any auto-numbers (we don't allow autonumber values to be
+        // modified)
+        column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, 
+                                             rowState, null));
+      } else if(column.getRowValue(row) == Column.KEEP_VALUE) {
+        // fill in any "keep value" fields
+        column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column,
+                                             rowState, keepRawVarValues));
+      } else if(_indexColumns.contains(column)) {
+        // read row value to help update indexes
+        getRowColumn(getFormat(), rowBuffer, column, rowState, null);
+      }
+    }
+
+    // generate new row bytes
+    ByteBuffer newRowData = createRow(
+        row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize,
+        keepRawVarValues);
+
+    if (newRowData.limit() > getFormat().MAX_ROW_SIZE) {
+      throw new IOException("Row size " + newRowData.limit() + 
+                            " is too large");
+    }
+
+    if(!_indexDatas.isEmpty()) {
+
+      Object[] oldRowValues = rowState.getRowValues();
+
+      // check foreign keys before actually updating
+      _fkEnforcer.updateRow(oldRowValues, row);
+
+      // delete old values from indexes
+      for(IndexData indexData : _indexDatas) {
+        indexData.deleteRow(oldRowValues, rowId);
+      }
+    }
+    
+    // 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();
+
+      RowId 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());
+      }
+    }
+
+    // update the indexes
+    for(IndexData indexData : _indexDatas) {
+      indexData.addRow(row, rowId);
+    }
+
+    writeDataPage(dataPage, pageNumber);
+
+    updateTableDefinition(0);
+  }
+   
+  private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, 
+                                      int pageNumber)
+    throws IOException
+  {
+    if(dataPage == null) {
+
+      // 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;
+        }
+        dataPage = _addRowBufferH.setPage(getPageChannel(), tmpPageNumber);
+        if(dataPage.get() == PageTypes.DATA) {
+          // found last data page, only use if actually listed in free space
+          // pages
+          if(_freeSpacePages.containsPageNumber(tmpPageNumber)) {
+            pageNumber = tmpPageNumber;
+          }
+          break;
+        }
+      }
+
+      if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) {
+        // No data pages exist (with free space).  Create a new one.
+        return newDataPage();
+      }
+    
+    }
+
+    if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) {
+
+      // Last data page is full.  Create a new one.
+      writeDataPage(dataPage, pageNumber);
+      _freeSpacePages.removePageNumber(pageNumber);
+
+      dataPage = newDataPage();
+    }
+
+    return dataPage;
+  }
+  /**
+   * 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 {
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Creating new data page");
+    }
+    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;
+  }
+  
+  ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer)
+    throws IOException
+  {
+    return createRow(rowArray, buffer, 0, Collections.<Column,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<Column,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 (Column col : _columns) {
+
+      if(col.isVariableLength()) {
+        continue;
+      }
+        
+      Object rowValue = col.getRowValue(rowArray);
+
+      if (col.getType() == DataType.BOOLEAN) {
+        
+        if(Column.toBooleanValue(rowValue)) {
+          //Booleans are stored in the null mask
+          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 (Column 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 (Column 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();
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit()));
+    }
+    return buffer;
+  }
+
+  /**
+   * Fill in all autonumber column values.
+   */
+  private void handleAutoNumbersForAdd(Object[] row)
+    throws IOException
+  {
+    if(_autoNumColumns.isEmpty()) {
+      return;
+    }
+
+    Object complexAutoNumber = null;
+    for(Column col : _autoNumColumns) {
+      // ignore given row value, use next autonumber
+      Column.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);
+    }
+  }
+
+  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);
+    }
+  }
+
+  @Override
+  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;
+  }
+  
+  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;
+  }
+  
+  @Override
+  public String toString() {
+    StringBuilder rtn = new StringBuilder();
+    rtn.append("Type: " + _tableType + 
+               ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)"));
+    rtn.append("\nName: " + _name);
+    rtn.append("\nRow count: " + _rowCount);
+    rtn.append("\nColumn count: " + _columns.size());
+    rtn.append("\nIndex (data) count: " + _indexCount);
+    rtn.append("\nLogical Index count: " + _logicalIndexCount);
+    rtn.append("\nColumns:\n");
+    for(Column col : _columns) {
+      rtn.append(col);
+    }
+    rtn.append("\nIndexes:\n");
+    for(Index index : _indexes) {
+      rtn.append(index);
+    }
+    rtn.append("\nOwned pages: " + _ownedPages + "\n");
+    return rtn.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();
+    StringBuilder rtn = new StringBuilder();
+    for(Iterator<Column> iter = _columns.iterator(); iter.hasNext(); ) {
+      Column col = iter.next();
+      rtn.append(col.getName());
+      if (iter.hasNext()) {
+        rtn.append("\t");
+      }
+    }
+    rtn.append("\n");
+    Map<String, Object> row;
+    int rowCount = 0;
+    while ((rowCount++ < limit) && (row = getNextRow()) != null) {
+      for(Iterator<Object> iter = row.values().iterator(); iter.hasNext(); ) {
+        Object obj = iter.next();
+        if (obj instanceof byte[]) {
+          byte[] b = (byte[]) obj;
+          rtn.append(ByteUtil.toHexString(b));
+          //This block can be used to easily dump a binary column to a file
+          /*java.io.File f = java.io.File.createTempFile("ole", ".bin");
+            java.io.FileOutputStream out = new java.io.FileOutputStream(f);
+            out.write(b);
+            out.flush();
+            out.close();*/
+        } else {
+          rtn.append(String.valueOf(obj));
+        }
+        if (iter.hasNext()) {
+          rtn.append("\t");
+        }
+      }
+      rtn.append("\n");
+    }
+    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.
+   */
+  private 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(RowId 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, RowId 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;
+  }
+
+  /**
+   * @return the "AutoNumber" columns in the given collection of columns.
+   * @usage _advanced_method_
+   */
+  public static List<Column> getAutoNumberColumns(Collection<Column> columns) {
+    List<Column> autoCols = new ArrayList<Column>(1);
+    for(Column c : columns) {
+      if(c.isAutoNumber()) {
+        autoCols.add(c);
+      }
+    }
+    return (!autoCols.isEmpty() ? autoCols : Collections.<Column>emptyList());
+  }
+
+  /**
+   * 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 RowId _headerRowId = RowId.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 RowId _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 RowId 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 setRowValue(int idx, Object value) {
+      _haveRowValues = true;
+      _rowValues[idx] = value;
+      return value;
+    }
+    
+    public Object[] getRowValues() {
+      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 RowId getHeaderRowId() {
+      return _headerRowId;
+    }
+
+    public int getRowsOnHeaderPage() {
+      return _rowsOnHeaderPage;
+    }
+    
+    private ByteBuffer getHeaderPage()
+      throws IOException
+    {
+      checkForModification();
+      return _headerRowBufferH.getPage(getPageChannel());
+    }
+
+    private ByteBuffer setHeaderRow(RowId 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(RowId 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(Column column,
+                                  byte[] columnData,
+                                  Exception error)
+      throws IOException
+    {
+      return getErrorHandler().handleRowError(column, columnData,
+                                              this, error);
+    }  
+
+    @Override
+    public String toString()
+    {
+      return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " +
+        _finalRowId;
+    }
+  }
+  
+}
index 920ce25533e0d6c19d24e282287087f90b73467b..292c9bfa83315359e63b4f187cdd46ff8bed04e4 100644 (file)
@@ -125,8 +125,8 @@ public class UsageMap
     PageChannel pageChannel = database.getPageChannel();
     ByteBuffer tableBuffer = pageChannel.createPageBuffer();
     pageChannel.readPage(tableBuffer, pageNum);
-    short rowStart = Table.findRowStart(tableBuffer, rowNum, format);
-    int rowEnd = Table.findRowEnd(tableBuffer, rowNum, format);
+    short rowStart = TableImpl.findRowStart(tableBuffer, rowNum, format);
+    int rowEnd = TableImpl.findRowEnd(tableBuffer, rowNum, format);
     tableBuffer.limit(rowEnd);    
     byte mapType = tableBuffer.get(rowStart);
     UsageMap rtn = new UsageMap(database, tableBuffer, pageNum, rowStart);
index 5410e41e28713679de98216db0a1c45f30bf50d9..73357294f871cff364b6f7ab547e11f89659bc8d 100644 (file)
@@ -26,7 +26,7 @@ import java.util.Map;
 
 import com.healthmarketscience.jackcess.ByteUtil;
 import com.healthmarketscience.jackcess.Column;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
 
 
 /**
@@ -47,7 +47,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
   private final Column _fileFlagsCol;
   
   public AttachmentColumnInfo(Column column, int complexId,
-                              Table typeObjTable, Table flatTable)
+                              TableImpl typeObjTable, TableImpl flatTable)
     throws IOException
   {
     super(column, complexId, typeObjTable, flatTable);
@@ -183,7 +183,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo<Attachment>
   }
 
   
-  public static boolean isAttachmentColumn(Table typeObjTable) {
+  public static boolean isAttachmentColumn(TableImpl typeObjTable) {
     // attachment data has these columns FileURL(MEMO), FileName(TEXT),
     // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME),
     // FileFlags(LONG)
index 21b7b5d3463c17c7da2363042ef9c1752e226ac8..d78c4a2167caab9808883fd2ec250fe0d2c4665c 100644 (file)
@@ -35,7 +35,7 @@ import com.healthmarketscience.jackcess.DatabaseImpl;
 import com.healthmarketscience.jackcess.IndexCursor;
 import com.healthmarketscience.jackcess.JetFormat;
 import com.healthmarketscience.jackcess.PageChannel;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -58,7 +58,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
 
   private final Column _column;
   private final int _complexTypeId;
-  private final Table _flatTable;
+  private final TableImpl _flatTable;
   private final List<Column> _typeCols;
   private final Column _pkCol;
   private final Column _complexValFkCol;
@@ -66,7 +66,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
   private IndexCursor _complexValIdCursor;
   
   protected ComplexColumnInfo(Column column, int complexTypeId,
-                              Table typeObjTable, Table flatTable)
+                              TableImpl typeObjTable, TableImpl flatTable)
     throws IOException
   {
     _column = column;
@@ -109,7 +109,7 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
         offset + column.getFormat().OFFSET_COLUMN_COMPLEX_ID);
 
     DatabaseImpl db = column.getDatabase();
-    Table complexColumns = db.getSystemComplexColumns();
+    TableImpl complexColumns = db.getSystemComplexColumns();
     IndexCursor cursor = IndexCursor.createCursor(
         complexColumns, complexColumns.getPrimaryKeyIndex());
     if(!cursor.findFirstRowByEntry(complexTypeId)) {
@@ -127,8 +127,8 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
     int flatTableId = (Integer)cColRow.get(COL_FLAT_TABLE_ID);
     int typeObjId = (Integer)cColRow.get(COL_COMPLEX_TYPE_OBJECT_ID);
 
-    Table typeObjTable = db.getTable(typeObjId);
-    Table flatTable = db.getTable(flatTableId);
+    TableImpl typeObjTable = db.getTable(typeObjId);
+    TableImpl flatTable = db.getTable(flatTableId);
 
     if((typeObjTable == null) || (flatTable == null)) {
       throw new IOException(
@@ -370,7 +370,8 @@ public abstract class ComplexColumnInfo<V extends ComplexValue>
     return rtn.toString();
   }
 
-  protected static void diffFlatColumns(Table typeObjTable, Table flatTable,
+  protected static void diffFlatColumns(TableImpl typeObjTable, 
+                                        TableImpl flatTable,
                                         List<Column> typeCols,
                                         List<Column> otherCols)
   {
index b1bec203164cdec06e282bcee00aa5c4af6811cc..2cf504c023458167c720ef00999d489bf05961af 100644 (file)
@@ -27,7 +27,7 @@ import java.util.Set;
 
 import com.healthmarketscience.jackcess.Column;
 import com.healthmarketscience.jackcess.DataType;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
 
 /**
  * Complex column info for a column holding multiple values per row.
@@ -43,7 +43,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue>
   private final Column _valueCol;
   
   public MultiValueColumnInfo(Column column, int complexId,
-                              Table typeObjTable, Table flatTable) 
+                              TableImpl typeObjTable, TableImpl flatTable) 
     throws IOException
   {
     super(column, complexId, typeObjTable, flatTable);
@@ -88,7 +88,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo<SingleValue>
     return new SingleValueImpl(INVALID_ID, complexValueFk, value);
   }
 
-  public static boolean isMultiValueColumn(Table typeObjTable) {
+  public static boolean isMultiValueColumn(TableImpl typeObjTable) {
     // if we found a single value of a "simple" type, then we are dealing with
     // a multi-value column
     List<Column> typeCols = typeObjTable.getColumns();
index 03bd8b1d6ff68d19d24741840344828a9ccb2f40..25a28f7a0476a97e0ad31a08b02b048edf1d472f 100644 (file)
@@ -25,7 +25,7 @@ import java.util.List;
 import java.util.Map;
 
 import com.healthmarketscience.jackcess.Column;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
 
 /**
  * Complex column info for an unsupported complex type.
@@ -35,8 +35,8 @@ import com.healthmarketscience.jackcess.Table;
 public class UnsupportedColumnInfo extends ComplexColumnInfo<UnsupportedValue>
 {
 
-  public UnsupportedColumnInfo(Column column, int complexId, Table typeObjTable,
-                               Table flatTable)
+  public UnsupportedColumnInfo(Column column, int complexId, TableImpl typeObjTable,
+                               TableImpl flatTable)
     throws IOException
   {
     super(column, complexId, typeObjTable, flatTable);
index 8fc56221ba4d4988fe4ea14d360ed883668b0008..dedcb5354db25cf257619ccb83a05fd8777e31e0 100644 (file)
@@ -26,7 +26,7 @@ import java.util.List;
 import java.util.Map;
 
 import com.healthmarketscience.jackcess.Column;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
 
 /**
  * Complex column info for a column which tracking the version history of an
@@ -45,7 +45,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version>
   private final Column _modifiedCol;
   
   public VersionHistoryColumnInfo(Column column, int complexId,
-                                  Table typeObjTable, Table flatTable) 
+                                  TableImpl typeObjTable, TableImpl flatTable) 
     throws IOException
   {
     super(column, complexId, typeObjTable, flatTable);
@@ -151,7 +151,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo<Version>
     return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate);
   }
 
-  public static boolean isVersionHistoryColumn(Table typeObjTable) {
+  public static boolean isVersionHistoryColumn(TableImpl typeObjTable) {
     // version history data has these columns <value>(MEMO),
     // <modified>(SHORT_DATE_TIME)
     List<Column> typeCols = typeObjTable.getColumns();