]> source.dussan.org Git - jackcess.git/commitdiff
implement handling of usagemaps for long value (MEMO/OLE) columns, fixes issue 95
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 5 Jul 2013 12:35:01 +0000 (12:35 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 5 Jul 2013 12:35:01 +0000 (12:35 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@744 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/IndexData.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TableCreator.java
src/java/com/healthmarketscience/jackcess/UsageMap.java

index 87188fafbd1b048f1ae18282e8222c3ed5be3320..181fe3fa63732b4517a2346145e576433362a166 100644 (file)
@@ -4,6 +4,11 @@
     <author email="javajedi@users.sf.net">Tim McCune</author>
   </properties>
   <body>
+    <release version="1.2.14" date="TBD">
+      <action dev="jahlborn" type="fix" system="SourceForge2" issue="95">
+        Implement handling of usagemaps for long value (MEMO/OLE) columns.
+      </action>
+    </release>
     <release version="1.2.13" date="2013-06-18">
       <action dev="jahlborn" type="fix">
         Fix partial page updates when using CodecHandlers which can only do
index 5f445b16a75f9b26ee58ec91e80b9780555405b4..69412a8c6222348287eb9aaac59364007c2d5b9a 100644 (file)
@@ -217,6 +217,8 @@ public class Column implements Comparable<Column> {
   private ComplexColumnInfo<? extends ComplexValue> _complexInfo;
   /** properties for this column, if any */
   private PropertyMap _props;  
+  /** Holds additional info for writing long values */
+  private LongValueBufferHolder _lvalBufferH;
   
   /**
    * @usage _general_method_
@@ -314,10 +316,20 @@ public class Column implements Comparable<Column> {
     }
   }
 
+  /**
+   * Sets the usage maps for this column.
+   */
+  void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) {
+    _lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages);
+  }
+
   /**
    * Secondary column initialization after the table is fully loaded.
    */
   void postTableLoadInit() throws IOException {
+    if(getType().isLongValue() && (_lvalBufferH == null)) {
+      _lvalBufferH = new LegacyLongValueBufferHolder();
+    }
     if(_complexInfo != null) {
       _complexInfo.postTableLoadInit();
     }
@@ -608,6 +620,14 @@ public class Column implements Comparable<Column> {
     return getDatabase().getCalendar();
   }
 
+  /**
+   * Returns the number of database pages owned by this column.
+   * @usage _intermediate_method_
+   */
+  public int getOwnedPageCount() {
+    return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount());
+  }
+
   /**
    * Whether or not this column is "append only" (its history is tracked by a
    * separate version history column).
@@ -1313,7 +1333,6 @@ public class Column implements Comparable<Column> {
       def.put(value);
     } else {
       
-      TempPageHolder lvalBufferH = getTable().getLongValueBuffer();
       ByteBuffer lvalPage = null;
       int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
       byte firstLvalRow = 0;
@@ -1321,8 +1340,8 @@ public class Column implements Comparable<Column> {
       // write other page(s)
       switch(type) {
       case LONG_VALUE_TYPE_OTHER_PAGE:
-        lvalPage = getLongValuePage(value.length, lvalBufferH);
-        firstLvalPageNum = lvalBufferH.getPageNumber();
+        lvalPage = _lvalBufferH.getLongValuePage(value.length);
+        firstLvalPageNum = _lvalBufferH.getPageNumber();
         firstLvalRow = (byte)Table.addDataPageRow(lvalPage, value.length,
                                                   getFormat(), 0);
         lvalPage.put(value);
@@ -1334,12 +1353,13 @@ public class Column implements Comparable<Column> {
         ByteBuffer buffer = ByteBuffer.wrap(value);
         int remainingLen = buffer.remaining();
         buffer.limit(0);
-        lvalPage = getLongValuePage(getFormat().MAX_LONG_VALUE_ROW_SIZE,
-                                    lvalBufferH);
-        firstLvalPageNum = lvalBufferH.getPageNumber();
+        lvalPage = _lvalBufferH.getLongValuePage(remainingLen);
+        firstLvalPageNum = _lvalBufferH.getPageNumber();
+        firstLvalRow = (byte)Table.getRowsOnDataPage(lvalPage, getFormat());
         int lvalPageNum = firstLvalPageNum;
         ByteBuffer nextLvalPage = null;
         int nextLvalPageNum = 0;
+        int nextLvalRowNum = 0;
         while(remainingLen > 0) {
           lvalPage.clear();
 
@@ -1350,23 +1370,25 @@ public class Column implements Comparable<Column> {
 
           // figure out if we will need another page, and if so, allocate it
           if(chunkLength < remainingLen) {
-            // force a new page to be allocated
-            lvalBufferH.clear();
-            nextLvalPage = getLongValuePage(
-                getFormat().MAX_LONG_VALUE_ROW_SIZE, lvalBufferH);
-            nextLvalPageNum = lvalBufferH.getPageNumber();
+            // force a new page to be allocated for the chunk after this
+            _lvalBufferH.clear();
+            nextLvalPage = _lvalBufferH.getLongValuePage(
+                (remainingLen - chunkLength) + 4);
+            nextLvalPageNum = _lvalBufferH.getPageNumber();
+            nextLvalRowNum = Table.getRowsOnDataPage(nextLvalPage, 
+                                                     getFormat());
           } else {
             nextLvalPage = null;
             nextLvalPageNum = 0;
+            nextLvalRowNum = 0;
           }
 
           // add row to this page
           byte lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4,
                                                     getFormat(), 0);
           
-          // write next page info (we'll always be writing into row 0 for
-          // newly created pages)
-          lvalPage.put((byte)0); // row number
+          // write next page info
+          lvalPage.put((byte)nextLvalRowNum); // row number
           ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number
 
           // write this page's chunk of data
@@ -1376,17 +1398,6 @@ public class Column implements Comparable<Column> {
 
           // write new page to database
           getPageChannel().writePage(lvalPage, lvalPageNum);
-
-          if(lvalPageNum == firstLvalPageNum) {
-            // save initial row info
-            firstLvalRow = lvalRow;
-          } else {
-            // check assertion that we wrote to row 0 for all subsequent pages
-            if(lvalRow != (byte)0) {
-              throw new IllegalStateException("Expected row 0, but was " +
-                                              lvalRow);
-            }
-          }
           
           // move to next page
           lvalPage = nextLvalPage;
@@ -1425,28 +1436,6 @@ public class Column implements Comparable<Column> {
     lvalPage.putShort((short)0); // num rows in page
   }
 
-  /**
-   * Returns a long value data page with space for data of the given length.
-   */
-  private ByteBuffer getLongValuePage(int dataLength,
-                                      TempPageHolder lvalBufferH)
-    throws IOException
-  {
-    ByteBuffer lvalPage = null;
-    if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) {
-      lvalPage = lvalBufferH.getPage(getPageChannel());
-      if(Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
-        // the current page has space
-        return lvalPage;
-      }
-    }
-
-    // need new page
-    lvalPage = lvalBufferH.setNewPage(getPageChannel());
-    writeLongValueHeader(lvalPage);
-    return lvalPage;
-  }
-  
   /**
    * Serialize an Object into a raw byte value for this column in little
    * endian order
@@ -2052,7 +2041,6 @@ public class Column implements Comparable<Column> {
     throws IOException
   {
     List<Column> columns = creator.getColumns();
-    short columnNumber = (short) 0;
     short fixedOffset = (short) 0;
     short variableOffset = (short) 0;
     // we specifically put the "long variable" values after the normal
@@ -2060,13 +2048,11 @@ public class Column implements Comparable<Column> {
     // all (because "long variable" values can go in separate pages)
     short longVariableOffset = countNonLongVariableLength(columns);
     for (Column col : columns) {
-      // record this for later use when writing indexes
-      col.setColumnNumber(columnNumber);
 
       int position = buffer.position();
       buffer.put(col.getType().getValue());
       buffer.putInt(Table.MAGIC_TABLE_NUMBER);  //constant magic number
-      buffer.putShort(columnNumber);  //Column Number
+      buffer.putShort(col.getColumnNumber());  //Column Number
       if (col.isVariableLength()) {
         if(!col.getType().isLongValue()) {
           buffer.putShort(variableOffset++);
@@ -2076,7 +2062,7 @@ public class Column implements Comparable<Column> {
       } else {
         buffer.putShort((short) 0);
       }
-      buffer.putShort(columnNumber); //Column Number again
+      buffer.putShort(col.getColumnNumber()); //Column Number again
       if(col.getType().isTextual()) {
         // this will write 4 bytes (note we don't support writing dbs which
         // use the text code page)
@@ -2110,7 +2096,6 @@ public class Column implements Comparable<Column> {
       } else {
         buffer.putShort((short)0x0000); // unused
       }
-      columnNumber++;
       if (LOG.isDebugEnabled()) {
         LOG.debug("Creating new column def block\n" + ByteUtil.toHexString(
                   buffer, position, creator.getFormat().SIZE_COLUMN_DEF_BLOCK));
@@ -2441,4 +2426,132 @@ public class Column implements Comparable<Column> {
         of type MEMO) */
     private boolean _hyperlink;
   }
