diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2013-07-09 01:39:40 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2013-07-09 01:39:40 +0000 |
commit | a435d70e58333d20f86f53ebae83959f438239a2 (patch) | |
tree | b6e5f0ff4db569be7619ba6fcf079ad8dde851ed | |
parent | b8905cf9ecec87f760c923eb972868e070290e7d (diff) | |
download | jackcess-a435d70e58333d20f86f53ebae83959f438239a2.tar.gz jackcess-a435d70e58333d20f86f53ebae83959f438239a2.zip |
merge trunk changes through r744
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@745 f203690c-595d-4dc9-a70b-905162fa7fd2
6 files changed, 439 insertions, 137 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml index a3e5c6f..181fe3f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,7 +4,12 @@ <author email="javajedi@users.sf.net">Tim McCune</author> </properties> <body> - <release version="1.2.13" date="TBD"> + <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 full page encoding. diff --git a/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 154d939..13eb370 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -218,6 +218,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { private final ComplexColumnInfo<? extends ComplexValue> _complexInfo; /** properties for this column, if any */ private PropertyMap _props; + /** Holds additional info for writing long values */ + private LongValueBufferHolder _lvalBufferH; /** * @usage _advanced_method_ @@ -325,10 +327,20 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } } + /** + * 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) { ((ComplexColumnInfoImpl<? extends ComplexValue>)_complexInfo) .postTableLoadInit(); @@ -473,6 +485,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _textInfo._versionHistoryCol; } + /** + * Returns the number of database pages owned by this column. + * @usage _intermediate_method_ + */ + public int getOwnedPageCount() { + return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount()); + } + /** * @usage _advanced_method_ */ @@ -1054,7 +1074,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { def.put(value); } else { - TempPageHolder lvalBufferH = getTable().getLongValueBuffer(); ByteBuffer lvalPage = null; int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER; byte firstLvalRow = 0; @@ -1062,8 +1081,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // 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)TableImpl.addDataPageRow(lvalPage, value.length, getFormat(), 0); lvalPage.put(value); @@ -1075,12 +1094,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { 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)TableImpl.getRowsOnDataPage(lvalPage, getFormat()); int lvalPageNum = firstLvalPageNum; ByteBuffer nextLvalPage = null; int nextLvalPageNum = 0; + int nextLvalRowNum = 0; while(remainingLen > 0) { lvalPage.clear(); @@ -1091,23 +1111,25 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // 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 = TableImpl.getRowsOnDataPage(nextLvalPage, + getFormat()); } else { nextLvalPage = null; nextLvalPageNum = 0; + nextLvalRowNum = 0; } // add row to this page byte lvalRow = (byte)TableImpl.addDataPageRow(lvalPage, chunkLength + 4, - getFormat(), 0); + 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 @@ -1118,17 +1140,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // 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; lvalPageNum = nextLvalPageNum; @@ -1167,28 +1178,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } /** - * 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(TableImpl.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 * @param obj Object to serialize @@ -1805,7 +1794,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { throws IOException { List<ColumnBuilder> 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 @@ -1813,13 +1801,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // all (because "long variable" values can go in separate pages) short longVariableOffset = countNonLongVariableLength(columns); for (ColumnBuilder col : columns) { - // record this for later use when writing indexes - col.setColumnNumber(columnNumber); - int position = buffer.position(); buffer.put(col.getType().getValue()); buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number - buffer.putShort(columnNumber); //Column Number + buffer.putShort(col.getColumnNumber()); //Column Number if (col.getType().isVariableLength()) { if(!col.getType().isLongValue()) { buffer.putShort(variableOffset++); @@ -1829,7 +1814,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } 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) @@ -1863,7 +1848,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } else { buffer.putShort((short)0x0000); // unused } - columnNumber++; } for (ColumnBuilder col : columns) { TableImpl.writeName(buffer, col.getName(), creator.getCharset()); @@ -2165,4 +2149,132 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { 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(TableImpl.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 = TableImpl.findFreeRowSpace( + _ownedPages, _freeSpacePages, _longValueBufferH); + + if(newPage != null) { + if(TableImpl.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(); + } + } } diff --git a/src/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/java/com/healthmarketscience/jackcess/impl/IndexData.java index f4e95ee..a1e945b 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -81,7 +81,7 @@ public class IndexData { protected static final byte[] EMPTY_PREFIX = new byte[0]; - private static final short COLUMN_UNUSED = -1; + static final short COLUMN_UNUSED = -1; public static final byte ASCENDING_COLUMN_FLAG = (byte)0x01; @@ -335,6 +335,7 @@ public class IndexData { /** * Returns the number of database pages owned by this index data. + * @usage _intermediate_method_ */ public int getOwnedPageCount() { return _ownedPages.getPageCount(); @@ -432,10 +433,7 @@ public 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(); diff --git a/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java b/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java index 2311d36..8828ac2 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java +++ b/src/java/com/healthmarketscience/jackcess/impl/TableCreator.java @@ -24,15 +24,15 @@ 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; import com.healthmarketscience.jackcess.ColumnBuilder; -import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.IndexBuilder; /** * Helper class used to maintain state during table creation. @@ -47,7 +47,10 @@ class TableCreator private final List<ColumnBuilder> _columns; private final List<IndexBuilder> _indexes; private final Map<IndexBuilder,IndexState> _indexStates = - new HashMap<IndexBuilder,IndexState>(); + new IdentityHashMap<IndexBuilder,IndexState>(); + private final Map<ColumnBuilder,ColumnState> _columnStates = + new IdentityHashMap<ColumnBuilder,ColumnState>(); + private final List<ColumnBuilder> _lvalCols = new ArrayList<ColumnBuilder>(); private int _tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; private int _umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; private int _indexCount; @@ -110,6 +113,14 @@ class TableCreator return getPageChannel().allocateNewPage(); } + public ColumnState getColumnState(ColumnBuilder col) { + return _columnStates.get(col); + } + + public List<ColumnBuilder> getLongValueColumns() { + return _lvalCols; + } + /** * Creates the table in the database. * @usage _advanced_method_ @@ -118,6 +129,17 @@ class TableCreator validate(); + // assign column numbers and do some assorted column bookkeeping + short columnNumber = (short) 0; + for(ColumnBuilder 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) @@ -291,6 +313,41 @@ 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; + } } } diff --git a/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java index f69e8dd..a42b7c2 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -167,7 +167,7 @@ public class TableImpl implements 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); /** optional error handler to use when row errors are encountered */ @@ -246,14 +246,10 @@ public class TableImpl implements 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())); @@ -263,6 +259,27 @@ public class TableImpl implements Table readIndexDefinitions(tableBuffer); + // 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(ColumnImpl col : _columns) { + if(col.getColumnNumber() == umapColNum) { + col.setUsageMaps(colOwnedPages, colFreeSpacePages); + break; + } + } + } + // re-sort columns if necessary if(getDatabase().getColumnOrder() != ColumnOrder.DATA) { Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); @@ -354,14 +371,21 @@ public class TableImpl implements 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(ColumnImpl col : _columns) { + count += col.getOwnedPageCount(); + } + // note, we count owned pages from _physical_ indexes, not logical indexes // (otherwise we could double count pages) for(IndexData indexData : _indexDatas) { count += indexData.getOwnedPageCount(); } + return count; } @@ -920,9 +944,10 @@ public class TableImpl implements 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) @@ -959,6 +984,20 @@ public class TableImpl implements Table IndexImpl.writeDefinitions(creator, buffer); } + // write long value column usage map references + for(ColumnBuilder lvalCol : creator.getLongValueColumns()) { + buffer.putShort(lvalCol.getColumnNumber()); + TableCreator.ColumnState colState = + creator.getColumnState(lvalCol); + + // owned pages umap (both are on same page) + buffer.put(colState.getUmapOwnedRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + // free space pages umap + buffer.put(colState.getUmapFreeRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + } + //End of tabledef buffer.put((byte) 0xff); buffer.put((byte) 0xff); @@ -1083,53 +1122,70 @@ public class TableImpl implements Table private static void createUsageMapDefinitionBuffer(TableCreator creator) throws IOException { - // 2 table usage maps plus 1 for each index - int umapNum = 2 + creator.getIndexCount(); + List<ColumnBuilder> lvalCols = creator.getLongValueColumns(); + + // 2 table usage maps plus 1 for each index and 2 for each lval col + int indexUmapEnd = 2 + creator.getIndexCount(); + int umapNum = indexUmapEnd + (lvalCols.size() * 2); JetFormat format = creator.getFormat(); - int 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)); + int umapSpaceUsage = getRowSpaceUsage(umapRowLength, format); + PageChannel pageChannel = creator.getPageChannel(); + int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; + ByteBuffer umapBuf = null; + int freeSpace = 0; + int rowStart = 0; + int umapRowNum = 0; - // for now, don't handle writing that many indexes - if(freeSpace < 0) { - throw new IOException("FIXME attempting to write too many indexes"); + for(int i = 0; i < umapNum; ++i) { + + if(umapBuf == null) { + + // need new page for usage maps + if(umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) { + // first umap page has already been reserved + umapPageNumber = creator.getUmapPageNumber(); + } else { + // need another umap page + umapPageNumber = creator.reservePageNumber(); } - int umapPageNumber = creator.getUmapPageNumber(); + freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE; - 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); + 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()) { + umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); - for(int i = 0; i < creator.getIndexes().size(); ++i) { - IndexBuilder idx = creator.getIndexes().get(i); + 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); @@ -1138,16 +1194,54 @@ public class TableImpl implements 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; + + ColumnBuilder lvalCol = lvalCols.get(lvalColIdx); + TableCreator.ColumnState colState = + creator.getColumnState(lvalCol); + + umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); - rowStart -= usageMapRowLength; + 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; + } + } } /** @@ -1647,47 +1741,68 @@ public class TableImpl implements Table int pageNumber) throws IOException { + // assume incoming page is modified + boolean modifiedPage = true; + if(dataPage == null) { + // find owned page w/ free space + dataPage = findFreeRowSpace(_ownedPages, _freeSpacePages, + _addRowBufferH); + + if(dataPage == null) { + // No data pages exist (with free space). Create a new one. + return newDataPage(); + } + + // found a page, see if it will work + pageNumber = _addRowBufferH.getPageNumber(); + // since we just loaded this page, it is not yet modified + modifiedPage = false; + } + + if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) { + + // Last data page is full. Write old one and create a new one. + if(modifiedPage) { + writeDataPage(dataPage, pageNumber); + } + _freeSpacePages.removePageNumber(pageNumber, true); + + dataPage = newDataPage(); + } + + return dataPage; + } + + static ByteBuffer findFreeRowSpace( + UsageMap ownedPages, UsageMap freeSpacePages, + TempPageHolder rowBufferH) + throws IOException + { // find last data page (Not bothering to check other pages for free // space.) - UsageMap.PageCursor revPageCursor = _ownedPages.cursor(); + UsageMap.PageCursor revPageCursor = ownedPages.cursor(); revPageCursor.afterLast(); while(true) { int tmpPageNumber = revPageCursor.getPreviousPage(); if(tmpPageNumber < 0) { break; } - dataPage = _addRowBufferH.setPage(getPageChannel(), tmpPageNumber); + 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)) { - pageNumber = tmpPageNumber; + if(freeSpacePages.containsPageNumber(tmpPageNumber)) { + return dataPage; } - break; } } - if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) { - // No data pages exist (with free space). Create a new one. - return newDataPage(); + return null; } - } - - 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. */ @@ -2080,7 +2195,7 @@ public class TableImpl implements 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; diff --git a/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java b/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java index 4f4293d..6a80e04 100644 --- a/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java +++ b/src/java/com/healthmarketscience/jackcess/impl/UsageMap.java @@ -109,6 +109,21 @@ public class UsageMap /** * @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(DatabaseImpl 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 * @param pageNum Page number that this usage map is contained in * @param rowNum Number of the row on the page that contains this usage map * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on |