]> source.dussan.org Git - jackcess.git/commitdiff
implement and test index based cursor
authorJames Ahlborn <jtahlborn@yahoo.com>
Wed, 28 Nov 2007 22:07:06 +0000 (22:07 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Wed, 28 Nov 2007 22:07:06 +0000 (22:07 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@188 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/Cursor.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/UsageMap.java
test/data/indexCursorTest.mdb [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/CursorTest.java

index fc0a25cc6621b5741c501a82e48ffa011aad452e..1838be76dfa98ccf1106ca3ea1f012901a4f51ff 100644 (file)
@@ -5,6 +5,7 @@ package com.healthmarketscience.jackcess;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
@@ -411,12 +412,22 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   }
 
   /**
-   * Restores the current position to the previous position.
+   * Restores a current position for the cursor (current position becomes
+   * previous position).
    */
   protected void restorePosition(Position curPos)
     throws IOException
   {
-    if(!curPos.equals(_curPos)) {
+    restorePosition(curPos, _curPos);
+  }
+    
+  /**
+   * Restores a current and previous position for the cursor.
+   */
+  protected void restorePosition(Position curPos, Position prevPos)
+    throws IOException
+  {
+    if(!curPos.equals(_curPos) || !prevPos.equals(_prevPos)) {
       // make the current position previous, and the new position current
       _prevPos = _curPos;
       _curPos = curPos;
@@ -474,6 +485,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     throws IOException
   {
     Position curPos = _curPos;
+    Position prevPos = _prevPos;
     boolean found = false;
     try {
       found = findRowImpl(columnPattern, valuePattern);
@@ -481,7 +493,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     } finally {
       if(!found) {
         try {
-          restorePosition(curPos);
+          restorePosition(curPos, prevPos);
         } catch(IOException e) {
           LOG.error("Failed restoring position", e);
         }
@@ -504,6 +516,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     throws IOException
   {
     Position curPos = _curPos;
+    Position prevPos = _prevPos;
     boolean found = false;
     try {
       found = findRowImpl(rowPattern);
@@ -511,7 +524,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     } finally {
       if(!found) {
         try {
-          restorePosition(curPos);
+          restorePosition(curPos, prevPos);
         } catch(IOException e) {
           LOG.error("Failed restoring position", e);
         }
@@ -627,7 +640,6 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
   /**
    * Returns the given column from the current row.
    */
-  @SuppressWarnings("foo")
   public Object getCurrentRowValue(Column column)
     throws IOException
   {
@@ -754,21 +766,19 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     }
 
     @Override
-    protected void restorePosition(Position curPos)
+    protected void restorePosition(Position curPos, Position prevPos)
       throws IOException
     {
-      if(!(curPos instanceof ScanPosition)) {
+      if(!(curPos instanceof ScanPosition) ||
+         !(prevPos instanceof ScanPosition)) {
         throw new IllegalArgumentException(
-            "New position must be a scan position");
+            "Restored positions must be scan positions");
       }
-      super.restorePosition(curPos);
-      _ownedPagesCursor.setCurrentPage(curPos.getRowId().getPageNumber());
+      super.restorePosition(curPos, prevPos);
+      _ownedPagesCursor.restorePosition(curPos.getRowId().getPageNumber(),
+                                        prevPos.getRowId().getPageNumber());
     }
 
-    /**
-     * Position the buffer at the next row in the table
-     * @return a ByteBuffer narrowed to the next row, or null if none
-     */
     @Override
     protected Position findAnotherPosition(RowState rowState, Position curPos,
                                            boolean moveForward)
@@ -920,21 +930,97 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     }
 
     @Override
-    protected void restorePosition(Position curPos)
+    protected void restorePosition(Position curPos, Position prevPos)
       throws IOException
     {
-      if(!(curPos instanceof IndexPosition)) {
+      if(!(curPos instanceof IndexPosition) ||
+         !(prevPos instanceof IndexPosition)) {
         throw new IllegalArgumentException(
-            "New position must be an index position");
+            "Restored positions must be index positions");
+      }
+      super.restorePosition(curPos, prevPos);
+      _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(),
+                                   ((IndexPosition)prevPos).getEntry());
+    }
+
+    @Override
+    protected boolean findRowImpl(Column columnPattern, Object valuePattern)
+      throws IOException
+    {
+      Object[] rowValues = _entryCursor.getIndex().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);
+      Index.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 ObjectUtils.equals(getCurrentRowValue(columnPattern),
+                                valuePattern);
+    }
+
+    @Override
+    protected boolean findRowImpl(Map<String,Object> rowPattern)
+      throws IOException
+    {
+      Index index = _entryCursor.getIndex();
+      Object[] rowValues = index.constructIndexRow(rowPattern);
+
+      if(rowValues == null) {
+        // bummer, use the default table scan
+        return super.findRowImpl(rowPattern);
+      }
+      
+      // sweet, we can use our index
+      _entryCursor.beforeEntry(rowValues);
+      Index.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 =
+        new LinkedHashMap<String,Object>();
+      for(Column idxCol : _entryCursor.getIndex().getColumns()) {
+        indexRowPattern.put(idxCol.getName(),
+                            rowValues[idxCol.getColumnNumber()]);
       }
-      super.restorePosition(curPos);
-      _entryCursor.setCurrentEntry(((IndexPosition)curPos).getEntry());
+        
+      // there may be multiple columns which fit the pattern subset used by
+      // the index, so we need to keep checking until we no longer our index
+      // values no longer match
+      do {
+
+        if(!ObjectUtils.equals(getCurrentRow(indexRowPattern.keySet()),
+                               indexRowPattern)) {
+          // there are no more rows which could possibly match
+          break;
+        }
+
+        if(ObjectUtils.equals(getCurrentRow(rowPattern.keySet()),
+                              rowPattern)) {
+          // found it!
+          return true;
+        }
+
+      } while(moveToNextRow());
+        
+      // none of the potential rows matched
+      return false;
     }
     
-    /**
-     * Position the buffer at the next row in the table
-     * @return a ByteBuffer narrowed to the next row, or null if none
-     */
     @Override
     protected Position findAnotherPosition(RowState rowState, Position curPos,
                                            boolean moveForward)
@@ -1004,7 +1090,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>>
     @Override
     public final boolean equals(Object o) {
       return((this == o) ||
-             ((getClass() == o.getClass()) && equalsImpl(o)));
+             ((o != null) && (getClass() == o.getClass()) && equalsImpl(o)));
     }
 
     /**
index 6793a7bde6771ac44ed73ecea69b45f9e0126869..11d6330a3e24ee09155a45c198a70b2140371f32 100644 (file)
@@ -659,7 +659,60 @@ public class Index implements Comparable<Index> {
     
     return removed;
   }
+
+  /**
+   * Constructs an array of values appropriate for this index from the given
+   * column values, expected to match the columns for this index.
+   * @return the appropriate sparse array of data
+   * @throws IllegalArgumentException if the wrong number of values are
+   *         provided
+   */
+  public Object[] constructIndexRow(Object... values)
+  {
+    if(values.length != _columns.size()) {
+      throw new IllegalArgumentException(
+          "Wrong number of column values given " + values.length +
+          ", expected " + _columns.size());
+    }
+    int valIdx = 0;
+    Object[] idxRow = new Object[getTable().getMaxColumnCount()];
+    for(Column col : _columns.keySet()) {
+      idxRow[col.getColumnNumber()] = values[valIdx++];
+    }
+    return idxRow;
+  }
+    
+  /**
+   * Constructs an array of values appropriate for this index from the given
+   * column value.
+   * @return the appropriate sparse array of data or {@code null} if not all
+   *         columns for this index were provided
+   */
+  public Object[] constructIndexRow(String colName, Object value)
+  {
+    return constructIndexRow(Collections.singletonMap(colName, value));
+  }
   
+  /**
+   * Constructs an array of values appropriate for this index from the given
+   * column values.
+   * @return the appropriate sparse array of data or {@code null} if not all
+   *         columns for this index were provided
+   */
+  public Object[] constructIndexRow(Map<String,Object> row)
+  {
+    for(Column col : _columns.keySet()) {
+      if(!row.containsKey(col.getName())) {
+        return null;
+      }
+    }
+
+    Object[] idxRow = new Object[getTable().getMaxColumnCount()];
+    for(Column col : _columns.keySet()) {
+      idxRow[col.getColumnNumber()] = row.get(col.getName());
+    }
+    return idxRow;
+  }  
 
   @Override
   public String toString() {
@@ -898,7 +951,7 @@ public class Index implements Comparable<Index> {
     }
 
     public boolean isValid() {
-      return _rowId.isValid();
+      return(_entryColumns != null);
     }
     
     /**
@@ -934,7 +987,7 @@ public class Index implements Comparable<Index> {
     @Override
     public boolean equals(Object o) {
       return((this == o) ||
-             ((getClass() == o.getClass()) &&
+             ((o != null) && (getClass() == o.getClass()) &&
               (compareTo((Entry)o) == 0)));
     }
     
@@ -943,9 +996,11 @@ public class Index implements Comparable<Index> {
         return 0;
       }
 
-      // note, if the one or both of the entries has no entryColumns, it is a
-      // "special" entry which should be compared on the rowId alone
-      if((_entryColumns != null) && (other.getEntryColumns() != null)) {
+      // note, if the one or both of the entries are not valid, they are
+      // "special" entries, which are handled below
+      if(isValid() && other.isValid()) {
+
+        // comparing two normal entries
         Iterator<EntryColumn> myIter = _entryColumns.iterator();
         Iterator<EntryColumn> otherIter = other.getEntryColumns().iterator();
         while (myIter.hasNext()) {
@@ -960,8 +1015,34 @@ public class Index implements Comparable<Index> {
             return i;
           }
         }
+
+        // if entry columns are equal, sort by rowIds
+        return _rowId.compareTo(other.getRowId());
+      }
+
+      // this is the odd case where mixed entries are being compared.  if both
+      // entries are invalid or the rowIds are not equal, then use the rowId
+      // comparison.
+      int rowCmp = _rowId.compareTo(other.getRowId());
+      if((isValid() == other.isValid()) || (rowCmp != 0)) {
+        return rowCmp;
       }
-      return _rowId.compareTo(other.getRowId());
+
+      // at this point, the rowId's are equal, but the validity is not.  this
+      // will happen when a "special" entry is compared to something created
+      // by EntryCursor.afterEntry or EntryCursor.beforeEntry.  in this case,
+      // the FIRST_ENTRY is always least and the LAST_ENTRY is always
+      // greatest.
+      int cmp = 0;
+      Entry invalid = null;
+      if(!isValid()) {
+        cmp = -1;
+        invalid = this;
+      } else {
+        cmp = 1;
+        invalid = other;
+      }
+      return (cmp * (invalid.equals(FIRST_ENTRY.getRowId()) ? 1 : -1));
     }
     
 
@@ -1016,6 +1097,13 @@ public class Index implements Comparable<Index> {
         }
       }
 
+      @Override
+      public boolean equals(Object o) {
+        return((this == o) ||
+               ((o != null) && (o != null) && (getClass() == o.getClass()) &&
+                (compareTo((EntryColumn)o) == 0)));
+      }
+      
       /**
        * Write this non-null entry column to a buffer
        */
@@ -1335,6 +1423,10 @@ public class Index implements Comparable<Index> {
       reset();
     }
 
+    public Index getIndex() {
+      return Index.this;
+    }
+    
     /**
      * Returns the DirHandler for the given direction
      */
@@ -1376,7 +1468,6 @@ public class Index implements Comparable<Index> {
     public void beforeEntry(Object[] row)
       throws IOException
     {
-      // FIXME, change how row is given?
       restorePosition(new Entry(row, RowId.FIRST_ROW_ID, _columns));
     }
     
@@ -1387,23 +1478,8 @@ public class Index implements Comparable<Index> {
     public void afterEntry(Object[] row)
       throws IOException
     {
-      // FIXME, change how row is given?
       restorePosition(new Entry(row, RowId.LAST_ROW_ID, _columns));
     }
-
-    /**
-     * Returns the current entry.
-     */
-    public Entry getCurrentEntry() {
-      return _curPos.getEntry();
-    }
-
-    /**
-     * Resets the cursor to the given entry.
-     */
-    public void setCurrentEntry(Entry entry) {
-      restorePosition(entry);
-    }
     
     /**
      * @return valid entry if there was entry, {@link Index#LAST_ENTRY}
@@ -1422,12 +1498,22 @@ public class Index implements Comparable<Index> {
     }
 
     /**
-     * Restores a previous position for the cursor.
+     * Restores a current position for the cursor (current position becomes
+     * previous position).
      */
-    private void restorePosition(Entry curEntry)
+    private void restorePosition(Entry curEntry) {
+      restorePosition(curEntry, _curPos.getEntry());
+    }
+    
+    /**
+     * Restores a current and previous position for the cursor.
+     */
+    protected void restorePosition(Entry curEntry, Entry prevEntry)
     {
-      if(!curEntry.equals(_curPos.getEntry())) {
-        _prevPos = updatePosition(_curPos.getEntry());
+      if(!curEntry.equals(_curPos.getEntry()) ||
+         !prevEntry.equals(_prevPos.getEntry()))
+      {
+        _prevPos = updatePosition(prevEntry);
         _curPos = updatePosition(curEntry);
         _lastModCount = Index.this._modCount;
       } else {
@@ -1451,19 +1537,19 @@ public class Index implements Comparable<Index> {
      */
     private Position updatePosition(Entry entry) {
       int curIdx = FIRST_ENTRY_IDX;
-      boolean deleted = false;
+      boolean between = false;
       RowId rowId = entry.getRowId();
-      if(rowId.isValid()) {
+      if(entry.isValid()) {
         // find the new position for this entry
         int idx = findEntry(entry);
         if(idx >= 0) {
           curIdx = idx;
         } else {
-          // given entry was deleted.  our current position is now really
-          // between two indexes, but we cannot support that as an integer
-          // value so we set a flag instead
+          // given entry was not found exactly.  our current position is now
+          // really between two indexes, but we cannot support that as an
+          // integer value so we set a flag instead
           curIdx = missingIndexToInsertionPoint(idx);
-          deleted = true;
+          between = true;
         }
       } else if(rowId.equals(RowId.FIRST_ROW_ID)) {
         curIdx = FIRST_ENTRY_IDX;
@@ -1472,7 +1558,7 @@ public class Index implements Comparable<Index> {
       } else {
         throw new IllegalArgumentException("Invalid entry given: " + entry);
       }
-      return new Position(curIdx, entry, deleted);
+      return new Position(curIdx, entry, between);
     }
     
     /**
@@ -1494,7 +1580,7 @@ public class Index implements Comparable<Index> {
 
       _prevPos = _curPos;
       _curPos = handler.getAnotherPosition(_curPos.getIndex(),
-                                           _curPos.isDeleted());
+                                           _curPos.isBetween());
       return _curPos.getEntry();
     }
 
@@ -1509,7 +1595,7 @@ public class Index implements Comparable<Index> {
      * logic from value storage.
      */
     private abstract class DirHandler {
-      public abstract Position getAnotherPosition(int curIdx, boolean deleted);
+      public abstract Position getAnotherPosition(int curIdx, boolean between);
       public abstract Position getBeginningPosition();
       public abstract Position getEndPosition();
       protected final Position newPosition(int curIdx) {
@@ -1530,10 +1616,10 @@ public class Index implements Comparable<Index> {
      */
     private final class ForwardDirHandler extends DirHandler {
       @Override
-      public Position getAnotherPosition(int curIdx, boolean deleted) {
+      public Position getAnotherPosition(int curIdx, boolean between) {
         // note, curIdx does not need to be advanced if it was pointing at a
-        // deleted entry
-        if(!deleted) {
+        // between position
+        if(!between) {
           curIdx = ((curIdx == getBeginningPosition().getIndex()) ?
                     0 : (curIdx + 1));
         }
@@ -1554,10 +1640,10 @@ public class Index implements Comparable<Index> {
      */
     private final class ReverseDirHandler extends DirHandler {
       @Override
-      public Position getAnotherPosition(int curIdx, boolean deleted) {
-        // note, we ignore the deleted flag here because the index will be
-        // pointing at the correct next index in either the deleted or
-        // non-deleted case
+      public Position getAnotherPosition(int curIdx, boolean between) {
+        // note, we ignore the between flag here because the index will be
+        // pointing at the correct next index in either the between or
+        // non-between case
         curIdx = ((curIdx == getBeginningPosition().getIndex()) ?
                   (_entries.size() - 1) : (curIdx - 1));
         return newReversePosition(curIdx);
@@ -1583,16 +1669,16 @@ public class Index implements Comparable<Index> {
     private final Entry _entry;
     /** {@code true} if this entry does not currently exist in the entry list,
         {@code false} otherwise */
-    private final boolean _deleted;
+    private final boolean _between;
 
     private Position(int idx, Entry entry) {
       this(idx, entry, false);
     }
     
-    private Position(int idx, Entry entry, boolean deleted) {
+    private Position(int idx, Entry entry, boolean between) {
       _idx = idx;
       _entry = entry;
-      _deleted = deleted;
+      _between = between;
     }
 
     public int getIndex() {
@@ -1603,23 +1689,23 @@ public class Index implements Comparable<Index> {
       return _entry;
     }
 
-    public boolean isDeleted() {
-      return _deleted;
+    public boolean isBetween() {
+      return _between;
     }
 
     @Override
     public boolean equals(Object o) {
       return((this == o) ||
-             ((getClass() == o.getClass()) &&
+             ((o != null) && (getClass() == o.getClass()) &&
               (_idx == ((Position)o)._idx) &&
               _entry.equals(((Position)o)._entry) &&
-              (_deleted == ((Position)o)._deleted)));
+              (_between == ((Position)o)._between)));
     }
 
     @Override
     public String toString() {
-      return "Idx = " + _idx + ", Entry = " + _entry + ", Deleted = " +
-        _deleted;
+      return "Idx = " + _idx + ", Entry = " + _entry + ", Between = " +
+        _between;
     }
   }
   
index d5eacd20c05614001a33ff8c8b45795f22bab480..08c7d4a90bdafbd0ade678d021405d1c04e20c42 100644 (file)
@@ -171,6 +171,10 @@ public class Table
     return _name;
   }
 
+  public int getMaxColumnCount() {
+    return _maxColumnCount;
+  }
+  
   public Database getDatabase() {
     return _database;
   }
@@ -1487,7 +1491,7 @@ public class Table
     
     private RowState(boolean hardRowBuffer) {
       _headerRowBufferH = TempPageHolder.newHolder(hardRowBuffer);
-      _rowValues = new Object[Table.this._maxColumnCount];
+      _rowValues = new Object[Table.this.getMaxColumnCount()];
       _lastModCount = Table.this._modCount;
     }
 
index 8833edb9078117b632b1770bc1296a1d81314f21..4b1c76d270bac11235221863b08f49f27eb9a21f 100644 (file)
@@ -723,6 +723,10 @@ public class UsageMap
       reset();
     }
 
+    public UsageMap getUsageMap() {
+      return UsageMap.this;
+    }
+    
     /**
      * Returns the DirHandler for the given direction
      */
@@ -738,25 +742,6 @@ public class UsageMap
       return(UsageMap.this._modCount == _lastModCount);
     }    
 
-    /**
-     * Returns the current page number.
-     */
-    public int getCurrentPage() {
-      return _curPageNumber;
-    }
-
-    /**
-     * Resets the cursor to the given page number.
-     */
-    public void setCurrentPage(int curPageNumber) {
-      if(curPageNumber < UsageMap.this.getFirstPageNumber()) {
-        curPageNumber = RowId.FIRST_PAGE_NUMBER;
-      } else if(curPageNumber > UsageMap.this.getLastPageNumber()) {
-        curPageNumber = RowId.LAST_PAGE_NUMBER;
-      }
-      restorePosition(curPageNumber);
-    }
-    
     /**
      * @return valid page number if there was another page to read,
      *         {@link RowId#LAST_PAGE_NUMBER} otherwise
@@ -829,25 +814,49 @@ public class UsageMap
     }
 
     /**
-     * Restores a previous position for the cursor.
+     * Restores a current position for the cursor (current position becomes
+     * previous position).
      */
-    private void restorePosition(int curPageNumber) {
-      if(curPageNumber != _curPageNumber) {
-        _prevPageNumber = _curPageNumber;
-        _curPageNumber = curPageNumber;
+    private void restorePosition(int curPageNumber)
+    {
+      restorePosition(curPageNumber, _curPageNumber);
+    }
+    
+    /**
+     * Restores a current and previous position for the cursor.
+     */
+    protected void restorePosition(int curPageNumber, int prevPageNumber) {
+      if((curPageNumber != _curPageNumber) ||
+         (prevPageNumber != _prevPageNumber))
+      {
+        _prevPageNumber = updatePosition(prevPageNumber);
+        _curPageNumber = updatePosition(curPageNumber);
+        _lastModCount = UsageMap.this._modCount;
+      } else {
+        checkForModification();
       }
-      _lastModCount = UsageMap.this._modCount;
     }
 
     /**
      * Checks the usage map for modifications an updates state accordingly.
      */
     private void checkForModification() {
-      // since page numbers are not affected by modifications, we don't need
-      // to adjust anything
-      _lastModCount = UsageMap.this._modCount;      
+      if(!isUpToDate()) {
+        _prevPageNumber = updatePosition(_prevPageNumber);
+        _curPageNumber = updatePosition(_curPageNumber);
+        _lastModCount = UsageMap.this._modCount;
+      }
     }
 
+    private int updatePosition(int pageNumber) {
+      if(pageNumber < UsageMap.this.getFirstPageNumber()) {
+        pageNumber = RowId.FIRST_PAGE_NUMBER;
+      } else if(pageNumber > UsageMap.this.getLastPageNumber()) {
+        pageNumber = RowId.LAST_PAGE_NUMBER;
+      }
+      return pageNumber;
+    }
+    
     @Override
     public String toString() {
       return getClass().getSimpleName() + " CurPosition " + _curPageNumber +
diff --git a/test/data/indexCursorTest.mdb b/test/data/indexCursorTest.mdb
new file mode 100644 (file)
index 0000000..561c44c
Binary files /dev/null and b/test/data/indexCursorTest.mdb differ
index a99abef717dc8c88b7ec94074a6c031430415805..d10d2b74d9fbbd49d324fdc27ee74b483e3b8fc1 100644 (file)
@@ -2,6 +2,7 @@
 
 package com.healthmarketscience.jackcess;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -48,13 +49,42 @@ public class CursorTest extends TestCase {
     db.createTable("test", columns);
 
     Table table = db.getTable("test");
-    for(int i = 0; i < 10; ++i) {
-      table.addRow(i, "data" + i);
+    for(Map<String,Object> row : createTestTableData()) {
+      table.addRow(row.get("id"), row.get("value"));
     }
 
     return db;
   }
 
+  private static List<Map<String,Object>> createUnorderedTestTableData()
+    throws Exception
+  {
+    List<Map<String,Object>> expectedRows =
+      new ArrayList<Map<String,Object>>();
+    int[] ids = new int[]{3, 7, 6, 1, 2, 9, 0, 5, 4, 8};
+    for(int i : ids) {
+      expectedRows.add(createExpectedRow("id", i, "value", "data" + i));
+    }
+    return expectedRows;
+  }  
+  
+  private static Database createTestIndexTable() throws Exception {
+    File srcFile = new File("test/data/indexCursorTest.mdb");
+    File dbFile = File.createTempFile("databaseTest", ".mdb");
+    dbFile.deleteOnExit();
+    copyFile(srcFile, dbFile);
+    
+    Database db = Database.open(dbFile);
+
+    Table table = db.getTable("test");
+
+    for(Map<String,Object> row : createUnorderedTestTableData()) {
+      table.addRow(row.get("id"), row.get("value"));
+    }
+    
+    return db;
+  }
+
   public void testRowId() throws Exception {
     // test special cases
     RowId rowId1 = new RowId(1, 2);
@@ -74,27 +104,36 @@ public class CursorTest extends TestCase {
     Database db = createTestTable();
 
     Table table = db.getTable("test");
+    Cursor cursor = Cursor.createCursor(table);
+    doTestSimple(table, cursor);
+    db.close();
+  }
+
+  private void doTestSimple(Table table, Cursor cursor) throws Exception {  
     List<Map<String,Object>> expectedRows = createTestTableData();
 
-    Cursor cursor = Cursor.createCursor(table);
     List<Map<String, Object>> foundRows =
       new ArrayList<Map<String, Object>>();
     for(Map<String, Object> row : cursor) {
       foundRows.add(row);
     }
     assertEquals(expectedRows, foundRows);
-
-    db.close();
   }
 
   public void testSkip() throws Exception {
     Database db = createTestTable();
 
     Table table = db.getTable("test");
+    Cursor cursor = Cursor.createCursor(table);
+    doTestSkip(table, cursor);
+    
+    db.close();
+  }
+
+  private void doTestSkip(Table table, Cursor cursor) throws Exception {
     List<Map<String,Object>> expectedRows = createTestTableData();
     expectedRows.subList(1, 4).clear();
 
-    Cursor cursor = Cursor.createCursor(table);
     List<Map<String, Object>> foundRows =
       new ArrayList<Map<String, Object>>();
     foundRows.add(cursor.getNextRow());
@@ -105,17 +144,20 @@ public class CursorTest extends TestCase {
     assertEquals(expectedRows, foundRows);
 
     assertEquals(0, cursor.skipNextRows(3));
-
-    db.close();
   }
 
   public void testSearch() throws Exception {
     Database db = createTestTable();
 
     Table table = db.getTable("test");
-    List<Map<String,Object>> expectedRows = createTestTableData();
-
     Cursor cursor = Cursor.createCursor(table);
+    doTestSearch(table, cursor);
+    
+    db.close();
+  }
+
+  private void doTestSearch(Table table, Cursor cursor) throws Exception {
+    List<Map<String,Object>> expectedRows = createTestTableData();
 
     assertTrue(cursor.findRow(table.getColumn("id"), 3));
     assertEquals(createExpectedRow("id", 3,
@@ -140,28 +182,30 @@ public class CursorTest extends TestCase {
                  Cursor.findValue(table,
                                   table.getColumn("value"),
                                   table.getColumn("id"), 9));
-
-    db.close();
   }
 
   public void testReverse() throws Exception {
     Database db = createTestTable();
 
     Table table = db.getTable("test");
+    Cursor cursor = Cursor.createCursor(table);
+    doTestReverse(table, cursor);
+
+    db.close();
+  }
+
+  private void doTestReverse(Table table, Cursor cursor) throws Exception {
     List<Map<String,Object>> expectedRows = createTestTableData();
     Collections.reverse(expectedRows);
 
-    Cursor cursor = Cursor.createCursor(table);
     List<Map<String, Object>> foundRows =
       new ArrayList<Map<String, Object>>();
     for(Map<String, Object> row : cursor.reverseIterable()) {
       foundRows.add(row);
     }
-    assertEquals(expectedRows, foundRows);
-
-    db.close();
+    assertEquals(expectedRows, foundRows);    
   }
-
+  
   public void testLiveAddition() throws Exception {
     Database db = createTestTable();
 
@@ -169,6 +213,15 @@ public class CursorTest extends TestCase {
 
     Cursor cursor1 = Cursor.createCursor(table);
     Cursor cursor2 = Cursor.createCursor(table);
+    doTestLiveAddition(table, cursor1, cursor2);
+    
+    db.close();
+  }
+
+  private void doTestLiveAddition(Table table,
+                                  Cursor cursor1,
+                                  Cursor cursor2) throws Exception
+  {
     cursor1.skipNextRows(11);
     cursor2.skipNextRows(11);
 
@@ -189,10 +242,9 @@ public class CursorTest extends TestCase {
     assertFalse(cursor2.moveToNextRow());
     assertTrue(cursor1.isAfterLast());
     assertTrue(cursor2.isAfterLast());
-    
-    db.close();
   }
 
+  
   public void testLiveDeletion() throws Exception {
     Database db = createTestTable();
 
@@ -202,6 +254,17 @@ public class CursorTest extends TestCase {
     Cursor cursor2 = Cursor.createCursor(table);
     Cursor cursor3 = Cursor.createCursor(table);
     Cursor cursor4 = Cursor.createCursor(table);
+    doTestLiveDeletion(table, cursor1, cursor2, cursor3, cursor4);
+    
+    db.close();
+  }
+
+  private void doTestLiveDeletion(Table table,
+                                  Cursor cursor1,
+                                  Cursor cursor2,
+                                  Cursor cursor3,
+                                  Cursor cursor4) throws Exception
+  {
     cursor1.skipNextRows(2);
     cursor2.skipNextRows(3);
     cursor3.skipNextRows(3);
@@ -230,9 +293,81 @@ public class CursorTest extends TestCase {
     assertEquals(expectedNextRow, cursor3.getNextRow());
     
     assertEquals(expectedPrevRow, cursor3.getPreviousRow());
+  }
+
+  public void testSimpleIndex() throws Exception {
+    Database db = createTestIndexTable();
+
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+
+    assertTable(createUnorderedTestTableData(), table);
+
+    Cursor cursor = Cursor.createIndexCursor(table, idx);
+    doTestSimple(table, cursor);
+
+    db.close();
+  }
+
+  public void testSkipIndex() throws Exception {
+    Database db = createTestIndexTable();
+
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+    Cursor cursor = Cursor.createIndexCursor(table, idx);
+    doTestSkip(table, cursor);
     
     db.close();
   }
+  
+  public void testReverseIndex() throws Exception {
+    Database db = createTestIndexTable();
+
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+    Cursor cursor = Cursor.createIndexCursor(table, idx);
+    doTestReverse(table, cursor);
+
+    db.close();
+  }
+
+  public void testSearchIndex() throws Exception {
+    Database db = createTestIndexTable();
+
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+    Cursor cursor = Cursor.createIndexCursor(table, idx);
+    doTestSearch(table, cursor);
+    
+    db.close();
+  }
+
+  public void testLiveAdditionIndex() throws Exception {
+    Database db = createTestIndexTable();
 
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+
+    Cursor cursor1 = Cursor.createIndexCursor(table, idx);
+    Cursor cursor2 = Cursor.createIndexCursor(table, idx);
+    doTestLiveAddition(table, cursor1, cursor2);
+    
+    db.close();
+  }
+
+  public void testLiveDeletionIndex() throws Exception {
+    Database db = createTestIndexTable();
+
+    Table table = db.getTable("test");
+    Index idx = table.getIndexes().get(0);
+
+    Cursor cursor1 = Cursor.createIndexCursor(table, idx);
+    Cursor cursor2 = Cursor.createIndexCursor(table, idx);
+    Cursor cursor3 = Cursor.createIndexCursor(table, idx);
+    Cursor cursor4 = Cursor.createIndexCursor(table, idx);
+    doTestLiveDeletion(table, cursor1, cursor2, cursor3, cursor4);
+    
+    db.close();
+  }
   
 }