+
+  /**
+   * Manages secondary page buffers for long value writing.
+   */
+  private abstract class LongValueBufferHolder
+  {
+    /**
+     * Returns a long value data page with space for data of the given length.
+     */
+    public ByteBuffer getLongValuePage(int dataLength) throws IOException {
+
+      TempPageHolder lvalBufferH = getBufferHolder();
+      dataLength = Math.min(dataLength, getFormat().MAX_LONG_VALUE_ROW_SIZE);
+
+      ByteBuffer lvalPage = null;
+      if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) {
+        lvalPage = lvalBufferH.getPage(getPageChannel());
+        if(Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) {
+          // the current page has space
+          return lvalPage;
+        }
+      }
+
+      // need new page
+      return findNewPage(dataLength);
+    }
+
+    protected ByteBuffer findNewPage(int dataLength) throws IOException {
+      ByteBuffer lvalPage = getBufferHolder().setNewPage(getPageChannel());
+      writeLongValueHeader(lvalPage);
+      return lvalPage;
+    }
+
+    public int getOwnedPageCount() {
+      return 0;
+    }
+
+    /**
+     * Returns the page number of the current long value data page.
+     */
+    public int getPageNumber() {
+      return getBufferHolder().getPageNumber();
+    }
+
+    /**
+     * Discards the current the current long value data page.
+     */
+    public void clear() throws IOException {
+      getBufferHolder().clear();
+    }
+
+    protected abstract TempPageHolder getBufferHolder();
+  }
+
+  /**
+   * Manages a common, shared extra page for long values.  This is legacy
+   * behavior from before it was understood that there were additional usage
+   * maps for each columns.
+   */
+  private final class LegacyLongValueBufferHolder extends LongValueBufferHolder
+  {
+    @Override
+    protected TempPageHolder getBufferHolder() {
+      return getTable().getLongValueBuffer();
+    }
+  }
+
+  /**
+   * Manages the column usage maps for long values.
+   */
+  private final class UmapLongValueBufferHolder extends LongValueBufferHolder
+  {
+    /** Usage map of pages that this column owns */
+    private final UsageMap _ownedPages;
+    /** Usage map of pages that this column owns with free space on them */
+    private final UsageMap _freeSpacePages;
+    /** page buffer used to write "long value" data */
+    private final TempPageHolder _longValueBufferH =
+      TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
+
+    private UmapLongValueBufferHolder(UsageMap ownedPages,
+                                      UsageMap freeSpacePages) {
+      _ownedPages = ownedPages;
+      _freeSpacePages = freeSpacePages;
+    }
+
+    @Override
+    protected TempPageHolder getBufferHolder() {
+      return _longValueBufferH;
+    }
+
+    @Override
+    public int getOwnedPageCount() {
+      return _ownedPages.getPageCount();
+    }
+
+    @Override
+    protected ByteBuffer findNewPage(int dataLength) throws IOException {
+
+      // grab last owned page and check for free space.  
+      ByteBuffer newPage = Table.findFreeRowSpace(      
+          _ownedPages, _freeSpacePages, _longValueBufferH);
+      
+      if(newPage != null) {
+        if(Table.rowFitsOnDataPage(dataLength, newPage, getFormat())) {
+          return newPage;
+        }
+        // discard this page and allocate a new one
+        clear();
+      }
+
+      // nothing found on current pages, need new page
+      newPage = super.findNewPage(dataLength);
+      int pageNumber = getPageNumber();
+      _ownedPages.addPageNumber(pageNumber);
+      _freeSpacePages.addPageNumber(pageNumber);
+      return newPage;
+    }
+
+    @Override
+    public void clear() throws IOException {
+      int pageNumber = getPageNumber();
+      if(pageNumber != PageChannel.INVALID_PAGE_NUMBER) {
+        _freeSpacePages.removePageNumber(pageNumber, true);
+      }
+      super.clear();
+    }
+  }
 }
