]> source.dussan.org Git - jackcess.git/commitdiff
general revamp of table finding using index backed cursors; use object flags to deter...
authorJames Ahlborn <jtahlborn@yahoo.com>
Wed, 9 Mar 2011 04:58:10 +0000 (04:58 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Wed, 9 Mar 2011 04:58:10 +0000 (04:58 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@524 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/CaseInsensitiveColumnMatcher.java
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/Cursor.java
src/java/com/healthmarketscience/jackcess/CursorBuilder.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/IndexCursor.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/IndexData.java
src/java/com/healthmarketscience/jackcess/Table.java
test/src/java/com/healthmarketscience/jackcess/CursorBuilderTest.java

index 8aa3fa0100027559897536f5eca118d5149d3af5..a88d0d27a60c7be2878510edf693265c731e4fef 100644 (file)
@@ -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));
-  }
-
 }
index 9f963be0bdc56d1ea41d85c6757378b5f814773d..d22b61c6bd699bad3ab8dccf05023cc8327b3ac1 100644 (file)
@@ -117,6 +117,9 @@ public class Column implements Comparable<Column> {
   /** 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<Column> {
   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<Column> {
     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<Column> {
     _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<Column> {
     }
 
     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<Column> {
     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<Column> {
       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
index ee8395e23baeb4a70ea2b2ff914288eb2efd0b4c..58acfee89bef2b47adeb2330b655ebe63e32e01a 100644 (file)
@@ -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<Map<String, Object>>
   /** 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<Map<String, Object>>
   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<Map<String, Object>>
                                          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<Map<String, Object>>
                                          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<Map<String, Object>>
     return _table;
   }
 
-  public Index getIndex() {
-    return null;
-  }
-  
   public JetFormat getFormat() {
     return getTable().getFormat();
   }
@@ -334,11 +319,18 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
    */
   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<Map<String, Object>>
     
   }
 
-  /**
-   * 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
@@ -1400,7 +1185,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     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<Map<String, Object>>
     }
   }
   
-  /**
-   * 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();
-    }
-  }
-  
 }
index c1e930d8a35145bd73594965b3c7189e16b252f9..4e955d0535bcb563618eac481f291af849189618 100644 (file)
@@ -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<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()) {
       
@@ -125,13 +145,14 @@ public class CursorBuilder {
       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;
         }
@@ -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();
+  }
+
 }
index cc14cf5d6bc45986f36bb69575ea323b9299031b..806c7918f3b628abf25e725d9ef08a62149ddc17 100644 (file)
@@ -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<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));
   
   
   /**
@@ -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<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) */
@@ -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<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: " +
@@ -888,12 +915,12 @@ public class Database
   /**
    * @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;
   }
@@ -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<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))) {
@@ -1132,20 +1176,7 @@ public class Database
   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());
   }
 
   /**
@@ -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<String> _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<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());
+      }
+    }
+  }
   
 }
diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java
new file mode 100644 (file)
index 0000000..df31a09
--- /dev/null
@@ -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<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();
+    }
+  }
+  
+}
index fa2f67e3a4515707f3472f24ef270a020816cfbe..4aed9e191c3b0abe242d15835824e9cfde7c2272 100644 (file)
@@ -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:
index 0f3a076ecb9f5b6814178472d13e109588278d19..1b4cdb8c156e39718dd8bbe6ec86a732c31e52e3 100644 (file)
@@ -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;
   }
index 260238d5f44aa6e5920f9da6581f4bc3c2bba931..c1872fa8366628d847a16ea6fa5e39a117ef5ac3 100644 (file)
@@ -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());