public boolean matches(Table table, String columnName, Object value1,
Object value2)
{
- if(!isTextual(table.getColumn(columnName))) {
+ if(!table.getColumn(columnName).getType().isTextual()) {
// use simple equality
return SimpleColumnMatcher.INSTANCE.matches(table, columnName,
value1, value2);
}
}
- private static boolean isTextual(Column col)
- {
- DataType type = col.getType();
- return((type == DataType.TEXT) || (type == DataType.MEMO));
- }
-
}
/** mask for the unknown bit */
public static final byte UNKNOWN_FLAG_MASK = (byte)0x02;
+ /** the "general" text sort order */
+ public static final short GENERAL_SORT_ORDER = 1033;
+
/** pattern matching textual guid strings (allows for optional surrounding
'{' and '}') */
private static final Pattern GUID_PATTERN = Pattern.compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*");
private int _fixedDataOffset;
/** the index of the variable length data in the var len offset table */
private int _varLenTableIndex;
+ /** the collating sort order for a text field */
+ private short _textSortOrder = GENERAL_SORT_ORDER;
/** the auto number generator for this column (if autonumber column) */
private AutoNumberGenerator _autoNumberGenerator;
if (_type.getHasScalePrecision()) {
_precision = buffer.get(offset + getFormat().OFFSET_COLUMN_PRECISION);
_scale = buffer.get(offset + getFormat().OFFSET_COLUMN_SCALE);
+ } else if(_type.isTextual()) {
+ // co-located w/ precision/scale
+ _textSortOrder = buffer.getShort(
+ offset + getFormat().OFFSET_COLUMN_PRECISION);
+ if(_textSortOrder == 0) {
+ // probably a file we wrote, before handling sort order
+ _textSortOrder = GENERAL_SORT_ORDER;
+ }
}
byte flags = buffer.get(offset + getFormat().OFFSET_COLUMN_FLAGS);
_variableLength = ((flags & FIXED_LEN_FLAG_MASK) == 0);
_scale = newScale;
}
+ public short getTextSortOrder() {
+ return _textSortOrder;
+ }
+
+ public void setTextSortOrder(short newTextSortOrder) {
+ _textSortOrder = newTextSortOrder;
+ }
+
public void setLength(short length) {
_columnLength = length;
}
}
if(isCompressedUnicode()) {
- if((getType() != DataType.TEXT) &&
- (getType() != DataType.MEMO)) {
+ if(!getType().isTextual()) {
throw new IllegalArgumentException(
"Only textual columns allow unicode compression (text/memo)");
}
if(_autoNumber) {
rtn.append("\n\tLast AutoNumber: " + _autoNumberGenerator.getLast());
}
+ if(_type.isTextual()) {
+ rtn.append("\n\tText Sort order: " + _textSortOrder);
+ }
rtn.append("\n\n");
return rtn.toString();
}
if(col.getType().getHasScalePrecision()) {
buffer.put(col.getPrecision()); // numeric precision
buffer.put(col.getScale()); // numeric scale
+ } else if(col.getType().isTextual()) {
+ buffer.putShort(col.getTextSortOrder());
} else {
buffer.put((byte) 0x00); //unused
buffer.put((byte) 0x00); //unused
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
/** the last (exclusive) row id for this cursor */
private final Position _lastPos;
/** the previous row */
- private Position _prevPos;
+ protected Position _prevPos;
/** the current row */
- private Position _curPos;
+ protected Position _curPos;
/** ColumnMatcher to be used when matching column values */
- private ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE;
+ protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE;
protected Cursor(Id id, Table table, Position firstPos, Position lastPos) {
_id = id;
public static Cursor createIndexCursor(Table table, Index index)
throws IOException
{
- return createIndexCursor(table, index, null, null);
+ return IndexCursor.createCursor(table, index);
}
/**
Object[] startRow, Object[] endRow)
throws IOException
{
- return createIndexCursor(table, index, startRow, true, endRow, true);
+ return IndexCursor.createCursor(table, index, startRow, endRow);
}
/**
boolean endInclusive)
throws IOException
{
- if(table != index.getTable()) {
- throw new IllegalArgumentException(
- "Given index is not for given table: " + index + ", " + table);
- }
- if(!table.getFormat().INDEXES_SUPPORTED) {
- throw new IllegalArgumentException(
- "JetFormat " + table.getFormat() +
- " does not currently support index lookups");
- }
- return new IndexCursor(table, index,
- index.cursor(startRow, startInclusive,
- endRow, endInclusive));
+ return IndexCursor.createCursor(table, index, startRow, startInclusive,
+ endRow, endInclusive);
}
/**
return _table;
}
- public Index getIndex() {
- return null;
- }
-
public JetFormat getFormat() {
return getTable().getFormat();
}
*/
public void setColumnMatcher(ColumnMatcher columnMatcher) {
if(columnMatcher == null) {
- columnMatcher = SimpleColumnMatcher.INSTANCE;
+ columnMatcher = getDefaultColumnMatcher();
}
_columnMatcher = columnMatcher;
}
+ /**
+ * Returns the default ColumnMatcher for this Cursor.
+ */
+ protected ColumnMatcher getDefaultColumnMatcher() {
+ return SimpleColumnMatcher.INSTANCE;
+ }
+
/**
* Returns the current state of the cursor which can be restored at a future
* point in time by a call to {@link #restoreSavepoint}.
}
- /**
- * Indexed cursor.
- */
- private static final class IndexCursor extends Cursor
- {
- /** IndexDirHandler for forward traversal */
- private final IndexDirHandler _forwardDirHandler =
- new ForwardIndexDirHandler();
- /** IndexDirHandler for backward traversal */
- private final IndexDirHandler _reverseDirHandler =
- new ReverseIndexDirHandler();
- /** logical index which this cursor is using */
- private final Index _index;
- /** Cursor over the entries of the relvant index */
- private final IndexData.EntryCursor _entryCursor;
-
- private IndexCursor(Table table, Index index,
- IndexData.EntryCursor entryCursor)
- throws IOException
- {
- super(new Id(table, index), table,
- new IndexPosition(entryCursor.getFirstEntry()),
- new IndexPosition(entryCursor.getLastEntry()));
- _index = index;
- _index.initialize();
- _entryCursor = entryCursor;
- }
-
- @Override
- public Index getIndex() {
- return _index;
- }
-
- @Override
- protected IndexDirHandler getDirHandler(boolean moveForward) {
- return (moveForward ? _forwardDirHandler : _reverseDirHandler);
- }
-
- @Override
- protected boolean isUpToDate() {
- return(super.isUpToDate() && _entryCursor.isUpToDate());
- }
-
- @Override
- protected void reset(boolean moveForward) {
- _entryCursor.reset(moveForward);
- super.reset(moveForward);
- }
-
- @Override
- protected void restorePositionImpl(Position curPos, Position prevPos)
- throws IOException
- {
- if(!(curPos instanceof IndexPosition) ||
- !(prevPos instanceof IndexPosition)) {
- throw new IllegalArgumentException(
- "Restored positions must be index positions");
- }
- _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(),
- ((IndexPosition)prevPos).getEntry());
- super.restorePositionImpl(curPos, prevPos);
- }
-
- @Override
- protected boolean findRowImpl(Column columnPattern, Object valuePattern)
- throws IOException
- {
- Object[] rowValues = _entryCursor.getIndexData().constructIndexRow(
- columnPattern.getName(), valuePattern);
-
- if(rowValues == null) {
- // bummer, use the default table scan
- return super.findRowImpl(columnPattern, valuePattern);
- }
-
- // sweet, we can use our index
- _entryCursor.beforeEntry(rowValues);
- IndexData.Entry startEntry = _entryCursor.getNextEntry();
- if(!startEntry.getRowId().isValid()) {
- // at end of index, no potential matches
- return false;
- }
-
- // either we found a row with the given value, or none exist in the
- // table
- restorePosition(new IndexPosition(startEntry));
- return currentRowMatches(columnPattern, valuePattern);
- }
-
- @Override
- protected boolean findRowImpl(Map<String,Object> rowPattern)
- throws IOException
- {
- IndexData indexData = _entryCursor.getIndexData();
- Object[] rowValues = indexData.constructIndexRow(rowPattern);
-
- if(rowValues == null) {
- // bummer, use the default table scan
- return super.findRowImpl(rowPattern);
- }
-
- // sweet, we can use our index
- _entryCursor.beforeEntry(rowValues);
- IndexData.Entry startEntry = _entryCursor.getNextEntry();
- if(!startEntry.getRowId().isValid()) {
- // at end of index, no potential matches
- return false;
- }
- restorePosition(new IndexPosition(startEntry));
-
- Map<String,Object> indexRowPattern = null;
- if(rowPattern.size() == indexData.getColumns().size()) {
- // the rowPattern matches our index columns exactly, so we can
- // streamline our testing below
- indexRowPattern = rowPattern;
- } else {
- // the rowPattern has more columns than just the index, so we need to
- // do more work when testing below
- indexRowPattern =
- new LinkedHashMap<String,Object>();
- for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) {
- indexRowPattern.put(idxCol.getName(),
- rowValues[idxCol.getColumnIndex()]);
- }
- }
-
- // there may be multiple columns which fit the pattern subset used by
- // the index, so we need to keep checking until our index values no
- // longer match
- do {
-
- if(!currentRowMatches(indexRowPattern)) {
- // there are no more rows which could possibly match
- break;
- }
-
- // note, if rowPattern == indexRowPattern, no need to do an extra
- // comparison with the current row
- if((rowPattern == indexRowPattern) || currentRowMatches(rowPattern)) {
- // found it!
- return true;
- }
-
- } while(moveToNextRow());
-
- // none of the potential rows matched
- return false;
- }
-
- @Override
- protected Position findAnotherPosition(RowState rowState, Position curPos,
- boolean moveForward)
- throws IOException
- {
- IndexDirHandler handler = getDirHandler(moveForward);
- IndexPosition endPos = (IndexPosition)handler.getEndPosition();
- IndexData.Entry entry = handler.getAnotherEntry();
- return ((!entry.equals(endPos.getEntry())) ?
- new IndexPosition(entry) : endPos);
- }
-
- /**
- * Handles moving the table index cursor in a given direction. Separates
- * cursor logic from value storage.
- */
- private abstract class IndexDirHandler extends DirHandler {
- public abstract IndexData.Entry getAnotherEntry()
- throws IOException;
- }
-
- /**
- * Handles moving the table index cursor forward.
- */
- private final class ForwardIndexDirHandler extends IndexDirHandler {
- @Override
- public Position getBeginningPosition() {
- return getFirstPosition();
- }
- @Override
- public Position getEndPosition() {
- return getLastPosition();
- }
- @Override
- public IndexData.Entry getAnotherEntry() throws IOException {
- return _entryCursor.getNextEntry();
- }
- }
-
- /**
- * Handles moving the table index cursor backward.
- */
- private final class ReverseIndexDirHandler extends IndexDirHandler {
- @Override
- public Position getBeginningPosition() {
- return getLastPosition();
- }
- @Override
- public Position getEndPosition() {
- return getFirstPosition();
- }
- @Override
- public IndexData.Entry getAnotherEntry() throws IOException {
- return _entryCursor.getPreviousEntry();
- }
- }
-
- }
/**
* Identifier for a cursor. Will be equal to any other cursor of the same
private final String _tableName;
private final String _indexName;
- private Id(Table table, Index index) {
+ protected Id(Table table, Index index) {
_tableName = table.getName();
_indexName = ((index != null) ? index.getName() : null);
}
}
}
- /**
- * Value object which maintains the current position of an IndexCursor.
- */
- private static final class IndexPosition extends Position
- {
- private final IndexData.Entry _entry;
-
- private IndexPosition(IndexData.Entry entry) {
- _entry = entry;
- }
-
- @Override
- public RowId getRowId() {
- return getEntry().getRowId();
- }
-
- public IndexData.Entry getEntry() {
- return _entry;
- }
-
- @Override
- protected boolean equalsImpl(Object o) {
- return getEntry().equals(((IndexPosition)o).getEntry());
- }
-
- @Override
- public String toString() {
- return "Entry = " + getEntry();
- }
- }
-
}
package com.healthmarketscience.jackcess;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
-import org.apache.commons.lang.ObjectUtils;
/**
* Builder style class for constructing a Cursor. By default, a cursor is
* Sets an index to use for the cursor by searching the table for an index
* with exactly the given columns.
* @throws IllegalArgumentException if no index can be found on the table
- * with the given name
+ * with the given columns
+ */
+ public CursorBuilder setIndexByColumnNames(String... columnNames) {
+ return setIndexByColumns(Arrays.asList(columnNames));
+ }
+
+ /**
+ * Sets an index to use for the cursor by searching the table for an index
+ * with exactly the given columns.
+ * @throws IllegalArgumentException if no index can be found on the table
+ * with the given columns
*/
public CursorBuilder setIndexByColumns(Column... columns) {
- List<Column> searchColumns = Arrays.asList(columns);
+ List<String> colNames = new ArrayList<String>();
+ for(Column col : columns) {
+ colNames.add(col.getName());
+ }
+ return setIndexByColumns(colNames);
+ }
+
+ /**
+ * Searches for an index with the given column names.
+ */
+ private CursorBuilder setIndexByColumns(List<String> searchColumns) {
boolean found = false;
for(Index index : _table.getIndexes()) {
if(indexColumns.size() != searchColumns.size()) {
continue;
}
- Iterator<Column> sIter = searchColumns.iterator();
+ Iterator<String> sIter = searchColumns.iterator();
Iterator<IndexData.ColumnDescriptor> iIter = indexColumns.iterator();
boolean matches = true;
while(sIter.hasNext()) {
- Column sCol = sIter.next();
- IndexData.ColumnDescriptor iCol = iIter.next();
- if(!ObjectUtils.equals(sCol.getName(), iCol.getName())) {
+ String sColName = sIter.next();
+ String iColName = iIter.next().getName();
+ if((sColName != iColName) &&
+ ((sColName == null) || !sColName.equalsIgnoreCase(iColName))) {
matches = false;
break;
}
return cursor;
}
+ /**
+ * Returns a new index cursor for the table, constructed to the given
+ * specifications.
+ */
+ public IndexCursor toIndexCursor()
+ throws IOException
+ {
+ return (IndexCursor)toCursor();
+ }
+
}
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
/** System catalog column name of the properties column */
private static final String CAT_COL_PROPS = "LvProp";
+ /** top-level parentid for a database */
+ private static final int DB_PARENT_ID = 0xF000000;
+
/** the maximum size of any of the included "empty db" resources */
- private static final long MAX_EMPTYDB_SIZE = 320000L;
+ private static final long MAX_EMPTYDB_SIZE = 330000L;
+
+ /** this object is a "system" object */
+ static final int SYSTEM_OBJECT_FLAG = 0x80000000;
+ /** this object is another type of "system" object */
+ static final int ALT_SYSTEM_OBJECT_FLAG = 0x02;
+ /** this object is hidden */
+ static final int HIDDEN_OBJECT_FLAG = 0x08;
+ /** all flags which seem to indicate some type of system object */
+ static final int SYSTEM_OBJECT_FLAGS =
+ SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG;
public static enum FileFormat {
/** Prefix for column or table names that are reserved words */
private static final String ESCAPE_PREFIX = "x";
- /** Prefix that flags system tables */
- private static final String PREFIX_SYSTEM = "MSys";
/** Name of the system object that is the parent of all tables */
private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables";
/** Name of the table that contains system access control entries */
/** System object type for query definitions */
private static final Short TYPE_QUERY = (short) 5;
- /** the columns to read when reading system catalog initially */
+ /** max number of table lookups to cache */
+ private static final int MAX_CACHED_LOOKUP_TABLES = 50;
+
+ /** the columns to read when reading system catalog normally */
private static Collection<String> SYSTEM_CATALOG_COLUMNS =
- new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID));
-
- /** the columns to read when finding queries */
- private static Collection<String> SYSTEM_CATALOG_QUERY_COLUMNS =
- new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
+ new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
CAT_COL_FLAGS));
+ /** the columns to read when finding table names */
+ private static Collection<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS =
+ new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
+ CAT_COL_FLAGS, CAT_COL_PARENT_ID));
/**
/** Format that the containing database is in */
private final JetFormat _format;
/**
- * Map of UPPERCASE table names to page numbers containing their definition
- * and their stored table name.
+ * Cache map of UPPERCASE table names to page numbers containing their
+ * definition and their stored table name (max size
+ * MAX_CACHED_LOOKUP_TABLES).
*/
- private Map<String, TableInfo> _tableLookup =
- new HashMap<String, TableInfo>();
+ private final Map<String, TableInfo> _tableLookup =
+ new LinkedHashMap<String, TableInfo>() {
+ private static final long serialVersionUID = 0L;
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<String, TableInfo> e) {
+ return(size() > MAX_CACHED_LOOKUP_TABLES);
+ }
+ };
/** set of table names as stored in the mdb file, created on demand */
private Set<String> _tableNames;
/** Reads and writes database pages */
private final PageChannel _pageChannel;
/** System catalog table */
private Table _systemCatalog;
+ /** utility table finder */
+ private TableFinder _tableFinder;
/** System access control entries table */
private Table _accessControlEntries;
/** System relationships table (initialized on first use) */
private TimeZone _timeZone;
/** the ordering used for table columns */
private Table.ColumnOrder _columnOrder;
+ /** cache of in-use tables */
+ private final TableCache _tableCache = new TableCache();
/**
* Open an existing Database. If the existing file is not writeable, the
*/
private void readSystemCatalog() throws IOException {
_systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
- defaultUseBigIndex());
- for(Map<String,Object> row :
- Cursor.createCursor(_systemCatalog).iterable(
- SYSTEM_CATALOG_COLUMNS))
- {
- String name = (String) row.get(CAT_COL_NAME);
- if (name != null && TYPE_TABLE.equals(row.get(CAT_COL_TYPE))) {
- if (!name.startsWith(PREFIX_SYSTEM)) {
- addTable((String) row.get(CAT_COL_NAME), (Integer) row.get(CAT_COL_ID));
- } else if(TABLE_SYSTEM_ACES.equals(name)) {
- int pageNumber = (Integer)row.get(CAT_COL_ID);
- _accessControlEntries = readTable(TABLE_SYSTEM_ACES, pageNumber,
- defaultUseBigIndex());
- }
- } else if (SYSTEM_OBJECT_NAME_TABLES.equals(name)) {
- _tableParentId = (Integer) row.get(CAT_COL_ID);
- }
- }
+ SYSTEM_OBJECT_FLAGS, defaultUseBigIndex());
- // check for required system values
- if(_accessControlEntries == null) {
- throw new IOException("Did not find required " + TABLE_SYSTEM_ACES +
- " table");
+ try {
+ _tableFinder = new DefaultTableFinder(
+ new CursorBuilder(_systemCatalog)
+ .setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME)
+ .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
+ .toIndexCursor());
+ } catch(IllegalArgumentException e) {
+ LOG.info("Could not find expected index on table " +
+ _systemCatalog.getName());
+ // use table scan instead
+ _tableFinder = new FallbackTableFinder(
+ new CursorBuilder(_systemCatalog)
+ .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
+ .toCursor());
}
- if(_tableParentId == null) {
+
+ _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID,
+ SYSTEM_OBJECT_NAME_TABLES);
+
+ if(_tableParentId == null) {
throw new IOException("Did not find required parent table id");
}
+
+ _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES);
if (LOG.isDebugEnabled()) {
LOG.debug("Finished reading system catalog. Tables: " +
/**
* @return The names of all of the user tables (String)
*/
- public Set<String> getTableNames() {
+ public Set<String> getTableNames() throws IOException {
if(_tableNames == null) {
- _tableNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
- for(TableInfo tableInfo : _tableLookup.values()) {
- _tableNames.add(tableInfo.tableName);
- }
+ Set<String> tableNames =
+ new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+ _tableFinder.getTableNames(tableNames);
+ _tableNames = tableNames;
}
return _tableNames;
}
* @return The table, or null if it doesn't exist
*/
public Table getTable(String name, boolean useBigIndex) throws IOException {
+ return getTable(name, false, useBigIndex);
+ }
+ /**
+ * @param name Table name
+ * @param includeSystemTables whether to consider returning a system table
+ * @param useBigIndex whether or not "big index support" should be enabled
+ * for the table (this value will override any other
+ * settings)
+ * @return The table, or null if it doesn't exist
+ */
+ private Table getTable(String name, boolean includeSystemTables,
+ boolean useBigIndex)
+ throws IOException
+ {
TableInfo tableInfo = lookupTable(name);
if ((tableInfo == null) || (tableInfo.pageNumber == null)) {
return null;
}
+ if(!includeSystemTables && isSystemObject(tableInfo.flags)) {
+ return null;
+ }
- return readTable(tableInfo.tableName, tableInfo.pageNumber, useBigIndex);
+ return readTable(tableInfo.tableName, tableInfo.pageNumber,
+ tableInfo.flags, useBigIndex);
}
/**
Map<Integer,List<Query.Row>> queryRowMap =
new HashMap<Integer,List<Query.Row>>();
for(Map<String,Object> row :
- Cursor.createCursor(_systemCatalog).iterable(
- SYSTEM_CATALOG_QUERY_COLUMNS))
+ Cursor.createCursor(_systemCatalog).iterable(SYSTEM_CATALOG_COLUMNS))
{
String name = (String) row.get(CAT_COL_NAME);
if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) {
public Table getSystemTable(String tableName)
throws IOException
{
- for(Map<String,Object> row :
- Cursor.createCursor(_systemCatalog).iterable(
- SYSTEM_CATALOG_COLUMNS))
- {
- String name = (String) row.get(CAT_COL_NAME);
- if (tableName.equalsIgnoreCase(name) &&
- TYPE_TABLE.equals(row.get(CAT_COL_TYPE))) {
- Integer pageNumber = (Integer) row.get(CAT_COL_ID);
- if(pageNumber != null) {
- return readTable(name, pageNumber, defaultUseBigIndex());
- }
- }
- }
- return null;
+ return getTable(tableName, true, defaultUseBigIndex());
}
/**
/**
* Reads a table with the given name from the given pageNumber.
*/
- private Table readTable(String name, int pageNumber, boolean useBigIndex)
+ private Table readTable(String name, int pageNumber, int flags,
+ boolean useBigIndex)
throws IOException
{
+ // first, check for existing table
+ Table table = _tableCache.get(pageNumber);
+ if(table != null) {
+ return table;
+ }
+
+ // need to load table from db
_pageChannel.readPage(_buffer, pageNumber);
byte pageType = _buffer.get(0);
if (pageType != PageTypes.TABLE_DEF) {
throw new IOException("Looking for " + name + " at page " + pageNumber +
", but page type is " + pageType);
}
- return new Table(this, _buffer, pageNumber, name, useBigIndex);
+ return _tableCache.put(
+ new Table(this, _buffer, pageNumber, name, flags, useBigIndex));
}
/**
private void addTable(String tableName, Integer pageNumber)
{
_tableLookup.put(toLookupTableName(tableName),
- new TableInfo(pageNumber, tableName));
+ new TableInfo(pageNumber, tableName, 0));
// clear this, will be created next time needed
_tableNames = null;
}
/**
* @return the tableInfo of the given table, if any
*/
- private TableInfo lookupTable(String tableName) {
- return _tableLookup.get(toLookupTableName(tableName));
+ private TableInfo lookupTable(String tableName) throws IOException {
+
+ String lookupTableName = toLookupTableName(tableName);
+ TableInfo tableInfo = _tableLookup.get(lookupTableName);
+ if(tableInfo != null) {
+ return tableInfo;
+ }
+
+ tableInfo = _tableFinder.lookupTable(tableName);
+
+ if(tableInfo != null) {
+ // cache for later
+ _tableLookup.put(lookupTableName, tableInfo);
+ }
+
+ return tableInfo;
}
/**
return ((tableName != null) ? tableName.toUpperCase() : null);
}
+ /**
+ * @return {@code true} if the given flags indicate that an object is some
+ * sort of system object, {@code false} otherwise.
+ */
+ private static boolean isSystemObject(int flags) {
+ return ((flags & SYSTEM_OBJECT_FLAGS) != 0);
+ }
+
/**
* Returns {@code false} if "big index support" has been disabled explicity
* on the this Database or via a system property, {@code true} otherwise.
{
public final Integer pageNumber;
public final String tableName;
+ public final int flags;
private TableInfo(Integer newPageNumber,
- String newTableName) {
+ String newTableName,
+ int newFlags) {
pageNumber = newPageNumber;
tableName = newTableName;
+ flags = newFlags;
}
}
private Iterator<String> _tableNameIter;
private TableIterator() {
- _tableNameIter = getTableNames().iterator();
+ try {
+ _tableNameIter = getTableNames().iterator();
+ } catch(IOException e) {
+ throw new IllegalStateException(e);
+ }
}
public boolean hasNext() {
}
}
}
+
+ /**
+ * Utility class for handling table lookups.
+ */
+ private abstract class TableFinder
+ {
+ public abstract Integer findObjectId(Integer parentId, String name)
+ throws IOException;
+
+ public abstract TableInfo lookupTable(String tableName)
+ throws IOException;
+
+ public abstract void getTableNames(Set<String> tableNames)
+ throws IOException;
+ }
+
+ /**
+ * Normal table lookup handler, using catalog table index.
+ */
+ private final class DefaultTableFinder extends TableFinder
+ {
+ private final IndexCursor _systemCatalogCursor;
+
+ private DefaultTableFinder(IndexCursor systemCatalogCursor) {
+ _systemCatalogCursor = systemCatalogCursor;
+ }
+
+ @Override
+ public Integer findObjectId(Integer parentId, String name)
+ throws IOException
+ {
+ if(!_systemCatalogCursor.findRowByEntry(parentId, name)) {
+ return null;
+ }
+ Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
+ return (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
+ }
+
+ @Override
+ public TableInfo lookupTable(String tableName) throws IOException {
+
+ if(!_systemCatalogCursor.findRowByEntry(_tableParentId, tableName)) {
+ return null;
+ }
+
+ Map<String,Object> row = _systemCatalogCursor.getCurrentRow(
+ SYSTEM_CATALOG_COLUMNS);
+ Integer pageNumber = (Integer)row.get(CAT_COL_ID);
+ String realName = (String)row.get(CAT_COL_NAME);
+ int flags = (Integer)row.get(CAT_COL_FLAGS);
+ Short type = (Short)row.get(CAT_COL_TYPE);
+
+ if(!TYPE_TABLE.equals(type)) {
+ return null;
+ }
+
+ return new TableInfo(pageNumber, realName, flags);
+ }
+
+ @Override
+ public void getTableNames(Set<String> tableNames) throws IOException {
+
+ IndexCursor tNameCursor = new CursorBuilder(_systemCatalog)
+ .setIndex(_systemCatalogCursor.getIndex())
+ .setStartEntry(_tableParentId, IndexData.MIN_VALUE)
+ .setEndEntry(_tableParentId, IndexData.MAX_VALUE)
+ .toIndexCursor();
+
+ for(Map<String,Object> row : tNameCursor.iterable(
+ SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) {
+
+ String tableName = (String)row.get(CAT_COL_NAME);
+ int flags = (Integer)row.get(CAT_COL_FLAGS);
+ Short type = (Short)row.get(CAT_COL_TYPE);
+
+ if(TYPE_TABLE.equals(type) && !isSystemObject(flags)) {
+ tableNames.add(tableName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Fallback table lookup handler, using catalog table scans.
+ */
+ private final class FallbackTableFinder extends TableFinder
+ {
+ private final Cursor _systemCatalogCursor;
+
+ private FallbackTableFinder(Cursor systemCatalogCursor) {
+ _systemCatalogCursor = systemCatalogCursor;
+ }
+
+ @Override
+ public Integer findObjectId(Integer parentId, String name)
+ throws IOException
+ {
+ Map<String,Object> rowPat = new HashMap<String,Object>();
+ rowPat.put(CAT_COL_PARENT_ID, parentId);
+ rowPat.put(CAT_COL_NAME, name);
+ if(!_systemCatalogCursor.findRow(rowPat)) {
+ return null;
+ }
+
+ Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
+ return (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
+ }
+
+ @Override
+ public TableInfo lookupTable(String tableName) throws IOException {
+
+ for(Map<String,Object> row : _systemCatalogCursor.iterable(
+ SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) {
+
+ Short type = (Short)row.get(CAT_COL_TYPE);
+ if(!TYPE_TABLE.equals(type)) {
+ continue;
+ }
+
+ int parentId = (Integer)row.get(CAT_COL_PARENT_ID);
+ if(parentId != _tableParentId) {
+ continue;
+ }
+
+ String realName = (String)row.get(CAT_COL_NAME);
+ if(!tableName.equalsIgnoreCase(realName)) {
+ continue;
+ }
+
+ Integer pageNumber = (Integer)row.get(CAT_COL_ID);
+ int flags = (Integer)row.get(CAT_COL_FLAGS);
+ return new TableInfo(pageNumber, realName, flags);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void getTableNames(Set<String> tableNames) throws IOException {
+
+ for(Map<String,Object> row : _systemCatalogCursor.iterable(
+ SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) {
+
+ String tableName = (String)row.get(CAT_COL_NAME);
+ int flags = (Integer)row.get(CAT_COL_FLAGS);
+ Short type = (Short)row.get(CAT_COL_TYPE);
+ int parentId = (Integer)row.get(CAT_COL_PARENT_ID);
+
+ if(parentId != _tableParentId) {
+ // no more tables
+ continue;
+ }
+
+ if(TYPE_TABLE.equals(type) && !isSystemObject(flags)) {
+ tableNames.add(tableName);
+ }
+ }
+ }
+ }
+
+ /**
+ * WeakReference for a Table which holds the table pageNumber (for later
+ * cache purging).
+ */
+ private static final class WeakTableReference extends WeakReference<Table>
+ {
+ private final Integer _pageNumber;
+
+ private WeakTableReference(Integer pageNumber, Table table,
+ ReferenceQueue<Table> queue) {
+ super(table, queue);
+ _pageNumber = pageNumber;
+ }
+
+ public Integer getPageNumber() {
+ return _pageNumber;
+ }
+ }
+
+ /**
+ * Cache of currently in-use tables, allows re-use of existing tables.
+ */
+ private static final class TableCache
+ {
+ private final Map<Integer,WeakTableReference> _tables =
+ new HashMap<Integer,WeakTableReference>();
+ private final ReferenceQueue<Table> _queue = new ReferenceQueue<Table>();
+
+ public Table get(Integer pageNumber) {
+ WeakTableReference ref = _tables.get(pageNumber);
+ return ((ref != null) ? ref.get() : null);
+ }
+
+ public Table put(Table table) {
+ purgeOldRefs();
+
+ Integer pageNumber = table.getTableDefPageNumber();
+ WeakTableReference ref = new WeakTableReference(
+ pageNumber, table, _queue);
+ _tables.put(pageNumber, ref);
+
+ return table;
+ }
+
+ private void purgeOldRefs() {
+ WeakTableReference oldRef = null;
+ while((oldRef = (WeakTableReference)_queue.poll()) != null) {
+ _tables.remove(oldRef.getPageNumber());
+ }
+ }
+ }
}
--- /dev/null
+/*
+Copyright (c) 2011 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 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
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.Table.RowState;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Cursor backed by an index with extended traversal options.
+ *
+ * @author James Ahlborn
+ */
+public class IndexCursor extends Cursor
+{
+ private static final Log LOG = LogFactory.getLog(IndexCursor.class);
+
+ /** IndexDirHandler for forward traversal */
+ private final IndexDirHandler _forwardDirHandler =
+ new ForwardIndexDirHandler();
+ /** IndexDirHandler for backward traversal */
+ private final IndexDirHandler _reverseDirHandler =
+ new ReverseIndexDirHandler();
+ /** logical index which this cursor is using */
+ private final Index _index;
+ /** Cursor over the entries of the relvant index */
+ private final IndexData.EntryCursor _entryCursor;
+ /** column names for the index entry columns */
+ private Set<String> _indexEntryPattern;
+
+ private IndexCursor(Table table, Index index,
+ IndexData.EntryCursor entryCursor)
+ throws IOException
+ {
+ super(new Id(table, index), table,
+ new IndexPosition(entryCursor.getFirstEntry()),
+ new IndexPosition(entryCursor.getLastEntry()));
+ _index = index;
+ _index.initialize();
+ _entryCursor = entryCursor;
+ }
+
+ /**
+ * Creates an indexed cursor for the given table.
+ * <p>
+ * Note, index based table traversal may not include all rows, as certain
+ * types of indexes do not include all entries (namely, some indexes ignore
+ * null entries, see {@link Index#shouldIgnoreNulls}).
+ *
+ * @param table the table over which this cursor will traverse
+ * @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)
+ throws IOException
+ {
+ return createCursor(table, index, null, null);
+ }
+
+ /**
+ * Creates an indexed cursor for the given table, narrowed to the given
+ * range.
+ * <p>
+ * Note, index based table traversal may not include all rows, as certain
+ * types of indexes do not include all entries (namely, some indexes ignore
+ * null entries, see {@link Index#shouldIgnoreNulls}).
+ *
+ * @param table the table over which this cursor will traverse
+ * @param index index for the table which will define traversal order as
+ * well as enhance certain lookups
+ * @param startRow the first row of data for the cursor (inclusive), or
+ * {@code null} for the first entry
+ * @param endRow the last row of data for the cursor (inclusive), or
+ * {@code null} for the last entry
+ */
+ public static IndexCursor createCursor(
+ Table table, Index index, Object[] startRow, Object[] endRow)
+ throws IOException
+ {
+ return createCursor(table, index, startRow, true, endRow, true);
+ }
+
+ /**
+ * Creates an indexed cursor for the given table, narrowed to the given
+ * range.
+ * <p>
+ * Note, index based table traversal may not include all rows, as certain
+ * types of indexes do not include all entries (namely, some indexes ignore
+ * null entries, see {@link Index#shouldIgnoreNulls}).
+ *
+ * @param table the table over which this cursor will traverse
+ * @param index index for the table which will define traversal order as
+ * well as enhance certain lookups
+ * @param startRow the first row of data for the cursor, or {@code null} for
+ * the first entry
+ * @param startInclusive whether or not startRow is inclusive or exclusive
+ * @param endRow the last row of data for the cursor, or {@code null} for
+ * the last entry
+ * @param endInclusive whether or not endRow is inclusive or exclusive
+ */
+ public static IndexCursor createCursor(Table table, Index index,
+ Object[] startRow,
+ boolean startInclusive,
+ Object[] endRow,
+ boolean endInclusive)
+ throws IOException
+ {
+ if(table != index.getTable()) {
+ throw new IllegalArgumentException(
+ "Given index is not for given table: " + index + ", " + table);
+ }
+ if(!table.getFormat().INDEXES_SUPPORTED) {
+ throw new IllegalArgumentException(
+ "JetFormat " + table.getFormat() +
+ " does not currently support index lookups");
+ }
+ IndexCursor cursor = new IndexCursor(table, index,
+ index.cursor(startRow, startInclusive,
+ endRow, endInclusive));
+ // init the column matcher appropriately for the index type
+ cursor.setColumnMatcher(null);
+ return cursor;
+ }
+
+ public Index getIndex() {
+ return _index;
+ }
+
+ /**
+ * Moves to the first row (as defined by the cursor) where the index entries
+ * match the given values (only valid for index-backed cursors). If a match
+ * is not found (or an exception is thrown), the cursor is restored to its
+ * previous state.
+ *
+ * @param entryValues the column values for the index's columns.
+ * @return {@code true} if a valid row was found with the given values,
+ * {@code false} if no row was found
+ */
+ public boolean findRowByEntry(Object... entryValues)
+ throws IOException
+ {
+ Position curPos = _curPos;
+ Position prevPos = _prevPos;
+ boolean found = false;
+ try {
+ found = findRowByEntryImpl(entryValues, true);
+ return found;
+ } finally {
+ if(!found) {
+ try {
+ restorePosition(curPos, prevPos);
+ } catch(IOException e) {
+ LOG.error("Failed restoring position", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Moves to the first row (as defined by the cursor) where the index entries
+ * are >= the given values (only valid for index-backed cursors). If a an
+ * exception is thrown, the cursor is restored to its previous state.
+ *
+ * @param entryValues the column values for the index's columns.
+ */
+ public void findClosestRowByEntry(Object... entryValues)
+ throws IOException
+ {
+ Position curPos = _curPos;
+ Position prevPos = _prevPos;
+ boolean found = false;
+ try {
+ findRowByEntryImpl(entryValues, false);
+ found = true;
+ } finally {
+ if(!found) {
+ try {
+ restorePosition(curPos, prevPos);
+ } catch(IOException e) {
+ LOG.error("Failed restoring position", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected IndexDirHandler getDirHandler(boolean moveForward) {
+ return (moveForward ? _forwardDirHandler : _reverseDirHandler);
+ }
+
+ @Override
+ protected boolean isUpToDate() {
+ return(super.isUpToDate() && _entryCursor.isUpToDate());
+ }
+
+ @Override
+ protected void reset(boolean moveForward) {
+ _entryCursor.reset(moveForward);
+ super.reset(moveForward);
+ }
+
+ @Override
+ protected void restorePositionImpl(Position curPos, Position prevPos)
+ throws IOException
+ {
+ if(!(curPos instanceof IndexPosition) ||
+ !(prevPos instanceof IndexPosition)) {
+ throw new IllegalArgumentException(
+ "Restored positions must be index positions");
+ }
+ _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(),
+ ((IndexPosition)prevPos).getEntry());
+ super.restorePositionImpl(curPos, prevPos);
+ }
+
+ @Override
+ protected boolean findRowImpl(Column columnPattern, Object valuePattern)
+ throws IOException
+ {
+ Object[] rowValues = _entryCursor.getIndexData().constructIndexRow(
+ columnPattern.getName(), valuePattern);
+
+ if(rowValues == null) {
+ // bummer, use the default table scan
+ return super.findRowImpl(columnPattern, valuePattern);
+ }
+
+ // sweet, we can use our index
+ if(!findPotentialRow(rowValues, true)) {
+ return false;
+ }
+
+ // either we found a row with the given value, or none exist in the
+ // table
+ return currentRowMatches(columnPattern, valuePattern);
+ }
+
+ /**
+ * Moves to the first row (as defined by the cursor) where the index entries
+ * match the given values. Caller manages save/restore on failure.
+ *
+ * @param entryValues the column values for the index's columns.
+ * @param requireMatch whether or not an exact match is found
+ * @return {@code true} if a valid row was found with the given values,
+ * {@code false} if no row was found
+ */
+ protected boolean findRowByEntryImpl(Object[] entryValues,
+ boolean requireMatch)
+ throws IOException
+ {
+ Object[] rowValues = _entryCursor.getIndexData()
+ .constructIndexRowFromEntry(entryValues);
+
+ // sweet, we can use our index
+ if(!findPotentialRow(rowValues, requireMatch)) {
+ return false;
+ } else if(!requireMatch) {
+ // nothing more to do, we have moved to the closest row
+ return true;
+ }
+
+ if(_indexEntryPattern == null) {
+ // init our set of index column names
+ _indexEntryPattern = new HashSet<String>();
+ for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
+ _indexEntryPattern.add(col.getName());
+ }
+ }
+
+ // check the next row to see if it actually matches
+ Map<String,Object> row = getCurrentRow(_indexEntryPattern);
+
+ for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
+ String columnName = col.getName();
+ Object patValue = rowValues[col.getColumnIndex()];
+ Object rowValue = row.get(columnName);
+ if(!_columnMatcher.matches(getTable(), columnName,
+ patValue, rowValue)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected boolean findRowImpl(Map<String,Object> rowPattern)
+ throws IOException
+ {
+ IndexData indexData = _entryCursor.getIndexData();
+ Object[] rowValues = indexData.constructIndexRow(rowPattern);
+
+ if(rowValues == null) {
+ // bummer, use the default table scan
+ return super.findRowImpl(rowPattern);
+ }
+
+ // sweet, we can use our index
+ if(!findPotentialRow(rowValues, true)) {
+ // at end of index, no potential matches
+ return false;
+ }
+
+ // find actual matching row
+ Map<String,Object> indexRowPattern = null;
+ if(rowPattern.size() == indexData.getColumns().size()) {
+ // the rowPattern matches our index columns exactly, so we can
+ // streamline our testing below
+ indexRowPattern = rowPattern;
+ } else {
+ // the rowPattern has more columns than just the index, so we need to
+ // do more work when testing below
+ indexRowPattern =
+ new LinkedHashMap<String,Object>();
+ for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) {
+ indexRowPattern.put(idxCol.getName(),
+ rowValues[idxCol.getColumnIndex()]);
+ }
+ }
+
+ // there may be multiple columns which fit the pattern subset used by
+ // the index, so we need to keep checking until our index values no
+ // longer match
+ do {
+
+ if(!currentRowMatches(indexRowPattern)) {
+ // there are no more rows which could possibly match
+ break;
+ }
+
+ // note, if rowPattern == indexRowPattern, no need to do an extra
+ // comparison with the current row
+ if((rowPattern == indexRowPattern) || currentRowMatches(rowPattern)) {
+ // found it!
+ return true;
+ }
+
+ } while(moveToNextRow());
+
+ // none of the potential rows matched
+ return false;
+ }
+
+ private boolean findPotentialRow(Object[] rowValues, boolean requireMatch)
+ throws IOException
+ {
+ _entryCursor.beforeEntry(rowValues);
+ IndexData.Entry startEntry = _entryCursor.getNextEntry();
+ if(requireMatch && !startEntry.getRowId().isValid()) {
+ // at end of index, no potential matches
+ return false;
+ }
+ // move to position and check it out
+ restorePosition(new IndexPosition(startEntry));
+ return true;
+ }
+
+ @Override
+ protected Position findAnotherPosition(RowState rowState, Position curPos,
+ boolean moveForward)
+ throws IOException
+ {
+ IndexDirHandler handler = getDirHandler(moveForward);
+ IndexPosition endPos = (IndexPosition)handler.getEndPosition();
+ IndexData.Entry entry = handler.getAnotherEntry();
+ return ((!entry.equals(endPos.getEntry())) ?
+ new IndexPosition(entry) : endPos);
+ }
+
+ @Override
+ protected ColumnMatcher getDefaultColumnMatcher() {
+ if(getIndex().isUnique()) {
+ // text indexes are case-insensitive, therefore we should always use a
+ // case-insensitive matcher for unique indexes.
+ return CaseInsensitiveColumnMatcher.INSTANCE;
+ }
+ return SimpleColumnMatcher.INSTANCE;
+ }
+
+ /**
+ * Handles moving the table index cursor in a given direction. Separates
+ * cursor logic from value storage.
+ */
+ private abstract class IndexDirHandler extends DirHandler {
+ public abstract IndexData.Entry getAnotherEntry()
+ throws IOException;
+ }
+
+ /**
+ * Handles moving the table index cursor forward.
+ */
+ private final class ForwardIndexDirHandler extends IndexDirHandler {
+ @Override
+ public Position getBeginningPosition() {
+ return getFirstPosition();
+ }
+ @Override
+ public Position getEndPosition() {
+ return getLastPosition();
+ }
+ @Override
+ public IndexData.Entry getAnotherEntry() throws IOException {
+ return _entryCursor.getNextEntry();
+ }
+ }
+
+ /**
+ * Handles moving the table index cursor backward.
+ */
+ private final class ReverseIndexDirHandler extends IndexDirHandler {
+ @Override
+ public Position getBeginningPosition() {
+ return getLastPosition();
+ }
+ @Override
+ public Position getEndPosition() {
+ return getFirstPosition();
+ }
+ @Override
+ public IndexData.Entry getAnotherEntry() throws IOException {
+ return _entryCursor.getPreviousEntry();
+ }
+ }
+
+ /**
+ * Value object which maintains the current position of an IndexCursor.
+ */
+ private static final class IndexPosition extends Position
+ {
+ private final IndexData.Entry _entry;
+
+ private IndexPosition(IndexData.Entry entry) {
+ _entry = entry;
+ }
+
+ @Override
+ public RowId getRowId() {
+ return getEntry().getRowId();
+ }
+
+ public IndexData.Entry getEntry() {
+ return _entry;
+ }
+
+ @Override
+ protected boolean equalsImpl(Object o) {
+ return getEntry().equals(((IndexPosition)o).getEntry());
+ }
+
+ @Override
+ public String toString() {
+ return "Entry = " + getEntry();
+ }
+ }
+
+}
/** special entry which is greater than any other entry */
public static final Entry LAST_ENTRY =
createSpecialEntry(RowId.LAST_ROW_ID);
+
+ /** special object which will always be greater than any other value, when
+ searching for an index entry range in a multi-value index */
+ public static final Object MAX_VALUE = new Object();
+
+ /** special object which will always be greater than any other value, when
+ searching for an index entry range in a multi-value index */
+ public static final Object MIN_VALUE = new Object();
protected static final int INVALID_INDEX_PAGE_NUMBER = 0;
continue;
}
+ if(value == MIN_VALUE) {
+ // null is the "least" value
+ _entryBuffer.write(getNullEntryFlag(col.isAscending()));
+ continue;
+ }
+ if(value == MAX_VALUE) {
+ // the opposite null is the "greatest" value
+ _entryBuffer.write(getNullEntryFlag(!col.isAscending()));
+ continue;
+ }
+
col.writeValue(value, _entryBuffer);
}
switch(col.getType()) {
case TEXT:
case MEMO:
+ if(col.getTextSortOrder() != Column.GENERAL_SORT_ORDER) {
+ // unsupported sort order
+ setReadOnly();
+ return new ReadOnlyColumnDescriptor(col, flags);
+ }
return new TextColumnDescriptor(col, flags);
case INT:
case LONG:
/** owning database */
private final Database _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 */
* for the table
*/
protected Table(Database database, ByteBuffer tableBuffer,
- int pageNumber, String name, boolean useBigIndex)
+ int pageNumber, String name, int flags, boolean useBigIndex)
throws IOException
{
_database = database;
_tableDefPageNumber = pageNumber;
_name = name;
+ _flags = flags;
_useBigIndex = useBigIndex;
int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE);
ByteBuffer nextPageBuffer = null;
return _name;
}
+ /**
+ * Whether or not this table has been marked as hidden.
+ */
+ public boolean isHidden() {
+ return((_flags & Database.HIDDEN_OBJECT_FLAG) != 0);
+ }
+
public boolean doUseBigIndex() {
return _useBigIndex;
}
Cursor expected, Cursor found)
{
assertSame(expected.getTable(), found.getTable());
- assertSame(expected.getIndex(), found.getIndex());
+ if(expected instanceof IndexCursor) {
+ assertSame(((IndexCursor)expected).getIndex(),
+ ((IndexCursor)found).getIndex());
+ }
assertEquals(expected.getSavepoint().getCurrentPosition(),
found.getSavepoint().getCurrentPosition());