index 2c24a2d3b6d8e8907ed552af710ab0ba3d46f6f4..d8076935975ffecb7ec30ccf3879007762faf74d 100644 (file)
@@ -78,7 +78,7 @@ public abstract class IndexData {
   
   protected static final byte[] EMPTY_PREFIX = new byte[0];
 
-  private static final short COLUMN_UNUSED = -1;
+  static final short COLUMN_UNUSED = -1;
 
   static final byte ASCENDING_COLUMN_FLAG = (byte)0x01;
 
@@ -332,6 +332,7 @@ public abstract class IndexData {
 
   /**
    * Returns the number of database pages owned by this index data.
+   * @usage _intermediate_method_
    */
   public int getOwnedPageCount() {
     return _ownedPages.getPageCount();
@@ -420,10 +421,7 @@ public abstract class IndexData {
       }
     }
 
-    int umapRowNum = tableBuffer.get();
-    int umapPageNum = ByteUtil.get3ByteInt(tableBuffer);
-    _ownedPages = UsageMap.read(getTable().getDatabase(), umapPageNum,
-                                umapRowNum, false);
+    _ownedPages = UsageMap.read(getTable().getDatabase(), tableBuffer, false);
     
     _rootPageNumber = tableBuffer.getInt();
 
index 67b31f9a34e664356a0ff42e32375f05c4123022..9cb8933effb5a34193f2506299c392ffdfa686a9 100644 (file)
@@ -172,7 +172,7 @@ public class Table
       every call) */
   private final TempBufferHolder _multiRowBufferH =
     TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true);
