]> source.dussan.org Git - jackcess.git/commitdiff
implement multi-page long value writing
authorJames Ahlborn <jtahlborn@yahoo.com>
Mon, 2 Oct 2006 15:57:57 +0000 (15:57 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Mon, 2 Oct 2006 15:57:57 +0000 (15:57 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@127 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/PageChannel.java
src/java/com/healthmarketscience/jackcess/Table.java
test/data/test2BinData.dat [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java

index 5237c2b031e4f508ab03c409ff066569bff97c18..a8f2265831a77c4118db2e9131919402171384a3 100644 (file)
@@ -313,7 +313,7 @@ public class Column implements Comparable<Column> {
       return readCurrencyValue(buffer);
     } else if (_type == DataType.OLE) {
       if (data.length > 0) {
-        return readLongBinaryValue(data);
+        return readLongValue(data);
       } else {
         return null;
       }
@@ -340,7 +340,7 @@ public class Column implements Comparable<Column> {
    *                <code>LONG_VALUE_TYPE_*</code>
    * @return The LVAL data
    */
-  private byte[] readLongBinaryValue(byte[] lvalDefinition)
+  private byte[] readLongValue(byte[] lvalDefinition)
     throws IOException
   {
     ByteBuffer def = ByteBuffer.wrap(lvalDefinition);
@@ -353,18 +353,30 @@ public class Column implements Comparable<Column> {
     }
     byte[] rtn = new byte[length];
     byte type = def.get();
-    switch (type) {
-      case LONG_VALUE_TYPE_OTHER_PAGE:
-        if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) {
-          throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
-              " bytes in long value definition, but found " +
-                                lvalDefinition.length);
-        }
 
+    if(type == LONG_VALUE_TYPE_THIS_PAGE) {
+
+      // inline long value
+      def.getInt();  //Skip over lval_dp
+      def.getInt();  //Skip over unknown
+      def.get(rtn);
+
+    } else {
+
+      // long value on other page(s)
+      if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) {
+        throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
+                              " bytes in long value definition, but found " +
+                              lvalDefinition.length);
+      }
+
+      byte rowNum = def.get();
+      int pageNum = ByteUtil.get3ByteInt(def, def.position());
+      ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+      
+      switch (type) {
+      case LONG_VALUE_TYPE_OTHER_PAGE:
         {
-          byte rowNum = def.get();
-          int pageNum = ByteUtil.get3ByteInt(def, def.position());
-          ByteBuffer lvalPage = _pageChannel.createPageBuffer();
           _pageChannel.readPage(lvalPage, pageNum);
 
           short rowStart = Table.findRowStart(lvalPage, rowNum, _format);
@@ -379,25 +391,9 @@ public class Column implements Comparable<Column> {
         }
         break;
         
-      case LONG_VALUE_TYPE_THIS_PAGE:
-        def.getInt();  //Skip over lval_dp
-        def.getInt();  //Skip over unknown
-        def.get(rtn);
-        break;
-        
       case LONG_VALUE_TYPE_OTHER_PAGES:
 
         ByteBuffer rtnBuf = ByteBuffer.wrap(rtn);
-        
-        if (lvalDefinition.length != _format.SIZE_LONG_VALUE_DEF) {
-          throw new IOException("Expected " + _format.SIZE_LONG_VALUE_DEF +
-              " bytes in long value definition, but found " +
-                                lvalDefinition.length);
-        }
-        byte rowNum = def.get();
-        int pageNum = ByteUtil.get3ByteInt(def, def.position());
-        ByteBuffer lvalPage = _pageChannel.createPageBuffer();
-
         int remainingLen = length;
         while(remainingLen > 0) {
           lvalPage.clear();
@@ -405,7 +401,6 @@ public class Column implements Comparable<Column> {
 
           short rowStart = Table.findRowStart(lvalPage, rowNum, _format);
           short rowEnd = Table.findRowEnd(lvalPage, rowNum, _format);
-
           
           // read next page information
           lvalPage.position(rowStart);
@@ -428,7 +423,9 @@ public class Column implements Comparable<Column> {
         
       default:
         throw new IOException("Unrecognized long value type: " + type);
+      }
     }
+    
     return rtn;
   }
   
@@ -439,7 +436,7 @@ public class Column implements Comparable<Column> {
   private String readLongStringValue(byte[] lvalDefinition)
     throws IOException
   {
-    byte[] binData = readLongBinaryValue(lvalDefinition);
+    byte[] binData = readLongValue(lvalDefinition);
     if(binData == null) {
       return null;
     }
@@ -637,61 +634,137 @@ public class Column implements Comparable<Column> {
   }
   
   /**
-   * Write an LVAL column into a ByteBuffer inline (LONG_VALUE_TYPE_THIS_PAGE)
+   * Write an LVAL column into a ByteBuffer inline if it fits, otherwise in
+   * other data page(s).
    * @param value Value of the LVAL column
-   * @return A buffer containing the LVAL definition and the column value
+   * @return A buffer containing the LVAL definition and (possibly) the column
+   *         value (unless written to other pages)
    */
   public ByteBuffer writeLongValue(byte[] value,
                                    int remainingRowLength) throws IOException
   {
-    // FIXME, take remainingRowLength into account (don't always write inline)
-    
     if(value.length > getType().getMaxSize()) {
       throw new IOException("value too big for column");
     }
-    ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF + value.length);
+
+    // determine which type to write
+    byte type = 0;
+    int lvalDefLen = _format.SIZE_LONG_VALUE_DEF;
+    if((_format.SIZE_LONG_VALUE_DEF + value.length) <= remainingRowLength) {
+      type = LONG_VALUE_TYPE_THIS_PAGE;
+      lvalDefLen += value.length;
+    } else if(Table.getRowSpaceUsage(value.length, _format) <=
+              _format.MAX_ROW_SIZE)
+    {
+      type = LONG_VALUE_TYPE_OTHER_PAGE;
+    } else {
+      type = LONG_VALUE_TYPE_OTHER_PAGES;
+    }
+
+    ByteBuffer def = ByteBuffer.allocate(lvalDefLen);
     def.order(ByteOrder.LITTLE_ENDIAN);
     ByteUtil.put3ByteInt(def, value.length);
-    def.put(LONG_VALUE_TYPE_THIS_PAGE);
-    def.putInt(0);
-    def.putInt(0);  //Unknown
-    def.put(value);
+    def.put(type);
+
+    if(type == LONG_VALUE_TYPE_THIS_PAGE) {
+      // write long value inline
+      def.putInt(0);
+      def.putInt(0);  //Unknown
+      def.put(value);
+    } else {
+      
+      int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER;
+      byte firstLvalRow = 0;
+
+      ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+      
+      // write other page(s)
+      switch(type) {
+      case LONG_VALUE_TYPE_OTHER_PAGE:
+        writeLongValueHeader(lvalPage);
+        firstLvalRow = (byte)Table.addDataPageRow(lvalPage,
+                                                  value.length,
+                                                  _format);
+        lvalPage.put(value);
+        firstLvalPageNum = _pageChannel.writeNewPage(lvalPage);
+        break;
+
+      case LONG_VALUE_TYPE_OTHER_PAGES:
+
+        ByteBuffer buffer = ByteBuffer.wrap(value);
+        int remainingLen = buffer.remaining();
+        buffer.limit(0);
+        int lvalPageNum = _pageChannel.allocateNewPage();
+        byte lvalRow = 0;
+        int nextLvalPageNum = 0;
+        while(remainingLen > 0) {
+          lvalPage.clear();
+          writeLongValueHeader(lvalPage);
+
+          // figure out how much we will put in this page
+          int chunkLength = Math.min(_format.MAX_ROW_SIZE - 4,
+                                     remainingLen);
+          nextLvalPageNum = ((chunkLength < remainingLen) ?
+                             _pageChannel.allocateNewPage() : 0);
+
+          // add row to this page
+          lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4,
+                                               _format);
+          
+          // write next page info (we'll always be writing into row 0 for
+          // newly created pages)
+          lvalPage.put((byte)0); // row number
+          ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number
+
+          // write this page's chunk of data
+          buffer.limit(buffer.limit() + chunkLength);
+          lvalPage.put(buffer);
+          remainingLen -= chunkLength;
+
+          // write new page to database
+          _pageChannel.writePage(lvalPage, lvalPageNum);
+          
+          // hang onto first page info
+          if(firstLvalPageNum == PageChannel.INVALID_PAGE_NUMBER) {
+            firstLvalPageNum = lvalPageNum;
+            firstLvalRow = lvalRow;
+          }
+
+          // move to next page
+          lvalPageNum = nextLvalPageNum;
+        }
+        break;
+
+      default:
+        throw new IOException("Unrecognized long value type: " + type);
+      }
+
+      // update def
+      def.put(firstLvalRow);
+      ByteUtil.put3ByteInt(def, firstLvalPageNum);
+      def.putInt(0);  //Unknown
+      
+    }
+      
     def.flip();
-    return def;    
+    return def;
   }
-  
+
   /**
-   * Write an LVAL column into a ByteBuffer on another page
-   *    (LONG_VALUE_TYPE_OTHER_PAGE)
-   * @param value Value of the LVAL column
-   * @return A buffer containing the LVAL definition
+   * Writes the header info for a long value page.
    */
-  // FIXME, unused?
-  private ByteBuffer writeLongValueInNewPage(byte[] value) throws IOException {
-    ByteBuffer lvalPage = _pageChannel.createPageBuffer();
+  private void writeLongValueHeader(ByteBuffer lvalPage)
+  {
     lvalPage.put(PageTypes.DATA); //Page type
     lvalPage.put((byte) 1); //Unknown
     lvalPage.putShort((short) (_format.PAGE_SIZE -
-        _format.OFFSET_LVAL_ROW_LOCATION_BLOCK - _format.SIZE_ROW_LOCATION -
-        value.length)); //Free space
+                               _format.OFFSET_ROW_START)); //Free space
     lvalPage.put((byte) 'L');
     lvalPage.put((byte) 'V');
     lvalPage.put((byte) 'A');
     lvalPage.put((byte) 'L');
-    int offset = _format.PAGE_SIZE - value.length;
-    lvalPage.position(14);
-    lvalPage.putShort((short) offset);
-    lvalPage.position(offset);
-    lvalPage.put(value);
-    ByteBuffer def = ByteBuffer.allocate(_format.SIZE_LONG_VALUE_DEF);
-    def.order(ByteOrder.LITTLE_ENDIAN);
-    ByteUtil.put3ByteInt(def, value.length);
-    def.put(LONG_VALUE_TYPE_OTHER_PAGE);
-    def.put((byte) 0); //Row number
-    ByteUtil.put3ByteInt(def, _pageChannel.writeNewPage(lvalPage));  //Page #
-    def.putInt(0);  //Unknown
-    def.flip();
-    return def;    
+    lvalPage.putShort((short)0); // num rows in page
+    lvalPage.putInt(0); //unknown
   }
   
   /**
@@ -721,8 +794,6 @@ public class Column implements Comparable<Column> {
     // var length column
     if(!getType().isLongValue()) {
 
-      // FIXME, take remainingRowLength into account?  overflow pages?
-      
       // this is an "inline" var length field
       switch(getType()) {
       case NUMERIC:
index 327e96a6f26073c1f60a510dedc1457cc3293e3b..b1f0c2536ef6db32dc6772ce10d5653f169773e4 100644 (file)
@@ -100,8 +100,6 @@ public abstract class JetFormat {
   public final int OFFSET_FREE_SPACE;
   public final int OFFSET_NUM_ROWS_ON_DATA_PAGE;
   
-  public final int OFFSET_LVAL_ROW_LOCATION_BLOCK;
-  
   public final int OFFSET_USED_PAGES_USAGE_MAP_DEF;
   public final int OFFSET_FREE_PAGES_USAGE_MAP_DEF;
   
@@ -185,8 +183,6 @@ public abstract class JetFormat {
     OFFSET_FREE_SPACE = defineOffsetFreeSpace();
     OFFSET_NUM_ROWS_ON_DATA_PAGE = defineOffsetNumRowsOnDataPage();
     
-    OFFSET_LVAL_ROW_LOCATION_BLOCK = defineOffsetLvalRowLocationBlock();
-    
     OFFSET_USED_PAGES_USAGE_MAP_DEF = defineOffsetUsedPagesUsageMapDef();
     OFFSET_FREE_PAGES_USAGE_MAP_DEF = defineOffsetFreePagesUsageMapDef();
     
@@ -249,8 +245,6 @@ public abstract class JetFormat {
   protected abstract int defineOffsetFreeSpace();
   protected abstract int defineOffsetNumRowsOnDataPage();
   
-  protected abstract int defineOffsetLvalRowLocationBlock();
-  
   protected abstract int defineOffsetUsedPagesUsageMapDef();
   protected abstract int defineOffsetFreePagesUsageMapDef();
   
@@ -314,8 +308,6 @@ public abstract class JetFormat {
     protected int defineOffsetFreeSpace() { return 2; }
     protected int defineOffsetNumRowsOnDataPage() { return 12; }
     
-    protected int defineOffsetLvalRowLocationBlock() { return 10; }
-    
     protected int defineOffsetUsedPagesUsageMapDef() { return 4027; }
     protected int defineOffsetFreePagesUsageMapDef() { return 3958; }
     
index 0acaad8a36bee4db3e0de6c48ef63b8064defd9d..38653945e7cedf897ffc2c3570fc377d85e70b61 100644 (file)
@@ -44,6 +44,9 @@ public class PageChannel implements Channel {
   private static final Log LOG = LogFactory.getLog(PageChannel.class);
   
   static final int INVALID_PAGE_NUMBER = -1;
+
+  /** dummy buffer used when allocating new pages */
+  private static final ByteBuffer FORCE_BYTES = ByteBuffer.allocate(1);
   
   /** Global usage map always lives on page 1 */
   private static final int PAGE_GLOBAL_USAGE_MAP = 1;
@@ -113,6 +116,21 @@ public class PageChannel implements Channel {
     _globalUsageMap.removePageNumber(pageNumber);  //force is done here
     return pageNumber;
   }
+
+  /**
+   * Allocates a new page in the database.  Data in the page is undefined
+   * until it is written in a call to {@link #writePage}.
+   */
+  public int allocateNewPage() throws IOException {
+    long size = _channel.size();
+    FORCE_BYTES.rewind();
+    long offset = size + _format.PAGE_SIZE - FORCE_BYTES.remaining();
+    // this will force the file to be extended with mostly undefined bytes
+    _channel.write(FORCE_BYTES, offset);
+    int pageNumber = (int) (size / _format.PAGE_SIZE);
+    _globalUsageMap.removePageNumber(pageNumber);  //force is done here
+    return pageNumber;
+  }
   
   /**
    * @return Number of pages in the database
index ba65ab70fc0585f91aafe00ef8db65e249097c31..a0ed9a6256658d9471c9d817169b28fbb6963ff6 100644 (file)
@@ -701,7 +701,7 @@ public class Table
     }
     
     for (int i = 0; i < rowData.length; i++) {
-      rowSize = rowData[i].limit();
+      rowSize = rowData[i].remaining();
       int rowSpaceUsage = getRowSpaceUsage(rowSize, _format);
       short freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE);
       if (freeSpaceInPage < rowSpaceUsage) {
@@ -714,21 +714,16 @@ public class Table
         pageNumber = newDataPage(dataPage);
         freeSpaceInPage = dataPage.getShort(_format.OFFSET_FREE_SPACE);
       }
-      //Decrease free space record.
-      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));
-      short rowLocation = findRowEnd(dataPage, rowCount, _format);
-      rowLocation -= rowSize;
-      dataPage.putShort(getRowStartOffset(rowCount, _format), rowLocation);
-      dataPage.position(rowLocation);
+
+      // write out the row data
+      int rowNum = addDataPageRow(dataPage, rowSize, _format);
       dataPage.put(rowData[i]);
+
+      // update the indexes
       Iterator<Index> indIter = _indexes.iterator();
       while (indIter.hasNext()) {
         Index index = (Index) indIter.next();
-        index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowCount);
+        index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowNum);
       }
     }
     writeDataPage(dataPage, pageNumber);
@@ -926,6 +921,40 @@ public class Table
     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
+   */
+  public static int addDataPageRow(ByteBuffer dataPage,
+                                   int rowSize,
+                                   JetFormat format)
+  {
+    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), rowLocation);
+
+    // set position for row data
+    dataPage.position(rowLocation);
+
+    return rowCount;
+  }
+  
   public static short findRowStart(ByteBuffer buffer, int rowNum,
                                    JetFormat format)
   {
diff --git a/test/data/test2BinData.dat b/test/data/test2BinData.dat
new file mode 100644 (file)
index 0000000..3763155
Binary files /dev/null and b/test/data/test2BinData.dat differ
index 4fc87227bb6258dfb0a59f4d93005ac61b456a3a..51d9220c27f53c60d6552d66c526369765d3041c 100644 (file)
@@ -37,8 +37,16 @@ public class DatabaseTest extends TestCase {
   }
   
   static Database create() throws Exception {
+    return create(false);
+  }
+
+  static Database create(boolean keep) throws Exception {
     File tmp = File.createTempFile("databaseTest", ".mdb");
-    tmp.deleteOnExit();
+    if(keep) {
+      System.out.println("Created " + tmp);
+    } else {
+      tmp.deleteOnExit();
+    }
     return Database.create(tmp);
   }
 
@@ -277,7 +285,7 @@ public class DatabaseTest extends TestCase {
     assertEquals(2, table.getNextRow().get("D"));
   }
 
-  public void testReadMemo() throws Exception {
+  public void testReadLongValue() throws Exception {
 
     Database db = Database.open(new File("test/data/test2.mdb"));
     Table table = db.getTable("MSP_PROJECTS");
@@ -286,9 +294,14 @@ public class DatabaseTest extends TestCase {
     assertEquals("T", row.get("PROJ_PROP_COMPANY"));
     assertEquals("Standard", row.get("PROJ_INFO_CAL_NAME"));
     assertEquals("Project1", row.get("PROJ_PROP_TITLE"));
+    byte[] foundBinaryData = (byte[])row.get("RESERVED_BINARY_DATA");
+    System.out.println("FOO found len " + foundBinaryData.length);
+    byte[] expectedBinaryData =
+      toByteArray(new File("test/data/test2BinData.dat"));
+    assertTrue(Arrays.equals(expectedBinaryData, foundBinaryData));
   }
 
-  public void testWriteMemo() throws Exception {
+  public void testWriteLongValue() throws Exception {
 
     Database db = create();
 
@@ -301,18 +314,39 @@ public class DatabaseTest extends TestCase {
     col.setName("B");
     col.setType(DataType.MEMO);
     columns.add(col);
+    col = new Column();
+    col.setName("C");
+    col.setType(DataType.OLE);
+    columns.add(col);
     db.createTable("test", columns);
 
     String testStr = "This is a test";
+    StringBuilder strBuf = new StringBuilder();
+    for(int i = 0; i < 2030; ++i) {
+      char c = (char)('a' + (i % 26));
+      strBuf.append(c);
+    }
+    String longMemo = strBuf.toString();
+    byte[] oleValue = toByteArray(new File("test/data/test2BinData.dat"));
+    
     
     Table table = db.getTable("Test");
-    table.addRow(new Object[]{testStr, testStr});
+    table.addRow(testStr, testStr, null);
+    table.addRow(testStr, longMemo, oleValue);
+
     table.reset();
 
     Map<String, Object> row = table.getNextRow();
 
     assertEquals(testStr, row.get("A"));
     assertEquals(testStr, row.get("B"));
+    assertNull(row.get("C"));
+
+    row = table.getNextRow();
+    
+    assertEquals(testStr, row.get("A"));
+    assertEquals(longMemo, row.get("B"));
+    assertTrue(Arrays.equals(oleValue, (byte[])row.get("C")));
     
   }
 
@@ -635,5 +669,20 @@ public class DatabaseTest extends TestCase {
       ostream.close();
     }
   }
+
+  static byte[] toByteArray(File file)
+    throws IOException
+  {
+    // FIXME should really be using commons io IOUtils here, but don't want
+    // to add dep for one simple test method
+    FileInputStream istream = new FileInputStream(file);
+    try {
+      byte[] bytes = new byte[(int)file.length()];
+      istream.read(bytes);
+      return bytes;
+    } finally {
+      istream.close();
+    }
+  }
   
 }