/** 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 */
/**
* Only used by unit tests
*/
- Column(boolean testing, Table table) {
+ Column(boolean testing, TableImpl table) {
if(!testing) {
throw new IllegalArgumentException();
}
* @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;
/**
* @usage _general_method_
*/
- public Table getTable() {
+ public TableImpl getTable() {
return _table;
}
{
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");
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);
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);
}
// 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
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;
}
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()) {
}
}
for (Column col : columns) {
- Table.writeName(buffer, col.getName(), creator.getCharset());
+ TableImpl.writeName(buffer, col.getName(), creator.getCharset());
}
}
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;
/** 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 */
/** 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();
* 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);
}
* @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);
* @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
{
* 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,
* @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
{
* 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
{
* @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
{
* 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
{
return _id;
}
- public Table getTable() {
+ public TableImpl getTable() {
return _table;
}
{
// 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();
}
_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()));
}
/** 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();
// 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
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.
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);
}
{
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);
}
* 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
{
*/
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
/** 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 */
}
@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) {
* @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) {
}
@Override
- public Table getTable(String name) throws IOException {
+ public TableImpl getTable(String name) throws IOException {
return getTable(name, false);
}
* @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;
}
* @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);
}
@Override
- public Table getSystemTable(String tableName) throws IOException
+ public TableImpl getSystemTable(String tableName) throws IOException
{
return getTable(tableName, true);
}
/**
* 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;
}
", 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);
}
* 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 {
* 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;
}
{
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();
}
@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);
}
}
* @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();
+ }
}
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;
private List<Joiner> _primaryJoinersDoDel;
private List<Joiner> _secondaryJoiners;
- FKEnforcer(Table table) {
+ FKEnforcer(TableImpl table) {
_table = table;
// at this point, only init the index columns
return _data;
}
- public Table getTable() {
+ public TableImpl getTable() {
return getIndexData().getTable();
}
return null;
}
- Table refTable = getTable().getDatabase().getTable(
+ TableImpl refTable = getTable().getDatabase().getTable(
_reference.getOtherTablePageNumber());
if(refTable == null) {
// 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
// write index names
for(IndexBuilder idx : creator.getIndexes()) {
- Table.writeName(buffer, idx.getName(), creator.getCharset());
+ TableImpl.writeName(buffer, idx.getName(), creator.getCharset());
}
}
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;
/** 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
{
* @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);
* {@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);
* 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,
/** 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 */
/** 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;
* 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
{
return new IndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset);
}
- public Table getTable() {
+ public TableImpl getTable() {
return _table;
}
_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;
/*
-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
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_
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}).
* 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
* 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
* 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
* 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();
}
_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);
}
}
- 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);
--- /dev/null
+/*
+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;
+ }
+ }
+
+}
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);
import com.healthmarketscience.jackcess.ByteUtil;
import com.healthmarketscience.jackcess.Column;
-import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableImpl;
/**
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);
}
- 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)
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;
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;
private IndexCursor _complexValIdCursor;
protected ComplexColumnInfo(Column column, int complexTypeId,
- Table typeObjTable, Table flatTable)
+ TableImpl typeObjTable, TableImpl flatTable)
throws IOException
{
_column = column;
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)) {
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(
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)
{
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.
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);
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();
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.
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);
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
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);
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();