-  /** page buffer used to write out-of-line "long value" data */
+  /** page buffer used to write out-of-row "long value" data */
   private final TempPageHolder _longValueBufferH =
     TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
   /** "big index support" is optional */
@@ -333,14 +333,21 @@ public class Table
    * @usage _intermediate_method_
    */
   public int getApproximateOwnedPageCount() {
+
     // add a page for the table def (although that might actually be more than
     // one page)
     int count = _ownedPages.getPageCount() + 1;
+
+    for(Column col : _columns) {
+      count += col.getOwnedPageCount();
+    }
+
     // note, we count owned pages from _physical_ indexes, not logical indexes
     // (otherwise we could double count pages)
     for(IndexData indexData : _indexDatas) {
       count += indexData.getOwnedPageCount();
     }
+
     return count;
   }
   
@@ -971,9 +978,10 @@ public class Table
                       (format.SIZE_INDEX_DEFINITION + 
                        format.SIZE_INDEX_COLUMN_BLOCK)) + 
       (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK);
+    int colUmapLen = creator.getLongValueColumns().size() * 10;
     int totalTableDefSize = format.SIZE_TDEF_HEADER +
       (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + 
-      idxDataLen + format.SIZE_TDEF_TRAILER;
+      idxDataLen + colUmapLen + format.SIZE_TDEF_TRAILER;
 
     // total up the amount of space used by the column and index names (2
     // bytes per char + 2 bytes for the length)
@@ -1010,6 +1018,20 @@ public class Table
       Index.writeDefinitions(creator, buffer);
     }
 
+    // write long value column usage map references
+    for(Column lvalCol : creator.getLongValueColumns()) {
+      buffer.putShort(lvalCol.getColumnNumber());
+      TableCreator.ColumnState colState = 
+        creator.getColumnState(lvalCol);
+      
+      // owned pages umap (both are on same page)
+      buffer.put(colState.getUmapOwnedRowNumber());
+      ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
+      // free space pages umap
+      buffer.put(colState.getUmapFreeRowNumber());
+      ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
+    }
+
     //End of tabledef
     buffer.put((byte) 0xff);
     buffer.put((byte) 0xff);
