From 48e87a45641089eba1716e856505f74bd359f0e7 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Wed, 9 Mar 2011 04:58:10 +0000 Subject: [PATCH] general revamp of table finding using index backed cursors; use object flags to determine system/hidden objects; read/write text column sort order git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@524 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../CaseInsensitiveColumnMatcher.java | 8 +- .../healthmarketscience/jackcess/Column.java | 29 +- .../healthmarketscience/jackcess/Cursor.java | 278 +--------- .../jackcess/CursorBuilder.java | 45 +- .../jackcess/Database.java | 410 ++++++++++++--- .../jackcess/IndexCursor.java | 479 ++++++++++++++++++ .../jackcess/IndexData.java | 24 + .../healthmarketscience/jackcess/Table.java | 12 +- .../jackcess/CursorBuilderTest.java | 5 +- 9 files changed, 945 insertions(+), 345 deletions(-) create mode 100644 src/java/com/healthmarketscience/jackcess/IndexCursor.java diff --git a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java b/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java index 8aa3fa0..a88d0d2 100644 --- a/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java +++ b/src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java @@ -41,7 +41,7 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher { 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); @@ -61,10 +61,4 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher { } } - private static boolean isTextual(Column col) - { - DataType type = col.getType(); - return((type == DataType.TEXT) || (type == DataType.MEMO)); - } - } diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 9f963be..d22b61c 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -117,6 +117,9 @@ public class Column implements Comparable { /** 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*"); @@ -153,6 +156,8 @@ public class Column implements Comparable { 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; @@ -194,6 +199,14 @@ public class Column implements Comparable { 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); @@ -324,6 +337,14 @@ public class Column implements Comparable { _scale = newScale; } + public short getTextSortOrder() { + return _textSortOrder; + } + + public void setTextSortOrder(short newTextSortOrder) { + _textSortOrder = newTextSortOrder; + } + public void setLength(short length) { _columnLength = length; } @@ -447,8 +468,7 @@ public class Column implements Comparable { } if(isCompressedUnicode()) { - if((getType() != DataType.TEXT) && - (getType() != DataType.MEMO)) { + if(!getType().isTextual()) { throw new IllegalArgumentException( "Only textual columns allow unicode compression (text/memo)"); } @@ -1375,6 +1395,9 @@ public class Column implements Comparable { 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(); } @@ -1633,6 +1656,8 @@ public class Column implements Comparable { 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 diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index ee8395e..58acfee 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -31,7 +31,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; @@ -84,11 +83,11 @@ public abstract class Cursor implements Iterable> /** 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; @@ -122,7 +121,7 @@ public abstract class Cursor implements Iterable> public static Cursor createIndexCursor(Table table, Index index) throws IOException { - return createIndexCursor(table, index, null, null); + return IndexCursor.createCursor(table, index); } /** @@ -145,7 +144,7 @@ public abstract class Cursor implements Iterable> Object[] startRow, Object[] endRow) throws IOException { - return createIndexCursor(table, index, startRow, true, endRow, true); + return IndexCursor.createCursor(table, index, startRow, endRow); } /** @@ -173,18 +172,8 @@ public abstract class Cursor implements Iterable> 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); } /** @@ -293,10 +282,6 @@ public abstract class Cursor implements Iterable> return _table; } - public Index getIndex() { - return null; - } - public JetFormat getFormat() { return getTable().getFormat(); } @@ -334,11 +319,18 @@ public abstract class Cursor implements Iterable> */ 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}. @@ -1182,213 +1174,6 @@ public abstract class Cursor implements Iterable> } - /** - * 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 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 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(); - 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 @@ -1400,7 +1185,7 @@ public abstract class Cursor implements Iterable> 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); } @@ -1518,35 +1303,4 @@ public abstract class Cursor implements Iterable> } } - /** - * 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(); - } - } - } diff --git a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java index c1e930d..4e955d0 100644 --- a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -28,12 +28,12 @@ King of Prussia, PA 19406 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 @@ -114,10 +114,30 @@ public class CursorBuilder { * 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 searchColumns = Arrays.asList(columns); + List colNames = new ArrayList(); + for(Column col : columns) { + colNames.add(col.getName()); + } + return setIndexByColumns(colNames); + } + + /** + * Searches for an index with the given column names. + */ + private CursorBuilder setIndexByColumns(List searchColumns) { boolean found = false; for(Index index : _table.getIndexes()) { @@ -125,13 +145,14 @@ public class CursorBuilder { if(indexColumns.size() != searchColumns.size()) { continue; } - Iterator sIter = searchColumns.iterator(); + Iterator sIter = searchColumns.iterator(); Iterator 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; } @@ -274,4 +295,14 @@ public class CursorBuilder { return cursor; } + /** + * Returns a new index cursor for the table, constructed to the given + * specifications. + */ + public IndexCursor toIndexCursor() + throws IOException + { + return (IndexCursor)toCursor(); + } + } diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index cc14cf5..806c791 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -35,6 +35,8 @@ import java.io.Flushable; 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; @@ -52,6 +54,7 @@ import java.util.EnumSet; 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; @@ -221,8 +224,21 @@ public class Database /** 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 { @@ -256,8 +272,6 @@ public class Database /** 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 */ @@ -273,14 +287,17 @@ public class Database /** 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 SYSTEM_CATALOG_COLUMNS = - new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID)); - - /** the columns to read when finding queries */ - private static Collection SYSTEM_CATALOG_QUERY_COLUMNS = - new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, + new HashSet(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 SYSTEM_CATALOG_TABLE_NAME_COLUMNS = + new HashSet(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, + CAT_COL_FLAGS, CAT_COL_PARENT_ID)); /** @@ -331,17 +348,26 @@ public class Database /** 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 _tableLookup = - new HashMap(); + private final Map _tableLookup = + new LinkedHashMap() { + private static final long serialVersionUID = 0L; + @Override + protected boolean removeEldestEntry(Map.Entry e) { + return(size() > MAX_CACHED_LOOKUP_TABLES); + } + }; /** set of table names as stored in the mdb file, created on demand */ private Set _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) */ @@ -362,6 +388,8 @@ public class Database 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 @@ -851,33 +879,32 @@ public class Database */ private void readSystemCatalog() throws IOException { _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, - defaultUseBigIndex()); - for(Map 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: " + @@ -888,12 +915,12 @@ public class Database /** * @return The names of all of the user tables (String) */ - public Set getTableNames() { + public Set getTableNames() throws IOException { if(_tableNames == null) { - _tableNames = new TreeSet(String.CASE_INSENSITIVE_ORDER); - for(TableInfo tableInfo : _tableLookup.values()) { - _tableNames.add(tableInfo.tableName); - } + Set tableNames = + new TreeSet(String.CASE_INSENSITIVE_ORDER); + _tableFinder.getTableNames(tableNames); + _tableNames = tableNames; } return _tableNames; } @@ -925,14 +952,32 @@ public class Database * @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); } /** @@ -1081,8 +1126,7 @@ public class Database Map> queryRowMap = new HashMap>(); for(Map 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))) { @@ -1132,20 +1176,7 @@ public class Database public Table getSystemTable(String tableName) throws IOException { - for(Map 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()); } /** @@ -1335,16 +1366,25 @@ public class Database /** * 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)); } /** @@ -1532,7 +1572,7 @@ public class Database 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; } @@ -1540,8 +1580,22 @@ public class Database /** * @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; } /** @@ -1551,6 +1605,14 @@ public class Database 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. @@ -1679,11 +1741,14 @@ public class Database { 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; } } @@ -1695,7 +1760,11 @@ public class Database private Iterator _tableNameIter; private TableIterator() { - _tableNameIter = getTableNames().iterator(); + try { + _tableNameIter = getTableNames().iterator(); + } catch(IOException e) { + throw new IllegalStateException(e); + } } public boolean hasNext() { @@ -1717,5 +1786,216 @@ public class Database } } } + + /** + * 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 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 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 tableNames) throws IOException { + + IndexCursor tNameCursor = new CursorBuilder(_systemCatalog) + .setIndex(_systemCatalogCursor.getIndex()) + .setStartEntry(_tableParentId, IndexData.MIN_VALUE) + .setEndEntry(_tableParentId, IndexData.MAX_VALUE) + .toIndexCursor(); + + for(Map 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 rowPat = new HashMap(); + 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 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 tableNames) throws IOException { + + for(Map 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 + { + private final Integer _pageNumber; + + private WeakTableReference(Integer pageNumber, Table table, + ReferenceQueue
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 _tables = + new HashMap(); + private final ReferenceQueue
_queue = new ReferenceQueue
(); + + 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()); + } + } + } } diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java new file mode 100644 index 0000000..df31a09 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -0,0 +1,479 @@ +/* +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 _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. + *

+ * 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. + *

+ * 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. + *

+ * 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(); + for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { + _indexEntryPattern.add(col.getName()); + } + } + + // check the next row to see if it actually matches + Map 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 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 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(); + 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(); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/IndexData.java index fa2f67e..4aed9e1 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/IndexData.java @@ -62,6 +62,14 @@ public abstract class IndexData { /** 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; @@ -1078,6 +1086,17 @@ public abstract class IndexData { 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); } @@ -1437,6 +1456,11 @@ public abstract class IndexData { 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: diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 0f3a076..1b4cdb8 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -104,6 +104,8 @@ public class Table /** 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 */ @@ -186,12 +188,13 @@ public class 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; @@ -222,6 +225,13 @@ public class Table 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; } diff --git a/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java b/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java index 260238d..c1872fa 100644 --- a/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java @@ -44,7 +44,10 @@ public class CursorBuilderTest extends TestCase { 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()); -- 2.39.5