@@ -1141,53 +1163,70 @@ public class Table
   private static void createUsageMapDefinitionBuffer(TableCreator creator)
     throws IOException
   {
-    // 2 table usage maps plus 1 for each index
-    int umapNum = 2 + creator.getIndexCount();
+    List<Column> lvalCols = creator.getLongValueColumns();
+
+    // 2 table usage maps plus 1 for each index and 2 for each lval col
+    int indexUmapEnd = 2 + creator.getIndexCount();
+    int umapNum = indexUmapEnd + (lvalCols.size() * 2);
 
     JetFormat format = creator.getFormat();
-    int usageMapRowLength = format.OFFSET_USAGE_MAP_START +
+    int umapRowLength = 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 umapSpaceUsage = getRowSpaceUsage(umapRowLength, format);
+    PageChannel pageChannel = creator.getPageChannel();
+    int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER;
+    ByteBuffer umapBuf = null;
+    int freeSpace = 0;
+    int rowStart = 0;
+    int umapRowNum = 0;
 
-    int umapPageNumber = creator.getUmapPageNumber();
+    for(int i = 0; i < umapNum; ++i) {
 
-    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);
+      if(umapBuf == null) {
+
+        // need new page for usage maps
+        if(umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) {
+          // first umap page has already been reserved
+          umapPageNumber = creator.getUmapPageNumber();
+        } else {
+          // need another umap page
+          umapPageNumber = creator.reservePageNumber();
+        } 
+
+        freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE;
+
+        umapBuf = pageChannel.createPageBuffer();
+        umapBuf.put(PageTypes.DATA);
+        umapBuf.put((byte) 0x1);  //Unknown
+        umapBuf.putShort((short)freeSpace);  //Free space in page
+        umapBuf.putInt(0); //Table definition
+        umapBuf.putInt(0); //Unknown
+        umapBuf.putShort((short)0); //Number of records on this page
+
+        rowStart = findRowEnd(umapBuf, 0, format) - umapRowLength;
+        umapRowNum = 0;
       }
-      rowStart -= usageMapRowLength;
-    }
 
-    if(creator.hasIndexes()) {
-      
-      for(int i = 0; i < creator.getIndexes().size(); ++i) {
-        IndexBuilder idx = creator.getIndexes().get(i);
+      umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart);
+
+      if(i == 0) {
+
+        // table "owned pages" map definition
+        umapBuf.put(rowStart, UsageMap.MAP_TYPE_REFERENCE);
+
+      } else if(i == 1) {
+
+        // table "free space pages" map definition
+        umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+
+      } else if(i < indexUmapEnd) {
 
+        // index umap
+        int indexIdx = i - 2;
+        IndexBuilder idx = creator.getIndexes().get(indexIdx);
+        
         // allocate root page for the index
         int rootPageNumber = pageChannel.allocateNewPage();
-        int umapRowNum = i + 2;
 
         // stash info for later use
         TableCreator.IndexState idxState = creator.getIndexState(idx);
@@ -1196,16 +1235,54 @@ public class Table
         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);
+        umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+        umapBuf.putInt(rowStart + 1, rootPageNumber);
+        umapBuf.put(rowStart + 5, (byte)1);
+
+      } else {
+
+        // long value column umaps
+        int lvalColIdx = i - indexUmapEnd;
+        int umapType = lvalColIdx % 2;
+        lvalColIdx /= 2;
 
-        rowStart -= usageMapRowLength;
+        Column lvalCol = lvalCols.get(lvalColIdx);
+        TableCreator.ColumnState colState = 
+          creator.getColumnState(lvalCol);
+
+        umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+
+        if((umapType == 1) && 
+           (umapPageNumber != colState.getUmapPageNumber())) {
+          // we want to force both usage maps for a column to be on the same
+          // data page, so just discard the previous one we wrote
+          --i;
+          umapType = 0;
+        }
+        
+        if(umapType == 0) {
+          // lval column "owned pages" usage map
+          colState.setUmapOwnedRowNumber((byte)umapRowNum);
+          colState.setUmapPageNumber(umapPageNumber);
+        } else {
+          // lval column "free space pages" usage map (always on same page)
+          colState.setUmapFreeRowNumber((byte)umapRowNum);
+        } 
       }
-    }
 
-    pageChannel.writePage(rtn, umapPageNumber);
+      rowStart -= umapRowLength;
+      freeSpace -= umapSpaceUsage;
+      ++umapRowNum;
+
+      if((freeSpace <= umapSpaceUsage) || (i == (umapNum - 1))) {
+        // finish current page
+        umapBuf.putShort(format.OFFSET_FREE_SPACE, (short)freeSpace);
+        umapBuf.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, 
+                         (short)umapRowNum);
+        pageChannel.writePage(umapBuf, umapPageNumber);
+        umapBuf = null;
+      }
+    }
   }
 
   /**
@@ -1256,14 +1333,10 @@ public class Table
     _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);
+    tableBuffer.position(getFormat().OFFSET_OWNED_PAGES);
+    _ownedPages = UsageMap.read(getDatabase(), tableBuffer, false);
+    tableBuffer.position(getFormat().OFFSET_FREE_SPACE_PAGES);
+    _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false);
     
     for (int i = 0; i < _indexCount; i++) {
       _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat()));
@@ -1323,6 +1396,27 @@ public class Table
     
     Collections.sort(_indexes);
 
+    // read column usage map info
+    while(tableBuffer.remaining() >= 2) {
+
+      short umapColNum = tableBuffer.getShort();
+      if(umapColNum == IndexData.COLUMN_UNUSED) {
+        break;
+      }
+      
+      UsageMap colOwnedPages = UsageMap.read(
+          getDatabase(), tableBuffer, false);
+      UsageMap colFreeSpacePages = UsageMap.read(
+          getDatabase(), tableBuffer, false);
+    
+      for(Column col : _columns) {
+        if(col.getColumnNumber() == umapColNum) {
+          col.setUsageMaps(colOwnedPages, colFreeSpacePages);
+          break;
+        }
+      }
+    }
+
     // re-sort columns if necessary
     if(getDatabase().getColumnOrder() != ColumnOrder.DATA) {
       Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
@@ -1683,46 +1777,67 @@ public class Table
                                       int pageNumber)
     throws IOException
   {
+    // assume incoming page is modified
+    boolean modifiedPage = true;
+
     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;
-        }
-      }
+      // find owned page w/ free space
+      dataPage = findFreeRowSpace(_ownedPages, _freeSpacePages, 
+                                  _addRowBufferH);
 
-      if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) {
+      if(dataPage == null) {
         // No data pages exist (with free space).  Create a new one.
         return newDataPage();
       }
-    
+
+      // found a page, see if it will work
+      pageNumber = _addRowBufferH.getPageNumber();
+      // since we just loaded this page, it is not yet modified
+      modifiedPage = false;
     }
 
     if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) {
 
-      // Last data page is full.  Create a new one.
-      writeDataPage(dataPage, pageNumber);
-      _freeSpacePages.removePageNumber(pageNumber);
+      // Last data page is full.  Write old one and create a new one.
+      if(modifiedPage) {
+        writeDataPage(dataPage, pageNumber);
+      }
+      _freeSpacePages.removePageNumber(pageNumber, true);
 
       dataPage = newDataPage();
     }
 
     return dataPage;
   }
+
+  static ByteBuffer findFreeRowSpace(
+      UsageMap ownedPages, UsageMap freeSpacePages,
+      TempPageHolder rowBufferH)
+    throws IOException
+  {
+    // find last data page (Not bothering to check other pages for free
+    // space.)
+    UsageMap.PageCursor revPageCursor = ownedPages.cursor();
+    revPageCursor.afterLast();
+    while(true) {
+      int tmpPageNumber = revPageCursor.getPreviousPage();
+      if(tmpPageNumber < 0) {
+        break;
+      }
+      ByteBuffer dataPage = rowBufferH.setPage(ownedPages.getPageChannel(),
+                                               tmpPageNumber);
+      if(dataPage.get() == PageTypes.DATA) {
+        // found last data page, only use if actually listed in free space
+        // pages
+        if(freeSpacePages.containsPageNumber(tmpPageNumber)) {
+          return dataPage;
+        }
+      }
+    }
+
+    return null;
+  }
  
   /**
    * Updates the table definition after rows are modified.
@@ -2120,7 +2235,7 @@ public class Table
    * 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)
+  static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format)
     throws IOException
   {
     int rowsOnPage = 0;
index 75aab7c7a6f88c8d39bec82d0e2148ed031202b7..9f7911da5f64b89ab613f1c36b597a3198728cf9 100644 (file)
@@ -21,10 +21,11 @@ package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -42,7 +43,10 @@ class TableCreator
   private final List<Column> _columns;
   private final List<IndexBuilder> _indexes;
   private final Map<IndexBuilder,IndexState> _indexStates = 
-    new HashMap<IndexBuilder,IndexState>();
+    new IdentityHashMap<IndexBuilder,IndexState>();
+  private final Map<Column,ColumnState> _columnStates = 
+    new IdentityHashMap<Column,ColumnState>();
+  private final List<Column> _lvalCols = new ArrayList<Column>();
   private int _tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
   private int _umapPageNumber = PageChannel.INVALID_PAGE_NUMBER;
   private int _indexCount;
@@ -105,6 +109,14 @@ class TableCreator
     return getPageChannel().allocateNewPage();
   }
 
+  public ColumnState getColumnState(Column col) {
+    return _columnStates.get(col);
+  }
+
+  public List<Column> getLongValueColumns() {
+    return _lvalCols;
+  }
+
   /**
    * Creates the table in the database.
    * @usage _advanced_method_
@@ -113,6 +125,17 @@ class TableCreator
 
     validate();
 
+    // assign column numbers and do some assorted column bookkeeping
+    short columnNumber = (short) 0;
+    for(Column col : _columns) {
+      col.setColumnNumber(columnNumber++);
+      if(col.getType().isLongValue()) {
+        _lvalCols.add(col);
+        // only lval columns need extra state
+        _columnStates.put(col, new ColumnState());
+      }
+    }
+
     if(hasIndexes()) {
       // sort out index numbers.  for now, these values will always match
       // (until we support writing foreign key indexes)
@@ -267,7 +290,42 @@ class TableCreator
 
     public void setRootPageNumber(int newRootPageNumber) {
       _rootPageNumber = newRootPageNumber;
+    }    
+  }
+
+  /**
+   * Maintains additional state used during column creation.
+   * @usage _advanced_class_
+   */
+  static final class ColumnState
+  {
+    private byte _umapOwnedRowNumber;
+    private byte _umapFreeRowNumber;
+    // we always put both usage maps on the same page
+    private int _umapPageNumber;
+
+    public byte getUmapOwnedRowNumber() {
+      return _umapOwnedRowNumber;
+    }
+
+    public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) {
+      _umapOwnedRowNumber = newUmapOwnedRowNumber;
+    }
+
+    public byte getUmapFreeRowNumber() {
+      return _umapFreeRowNumber;
+    }
+
+    public void setUmapFreeRowNumber(byte newUmapFreeRowNumber) {
+      _umapFreeRowNumber = newUmapFreeRowNumber;
+    }
+
+    public int getUmapPageNumber() {
+      return _umapPageNumber;
+    }
+
+    public void setUmapPageNumber(int newUmapPageNumber) {
+      _umapPageNumber = newUmapPageNumber;
     }
-    
   }
 }
index 931891e529878983ed1cfd4790cec96606aabe6f..fa5d694cc85f73795dc447a6e90a36fa70848775 100644 (file)
@@ -109,6 +109,21 @@ public class UsageMap
   public PageChannel getPageChannel() {
     return getDatabase().getPageChannel();
   }
+
+  /**
+   * @param database database that contains this usage map
+   * @param buf buffer which contains the usage map row info
+   * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on
+   *         which type of map is found
+   */
+  public static UsageMap read(Database database, ByteBuffer buf,
+                              boolean assumeOutOfRangeBitsOn)
+    throws IOException
+  {
+    int umapRowNum = buf.get();
+    int umapPageNum = ByteUtil.get3ByteInt(buf);
+    return read(database, umapPageNum, umapRowNum, false);
+  }
   
   /**
    * @param database database that contains this usage map