]> source.dussan.org Git - jackcess.git/commitdiff
Move table iteration out of Table and into Cursor. First stage in
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 20 Nov 2007 21:03:11 +0000 (21:03 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 20 Nov 2007 21:03:11 +0000 (21:03 +0000)
        offering more complicated table access.

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@178 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Cursor.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/RowId.java
src/java/com/healthmarketscience/jackcess/Table.java
test/src/java/com/healthmarketscience/jackcess/CursorTest.java [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java

index b92065bfa7c8a6ad55ab4853d6a73332a1616bca..e027364f6d08448d3bfc9e45c7852f4b5c6bd5be 100644 (file)
@@ -6,6 +6,10 @@
   </properties>
   <body>
     <release version="1.1.10" date="TBD">
+      <action dev="jahlborn" type="update">
+        Move table iteration out of Table and into Cursor.  First stage in
+        offering more complicated table access.
+      </action>
       <action dev="jahlborn" type="fix" issue="1681954">
         Update table row count correctly on row deletion or bulk row addition,
         bug #1681954.
index 3a161db6f9004d53a888e0f23a0224056ceacef9..e0ae0a778936ecd6bd20e926ec974d9325df6f9f 100644 (file)
 
 package com.healthmarketscience.jackcess;
 
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import com.healthmarketscience.jackcess.Table.RowState;
+import org.apache.commons.lang.ObjectUtils;
+
+import static com.healthmarketscience.jackcess.PageChannel.INVALID_PAGE_NUMBER;
+import static com.healthmarketscience.jackcess.RowId.INVALID_ROW_NUMBER;
+
+
 /**
- * Describe class Cursor here.
- *
+ * Manages iteration for a Table.  Different cursors provide different methods
+ * of traversing a table.  Cursors should be fairly robust in the face of
+ * table modification during traversal (although depending on how the table is
+ * traversed, row updates may or may not be seen).  Multiple cursors may
+ * traverse the same table simultaneously.
+ * <p>
+ * Is not thread-safe.
  *
  * @author james
  */
-public class Cursor {
+public abstract class Cursor implements Iterable<Map<String, Object>>
+{
+  private static final int FIRST_PAGE_NUMBER = INVALID_PAGE_NUMBER;
+  private static final int LAST_PAGE_NUMBER = Integer.MAX_VALUE;
+
+  public static final RowId FIRST_ROW_ID = new RowId(
+      FIRST_PAGE_NUMBER, INVALID_ROW_NUMBER);
+
+  public static final RowId LAST_ROW_ID = new RowId(
+      LAST_PAGE_NUMBER, INVALID_ROW_NUMBER);
 
+  
   /** owning table */
-  private final Table _table;
-//   /** Number of the current row in a data page */
-//   private int _currentRowInPage = INVALID_ROW_NUMBER;
-//   /** Number of rows left to be read on the current page */
-//   private short _rowsLeftOnPage = 0;
-//   /** State used for reading the table rows */
-//   private RowState _rowState;
-//   /** Iterator over the pages that this table owns */
-//   private UsageMap.PageIterator _ownedPagesIterator;
+  protected final Table _table;
+  /** State used for reading the table rows */
+  protected final RowState _rowState;
+  /** the first (exclusive) row id for this iterator */
+  protected final RowId _firstRowId;
+  /** the last (exclusive) row id for this iterator */
+  protected final RowId _lastRowId;
+  /** the current row */
+  protected RowId _currentRowId;
   
+
+  protected Cursor(Table table, RowId firstRowId, RowId lastRowId) {
+    _table = table;
+    _rowState = _table.createRowState();
+    _firstRowId = firstRowId;
+    _lastRowId = lastRowId;
+    _currentRowId = firstRowId;
+  }
+
   /**
-   * Creates a new <code>Cursor</code> instance.
-   *
+   * Creates a normal, un-indexed cursor for the given table.
    */
-  public Cursor(Table table) {
-    _table = table;
+  public static Cursor createCursor(Table table) {
+    return new TableScanCursor(table);
+  }
+  
+  public Table getTable() {
+    return _table;
+  }
+  
+  public JetFormat getFormat() {
+    return getTable().getFormat();
+  }
+
+  public PageChannel getPageChannel() {
+    return getTable().getPageChannel();
+  }
+
+
+  /**
+   * Returns the first row id (exclusive) as defined by this cursor.
+   */
+  protected RowId getFirstRowId() {
+    return _firstRowId;
+  }
+  
+  /**
+   * Returns the last row id (exclusive) as defined by this cursor.
+   */
+  protected RowId getLastRowId() {
+    return _lastRowId;
+  }
+  
+  public void reset() {
+    _currentRowId = getFirstRowId();
+    _rowState.reset();
+  }  
+
+  /**
+   * Calls <code>reset</code> on this table and returns a modifiable Iterator
+   * which will iterate through all the rows of this table.  Use of the
+   * Iterator follows the same restrictions as a call to
+   * <code>getNextRow</code>.
+   * @throws IllegalStateException if an IOException is thrown by one of the
+   *         operations, the actual exception will be contained within
+   */
+  public Iterator<Map<String, Object>> iterator()
+  {
+    return iterator(null);
+  }
+  
+  /**
+   * Calls <code>reset</code> on this table and returns a modifiable Iterator
+   * which will iterate through all the rows of this table, returning only the
+   * given columns.  Use of the Iterator follows the same restrictions as a
+   * call to <code>getNextRow</code>.
+   * @throws IllegalStateException if an IOException is thrown by one of the
+   *         operations, the actual exception will be contained within
+   */
+  public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
+  {
+    return new RowIterator(columnNames);
+  }
+
+  /**
+   * Delete the current row (retrieved by a call to {@link #getNextRow}).
+   */
+  public void deleteCurrentRow() throws IOException {
+    _table.deleteRow(_rowState, _currentRowId);
+  }
+
+  /**
+   * @return The next row in this table (Column name -> Column value)
+   */
+  public Map<String, Object> getNextRow() throws IOException {
+    return getNextRow(null);
+  }
+
+  /**
+   * @param columnNames Only column names in this collection will be returned
+   * @return The next row in this table (Column name -> Column value)
+   */
+  public Map<String, Object> getNextRow(Collection<String> columnNames) 
+    throws IOException
+  {
+    if(moveToNextRow()) {
+      return getCurrentRow(columnNames);
+    }
+    return null;
+  }
+
+  /**
+   * Moves to the next row as defined by this cursor.
+   * @return {@code true} if a valid next row was found, {@code false}
+   *         otherwise
+   */
+  public boolean moveToNextRow()
+    throws IOException
+  {
+    if(_currentRowId.equals(getLastRowId())) {
+      // already at end
+      return false;
+    }
+    
+    _rowState.reset();
+    _currentRowId = findNextRowId(_currentRowId);
+    return(!_currentRowId.equals(getLastRowId()));
   }
 
+  /**
+   * Moves to the first row (as defined by the cursor) where the given column
+   * has the given value.  This may be more efficient on some cursors than
+   * others.
+   * 
+   * @return {@code true} if a valid row was found with the given value,
+   *         {@code false} if no row was found (and the cursor is now pointing
+   *         past the end of the table)
+   */
+  public boolean moveToRow(Column column, Object value)
+    throws IOException
+  {
+    while(moveToNextRow()) {
+      if(ObjectUtils.equals(value, getCurrentRowSingleColumn(column))) {
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  /**
+   * Moves to the first row (as defined by the cursor) where the given columns
+   * have the given values.  This may be more efficient on some cursors than
+   * others.
+   * 
+   * @return {@code true} if a valid row was found with the given values,
+   *         {@code false} if no row was found (and the cursor is now pointing
+   *         past the end of the table)
+   */
+  public boolean moveToRow(Map<String,Object> row)
+    throws IOException
+  {
+    while(moveToNextRow()) {
+      if(ObjectUtils.equals(row, getCurrentRow(row.keySet()))) {
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  /**
+   * Skips as many rows as possible up to the given number of rows.
+   * @return the number of rows skipped.
+   */
+  public int skipRows(int numRows)
+    throws IOException
+  {
+    int numSkippedRows = 0;
+    while((numSkippedRows < numRows) && moveToNextRow()) {
+      ++numSkippedRows;
+    }
+    return numSkippedRows;
+  }
+
+  /**
+   * Returns the current row in this cursor (Column name -> Column value).
+   * @param columnNames Only column names in this collection will be returned
+   */
+  public Map<String, Object> getCurrentRow()
+    throws IOException
+  {
+    return getCurrentRow(null);
+  }
+
+  /**
+   * Returns the current row in this cursor (Column name -> Column value).
+   * @param columnNames Only column names in this collection will be returned
+   */
+  public Map<String, Object> getCurrentRow(Collection<String> columnNames)
+    throws IOException
+  {
+    return _table.getRow(_rowState, columnNames);
+  }
+
+  /**
+   * Returns the given column from the current row.
+   */
+  public Object getCurrentRowSingleColumn(Column column)
+    throws IOException
+  {
+    return _table.getRowSingleColumn(_rowState, column);
+  }
+  
+  /**
+   * Returns {@code true} if the row is marked as deleted, {@code false}
+   * otherwise.  This method will not modify the rowState (it only looks at
+   * the "main" row, which is where the deleted flag is located).
+   */
+  protected final boolean isCurrentRowDeleted()
+    throws IOException
+  {
+    ByteBuffer rowBuffer = _rowState.getFinalPage();
+    int rowNum = _rowState.getFinalRowNumber();
+    
+    // note, we don't use findRowStart here cause we need the unmasked value
+    return Table.isDeletedRow(
+        rowBuffer.getShort(Table.getRowStartOffset(rowNum, getFormat())));
+  }
+
+  /**
+   * Returns the row count for the current page.  If the page number is
+   * invalid or the page is not a DATA page, 0 is returned.
+   */
+  protected final int getRowsOnCurrentDataPage(ByteBuffer rowBuffer)
+    throws IOException
+  {
+    int rowsOnPage = 0;
+    if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) {
+      rowsOnPage = 
+        rowBuffer.getShort(getFormat().OFFSET_NUM_ROWS_ON_DATA_PAGE);
+    }
+    return rowsOnPage;
+  }
+
+  /**
+   * Finds the next non-deleted row after the given row as defined by this
+   * cursor and returns the id of the row.  If there are no more rows, the
+   * returned rowId should equal the value returned by {@link #getLastRowId}.
+   */
+  protected abstract RowId findNextRowId(RowId currentRowId)
+    throws IOException;
+  
+  /**
+   * Row iterator for this table, supports modification.
+   */
+  private final class RowIterator implements Iterator<Map<String, Object>>
+  {
+    private Collection<String> _columnNames;
+    private boolean _hasNext = false;
+    
+    private RowIterator(Collection<String> columnNames)
+    {
+      try {
+        reset();
+        _columnNames = columnNames;
+        _hasNext = moveToNextRow();
+      } catch(IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    public boolean hasNext() { return _hasNext; }
+
+    public void remove() {
+      try {
+        deleteCurrentRow();
+      } catch(IOException e) {
+        throw new IllegalStateException(e);
+      }        
+    }
+    
+    public Map<String, Object> next() {
+      if(!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      try {
+        Map<String, Object> rtn = getCurrentRow(_columnNames);
+        _hasNext = moveToNextRow();
+        return rtn;
+      } catch(IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+    
+  }
+
+  /**
+   * Simple un-indexed cursor.
+   */
+  private static class TableScanCursor extends Cursor
+  {
+    /** Iterator over the pages that this table owns */
+    private final UsageMap.PageIterator _ownedPagesIterator;
+    
+    private TableScanCursor(Table table) {
+      super(table, FIRST_ROW_ID, LAST_ROW_ID);
+      _ownedPagesIterator = table.getOwnedPagesIterator();
+    }
+    
+    @Override
+    public void reset() {
+      _ownedPagesIterator.reset();
+      super.reset();
+    }
+
+    /**
+     * Position the buffer at the next row in the table
+     * @return a ByteBuffer narrowed to the next row, or null if none
+     */
+    @Override
+    protected RowId findNextRowId(RowId currentRowId)
+      throws IOException
+    {
+
+      // prepare to read next row
+      _rowState.reset();
+      int currentPageNumber = currentRowId.getPageNumber();
+      int currentRowNumber = currentRowId.getRowNumber();
+
+      int rowsOnPage = getRowsOnCurrentDataPage(
+          _rowState.setRow(currentPageNumber, currentRowNumber));
+    
+      // loop until we find the next valid row or run out of pages
+      while(true) {
+
+        currentRowNumber++;
+        if(currentRowNumber < rowsOnPage) {
+          _rowState.setRow(currentPageNumber, currentRowNumber);
+        } else {
+
+          // load next page
+          currentRowNumber = INVALID_ROW_NUMBER;
+          currentPageNumber = _ownedPagesIterator.getNextPage();
+        
+          ByteBuffer rowBuffer = _rowState.setRow(
+              currentPageNumber, currentRowNumber);
+          if(rowBuffer == null) {
+            //No more owned pages.  No more rows.
+            return getLastRowId();
+          }          
+
+          // update row count
+          rowsOnPage = getRowsOnCurrentDataPage(rowBuffer);
+
+          // start again from the top
+          continue;
+        }
+
+        if(!isCurrentRowDeleted()) {
+          // we found a non-deleted row, return it
+          return new RowId(currentPageNumber, currentRowNumber);
+        }
+      }
+    }
+    
+  }
+  
 }
index 3dba5ef0a9c67d242491782ec325deea6620c948..4e578137e6c317dc5b675d01ce43c241eb24e2b6 100644 (file)
@@ -519,14 +519,14 @@ public class Index implements Comparable<Index> {
    * @param pageNumber Page number on which the row is stored
    * @param rowNumber Row number at which the row is stored
    */
-  public void addRow(Object[] row, int pageNumber, byte rowNumber)
+  public void addRow(Object[] row, RowId rowId)
     throws IOException
   {
     // make sure we've parsed the entries
     initialize();
 
     ++_rowCount;
-    _entries.add(new Entry(row, pageNumber, rowNumber));
+    _entries.add(new Entry(row, rowId));
   }
 
   /**
@@ -538,22 +538,21 @@ public class Index implements Comparable<Index> {
    * @param pageNumber Page number on which the row is removed
    * @param rowNumber Row number at which the row is removed
    */
-  public void deleteRow(Object[] row, int pageNumber, byte rowNumber)
+  public void deleteRow(Object[] row, RowId rowId)
     throws IOException
   {
     // make sure we've parsed the entries
     initialize();
 
     --_rowCount;
-    Entry oldEntry = new Entry(row, pageNumber, rowNumber);
+    Entry oldEntry = new Entry(row, rowId);
     if(!_entries.remove(oldEntry)) {
       // the caller may have only read some of the row data, if this is the
       // case, just search for the page/row numbers
       boolean removed = false;
       for(Iterator<Entry> iter = _entries.iterator(); iter.hasNext(); ) {
         Entry entry = iter.next();
-        if((entry.getPage() == pageNumber) &&
-           (entry.getRow() == rowNumber)) {
+        if(entry.getRowId().equals(rowId)) {
           iter.remove();
           removed = true;
           break;
@@ -718,10 +717,8 @@ public class Index implements Comparable<Index> {
    */
   private class Entry implements Comparable<Entry> {
     
-    /** Page number on which the row is stored */
-    private int _page;
-    /** Row number at which the row is stored */
-    private byte _row;
+    /** page/row on which this row is stored */
+    private final RowId _rowId;
     /** Columns that are indexed */
     private List<EntryColumn> _entryColumns = new ArrayList<EntryColumn>();
     
@@ -731,10 +728,9 @@ public class Index implements Comparable<Index> {
      * @param page Page number on which the row is stored
      * @param rowNumber Row number at which the row is stored
      */
-    public Entry(Object[] values, int page, byte rowNumber) throws IOException
+    public Entry(Object[] values, RowId rowId) throws IOException
     {
-      _page = page;
-      _row = rowNumber;
+      _rowId = rowId;
       for(Map.Entry<Column, Byte> entry : _columns.entrySet()) {
         Column col = entry.getKey();
         Byte flags = entry.getValue();
@@ -755,8 +751,9 @@ public class Index implements Comparable<Index> {
         _entryColumns.add(newEntryColumn(col)
                           .initFromBuffer(buffer, flags, valuePrefix));
       }
-      _page = ByteUtil.get3ByteInt(buffer, ByteOrder.BIG_ENDIAN);
-      _row = buffer.get();
+      int page = ByteUtil.get3ByteInt(buffer, ByteOrder.BIG_ENDIAN);
+      int row = buffer.get();
+      _rowId = new RowId(page, row);
     }
 
     /**
@@ -773,13 +770,17 @@ public class Index implements Comparable<Index> {
     public List<EntryColumn> getEntryColumns() {
       return _entryColumns;
     }
+
+    public RowId getRowId() {
+      return _rowId;
+    }
     
     public int getPage() {
-      return _page;
+      return getRowId().getPageNumber();
     }
     
     public byte getRow() {
-      return _row;
+      return (byte)getRowId().getRowNumber();
     }
     
     public int size() {
@@ -797,15 +798,16 @@ public class Index implements Comparable<Index> {
       for(EntryColumn entryCol : _entryColumns) {
         entryCol.write(buffer);
       }
-      buffer.put((byte) (_page >>> 16));
-      buffer.put((byte) (_page >>> 8));
-      buffer.put((byte) _page);
-      buffer.put(_row);
+      int page = getPage();
+      buffer.put((byte) (page >>> 16));
+      buffer.put((byte) (page >>> 8));
+      buffer.put((byte) page);
+      buffer.put(getRow());
     }
     
     @Override
     public String toString() {
-      return ("Page = " + _page + ", Row = " + _row + ", Columns = " + _entryColumns + "\n");
+      return ("RowId = " + _rowId + ", Columns = " + _entryColumns + "\n");
     }
     
     public int compareTo(Entry other) {
@@ -826,8 +828,7 @@ public class Index implements Comparable<Index> {
           return i;
         }
       }
-      return new CompareToBuilder().append(_page, other.getPage())
-          .append(_row, other.getRow()).toComparison();
+      return _rowId.compareTo(other.getRowId());
     }
     
 
index 6ef7bc3a8a247c86c02d26b7e7d3fd4f41002876..a125759adb4371aa0de44834e583e7d32aec307a 100644 (file)
@@ -12,6 +12,8 @@ import org.apache.commons.lang.builder.CompareToBuilder;
  */
 public class RowId implements Comparable<RowId>
 {
+  public static final int INVALID_ROW_NUMBER = -1;
+
   private final int _pageNumber;
   private final int _rowNumber;
   
@@ -32,6 +34,10 @@ public class RowId implements Comparable<RowId>
     return _rowNumber;
   }
 
+  public boolean isValidRow() {
+    return(getRowNumber() != INVALID_ROW_NUMBER);
+  }
+  
   public int compareTo(RowId other) {
       return new CompareToBuilder()
         .append(getPageNumber(), other.getPageNumber())
index 08d7a2be035bcdfe5adaa19fb7d08b9f3b8569f0..dc064cba5f98a9c7e37842eb7d05737fed834be9 100644 (file)
@@ -45,6 +45,9 @@ import org.apache.commons.logging.LogFactory;
 
 /**
  * A single database table
+ * <p>
+ * Is not thread-safe.
+ * 
  * @author Tim McCune
  */
 public class Table
@@ -53,8 +56,6 @@ public class Table
   
   private static final Log LOG = LogFactory.getLog(Table.class);
 
-  private static final int INVALID_ROW_NUMBER = -1;
-  
   private static final short OFFSET_MASK = (short)0x1FFF;
 
   private static final short DELETED_ROW_MASK = (short)0x8000;
@@ -79,8 +80,6 @@ public class Table
 
   /** owning database */
   private final Database _database;
-  /** State used for reading the table rows */
-  private RowState _rowState;
   /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */
   private byte _tableType;
   /** Number of indexes on the table */
@@ -93,8 +92,6 @@ public class Table
   private int _lastAutoNumber;
   /** page number of the definition of this table */
   private final int _tableDefPageNumber;
-  /** Number of rows left to be read on the current page */
-  private short _rowsLeftOnPage = 0;
   /** max Number of columns in the table (includes previous deletions) */
   private short _maxColumnCount;
   /** max Number of variable columns in the table */
@@ -109,10 +106,14 @@ public class Table
   private final String _name;
   /** Usage map of pages that this table owns */
   private UsageMap _ownedPages;
-  /** Iterator over the pages that this table owns */
-  private UsageMap.PageIterator _ownedPagesIterator;
   /** Usage map of pages that this table owns with free space on them */
   private UsageMap _freeSpacePages;
+  /** modification count for the table, keeps row-states up-to-date */
+  private int _modCount;
+  
+  /** common cursor for iterating through the table, kept here for historic
+      reasons */
+  private Cursor _cursor;
   
   /**
    * Only used by unit tests
@@ -159,7 +160,8 @@ public class Table
     readTableDefinition(tableBuffer);
     tableBuffer = null;
 
-    _rowState = new RowState(true, _maxColumnCount);
+    // setup common cursor
+    _cursor = Cursor.createCursor(this);
   }
 
   /**
@@ -184,6 +186,18 @@ public class Table
   protected int getTableDefPageNumber() {
     return _tableDefPageNumber;
   }
+
+  public RowState createRowState() {
+    return new RowState(true);
+  }
+
+  protected UsageMap.PageIterator getOwnedPagesIterator() {
+    return _ownedPages.iterator();
+  }
+  
+  protected UsageMap.PageIterator getOwnedPagesReverseIterator() {
+    return _ownedPages.reverseIterator();
+  }
   
   /**
    * @return All of the columns in this table (unmodifiable List)
@@ -191,7 +205,20 @@ public class Table
   public List<Column> getColumns() {
     return Collections.unmodifiableList(_columns);
   }
-  
+
+  /**
+   * @return the column with the given name
+   */
+  public Column getColumn(String name) {
+    for(Column column : _columns) {
+      if(column.getName().equals(name)) {
+        return column;
+      }
+    }
+    throw new IllegalArgumentException("Column with name " + name +
+                                       " does not exist in this table");
+  }
+    
   /**
    * Only called by unit tests
    */
@@ -234,39 +261,40 @@ public class Table
    * table
    */
   public void reset() {
-    _rowsLeftOnPage = 0;
-    _ownedPagesIterator.reset();
-    _rowState.reset();
+    _cursor.reset();
   }
   
   /**
    * Delete the current row (retrieved by a call to {@link #getNextRow}).
    */
   public void deleteCurrentRow() throws IOException {
-    if (_rowState.getRowNumber() == INVALID_ROW_NUMBER) {
-      throw new IllegalStateException("Must call getNextRow first");
+    _cursor.deleteCurrentRow();
+  }
+  
+  /**
+   * Delete the current row (retrieved by a call to {@link #getNextRow}).
+   */
+  public void deleteRow(RowState rowState, RowId rowId) throws IOException {
+    if (!rowId.isValidRow()) {
+      throw new IllegalStateException("Given row is not valid: " + rowId);
     }
 
-    // FIXME, want to make this static, but writeDataPage is not static, also, this may screw up other rowstates...
-
     // see if row was already deleted
-    if(_rowState.isDeleted()) {
+    if(rowState.isDeleted()) {
       throw new IllegalStateException("Deleting already deleted row");
     }
     
     // delete flag always gets set in the "root" page (even if overflow row)
-    ByteBuffer rowBuffer = _rowState.getPage(getPageChannel());
-    int pageNumber = _rowState.getPageNumber();
-    int rowNumber = _rowState.getRowNumber();
-    int rowIndex = getRowStartOffset(rowNumber, getFormat());
+    ByteBuffer rowBuffer = rowState.getPage();
+    int rowIndex = getRowStartOffset(rowId.getRowNumber(), getFormat());
     rowBuffer.putShort(rowIndex, (short)(rowBuffer.getShort(rowIndex)
                                       | DELETED_ROW_MASK | OVERFLOW_ROW_MASK));
-    writeDataPage(rowBuffer, pageNumber);
-    _rowState.setDeleted(true);
+    writeDataPage(rowBuffer, rowId.getPageNumber());
+    rowState.setDeleted(true);
 
     // update the indexes
     for(Index index : _indexes) {
-      index.deleteRow(_rowState.getRowValues(), pageNumber, (byte)rowNumber);
+      index.deleteRow(rowState.getRowValues(), rowId);
     }
     
     // make sure table def gets updated
@@ -288,30 +316,23 @@ public class Table
   public Map<String, Object> getNextRow(Collection<String> columnNames) 
     throws IOException
   {
-    // find next row
-    ByteBuffer rowBuffer = positionAtNextRow();
-    if (rowBuffer == null) {
-      return null;
-    }
-
-    return getRow(_rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
-                  columnNames);
+    return _cursor.getNextRow(columnNames);
   }
   
   /**
    * Reads a single column from the given row.
    */
-  public static Object getRowSingleColumn(
-      RowState rowState, int pageNumber, int rowNum,
-      Column column, PageChannel pageChannel, JetFormat format)
+  public Object getRowSingleColumn(RowState rowState, Column column)
     throws IOException
   {
-    // set row state to correct page
-    rowState.reset();
-    rowState.setPage(pageChannel, pageNumber, rowNum);
-
+    if(this != column.getTable()) {
+      throw new IllegalArgumentException(
+          "Given column " + column + " is not from this table");
+    }
+    
     // position at correct row
-    ByteBuffer rowBuffer = positionAtRow(rowState, pageChannel, format);
+    ByteBuffer rowBuffer = positionAtRowData(rowState, getPageChannel(),
+                                             getFormat());
     if(rowBuffer == null) {
       // note, row state will indicate that row was deleted
       return null;
@@ -319,29 +340,24 @@ public class Table
     
     return getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column);
   }
+
   /**
    * Reads some columns from the given row.
    * @param columnNames Only column names in this collection will be returned
    */
-  public static Map<String, Object> getRow(
-      RowState rowState, int pageNumber, int rowNum,
-      Collection<Column> columns, PageChannel pageChannel, JetFormat format,
-      Collection<String> columnNames)
+  public Map<String, Object> getRow(
+      RowState rowState, Collection<String> columnNames)
     throws IOException
   {
-    // set row state to correct page
-    rowState.reset();
-    rowState.setPage(pageChannel, pageNumber, rowNum);
-
     // position at correct row
-    ByteBuffer rowBuffer = positionAtRow(rowState, pageChannel, format);
+    ByteBuffer rowBuffer = positionAtRowData(rowState, getPageChannel(),
+                                             getFormat());
     if(rowBuffer == null) {
       // note, row state will indicate that row was deleted
       return null;
     }
 
-    return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), columns,
+    return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
                   columnNames);
   }
 
@@ -358,6 +374,7 @@ public class Table
   {
     Map<String, Object> rtn = new LinkedHashMap<String, Object>(
         columns.size());
+    Object[] rowValues = rowState.getRowValues();
     for(Column column : columns) {
       Object value = null;
       if((columnNames == null) || (columnNames.contains(column.getName()))) {
@@ -370,7 +387,7 @@ public class Table
       // deletion.  note, most of the returned values are immutable, except
       // for binary data (returned as byte[]), but binary data shouldn't be
       // indexed anyway.
-      rowState._rowValues[column.getColumnNumber()] = value;
+      rowValues[column.getColumnNumber()] = value;
     }
     return rtn;
     
@@ -447,54 +464,6 @@ public class Table
     return nullMask;
   }
                                         
-  /**
-   * Position the buffer at the next row in the table
-   * @return a ByteBuffer narrowed to the next row, or null if none
-   */
-  private ByteBuffer positionAtNextRow() throws IOException {
-
-    // prepare to read next row
-    _rowState.reset();
-    
-    // loop until we find the next valid row or run out of pages
-    while(true) {
-
-      if (_rowsLeftOnPage == 0) {
-
-        // load next page
-        ByteBuffer rowBuffer = _rowState.setPage(
-            getPageChannel(), _ownedPagesIterator.getNextPage(),
-            INVALID_ROW_NUMBER);
-        if(rowBuffer == null) {
-          //No more owned pages.  No more rows.
-          return null;
-        }          
-        if(rowBuffer.get() != PageTypes.DATA) {
-          //Only interested in data pages
-          continue;
-        }
-
-        _rowsLeftOnPage = rowBuffer.getShort(getFormat().OFFSET_NUM_ROWS_ON_DATA_PAGE);
-        if(_rowsLeftOnPage == 0) {
-          // no rows on this page?
-          continue;
-        }
-        
-      }
-
-      // move to next row
-      _rowState.nextRowInPage();
-      _rowsLeftOnPage--;
-
-      ByteBuffer rowBuffer =
-        positionAtRow(_rowState, getPageChannel(), getFormat());
-      if(rowBuffer != null) {
-        // we found a non-deleted row, return it
-        return rowBuffer;
-      }
-    }
-  }
-
   /**
    * Sets the position and limit in a new buffer using the given rowState
    * according to the given row number and row end, following overflow row
@@ -503,16 +472,13 @@ public class Table
    * @return a ByteBuffer narrowed to the actual row data, or null if row was
    *         deleted
    */
-  private static ByteBuffer positionAtRow(RowState rowState, 
-                                          PageChannel pageChannel,
-                                          JetFormat format)
+  private static ByteBuffer positionAtRowData(RowState rowState, 
+                                              PageChannel pageChannel,
+                                              JetFormat format)
     throws IOException
   {
-    // reset row state
-    rowState.resetDuringSearch();
-    
     while(true) {
-      ByteBuffer rowBuffer = rowState.getFinalPage(pageChannel);
+      ByteBuffer rowBuffer = rowState.getFinalPage();
       int rowNum = rowState.getFinalRowNumber();
     
       // note, we don't use findRowStart here cause we need the unmasked value
@@ -555,8 +521,7 @@ public class Table
         // page/row
         int overflowRowNum = rowBuffer.get(rowStart);
         int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1);
-        rowState.setOverflowPage(pageChannel, overflowPageNum,
-                                 overflowRowNum);
+        rowState.setOverflowRow(overflowPageNum, overflowRowNum);
       
       } else {
 
@@ -589,7 +554,7 @@ public class Table
    */
   public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
   {
-    return new RowIterator(columnNames);
+    return _cursor.iterator(columnNames);
   }
 
   /**
@@ -888,7 +853,6 @@ public class Table
     byte rowNum = tableBuffer.get(getFormat().OFFSET_OWNED_PAGES);
     int pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_OWNED_PAGES + 1);
     _ownedPages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
-    _ownedPagesIterator = _ownedPages.iterator();
     rowNum = tableBuffer.get(getFormat().OFFSET_FREE_SPACE_PAGES);
     pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES + 1);
     _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
@@ -998,8 +962,9 @@ public class Table
     // write the page data
     getPageChannel().writePage(pageBuffer, pageNumber);
 
-    // if the overflow buffer is this page, invalidate it
-    _rowState.possiblyInvalidate(pageNumber, pageBuffer);
+    // update modification count so any active RowStates can keep themselves
+    // up-to-date
+    ++_modCount;
   }
   
   /**
@@ -1070,7 +1035,7 @@ public class Table
 
       // update the indexes
       for(Index index : _indexes) {
-        index.addRow(rows.get(i), pageNumber, (byte)rowNum);
+        index.addRow(rows.get(i), new RowId(pageNumber, rowNum));
       }
     }
     writeDataPage(dataPage, pageNumber);
@@ -1350,12 +1315,24 @@ public class Table
 
     return rowCount;
   }
+
+  public static boolean isDeletedRow(short rowStart) {
+    return ((rowStart & DELETED_ROW_MASK) != 0);
+  }
+  
+  public static boolean isOverflowRow(short rowStart) {
+    return ((rowStart & OVERFLOW_ROW_MASK) != 0);
+  }
+
+  public static short cleanRowStart(short rowStart) {
+    return (short)(rowStart & OFFSET_MASK);
+  }
   
   public static short findRowStart(ByteBuffer buffer, int rowNum,
                                    JetFormat format)
   {
-    return (short)(buffer.getShort(getRowStartOffset(rowNum, format))
-                   & OFFSET_MASK);
+    return cleanRowStart(
+        buffer.getShort(getRowStartOffset(rowNum, format)));
   }
 
   public static int getRowStartOffset(int rowNum, JetFormat format)
@@ -1368,8 +1345,8 @@ public class Table
   {
     return (short)((rowNum == 0) ?
                    format.PAGE_SIZE :
-                   (buffer.getShort(getRowEndOffset(rowNum, format))
-                    & OFFSET_MASK));
+                   cleanRowStart(
+                       buffer.getShort(getRowEndOffset(rowNum, format))));
   }
 
   public static int getRowEndOffset(int rowNum, JetFormat format)
@@ -1429,11 +1406,11 @@ public class Table
   /**
    * Maintains the state of reading a row of data.
    */
-  public static class RowState
+  public class RowState
   {
     /** Buffer used for reading the row data pages */
     private TempPageHolder _rowBufferH;
-    /** row number of the main row */
+    /** the row number on the main page */
     private int _rowNumber;
     /** true if the current row is an overflow row */
     private boolean _overflow;
@@ -1450,10 +1427,13 @@ public class Table
     private int _finalRowNumber;
     /** values read from the last row */
     private Object[] _rowValues;
+    /** last modification count seen on the table */
+    private int _lastModCount;
     
-    public RowState(boolean hardRowBuffer, int colCount) {
+    private RowState(boolean hardRowBuffer) {
       _rowBufferH = TempPageHolder.newHolder(hardRowBuffer);
-      _rowValues = new Object[colCount];
+      _rowValues = new Object[Table.this._maxColumnCount];
+      _lastModCount = Table.this._modCount;
     }
     
     public void reset() {
@@ -1462,40 +1442,32 @@ public class Table
     }
 
     public void resetDuringSearch() {
-      resetNewPage();
-      resetNewRow();
-    }
-    
-    private void resetNewRow() {
-      _finalRowNumber = INVALID_ROW_NUMBER;
-    }      
-    
-    private void resetNewPage() {
+      _finalRowNumber = RowId.INVALID_ROW_NUMBER;
       _finalRowBuffer = null;
       _deleted = false;
       _overflow = false;
-    }      
-    
-    public int getPageNumber() {
-      return _rowBufferH.getPageNumber();
     }
 
-    public int getRowNumber() {
-      return _rowNumber;
+    private void checkForModification() {
+      if(Table.this._modCount != _lastModCount) {
+        _rowBufferH.invalidate();
+        _overflowRowBufferH.invalidate();
+        _lastModCount = Table.this._modCount;
+      }
     }
     
-    public ByteBuffer getFinalPage(PageChannel pageChannel)
+    public ByteBuffer getFinalPage()
       throws IOException
     {
       if(_finalRowBuffer == null) {
         // (re)load current page
-        _finalRowBuffer = getPage(pageChannel);
+        _finalRowBuffer = getPage();
       }
       return _finalRowBuffer;
     }
 
     public int getFinalRowNumber() {
-      if(_finalRowNumber == INVALID_ROW_NUMBER) {
+      if(_finalRowNumber == RowId.INVALID_ROW_NUMBER) {
         _finalRowNumber = _rowNumber;
       }
       return _finalRowNumber;
@@ -1525,43 +1497,35 @@ public class Table
                                              modifiedBuffer);
     }
     
-    public ByteBuffer getPage(PageChannel pageChannel)
+    public ByteBuffer getPage()
       throws IOException
     {
-      return _rowBufferH.getPage(pageChannel);
-    }
-
-    public void nextRowInPage() {
-      setRowNumber(_rowNumber + 1);
+      checkForModification();
+      return _rowBufferH.getPage(getPageChannel());
     }
 
-    public void setRowNumber(int rowNumber) {
-      resetNewRow();
-      _rowNumber = rowNumber;
-      _finalRowNumber = rowNumber;
-    }
-    
-    public ByteBuffer setPage(PageChannel pageChannel, int pageNumber,
-                              int rowNumber)
+    public ByteBuffer setRow(int pageNumber, int rowNumber)
       throws IOException
     {
-      resetNewPage();
-      setRowNumber(rowNumber);
+      resetDuringSearch();
+      checkForModification();
+      _rowNumber = rowNumber;
+      _finalRowNumber = rowNumber;
       if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) {
-        _rowBufferH.invalidate();
         return null;
       }
-      _finalRowBuffer = _rowBufferH.setPage(pageChannel, pageNumber);
+      _finalRowBuffer = _rowBufferH.setPage(getPageChannel(), pageNumber);
       return _finalRowBuffer;
     }
 
-    public ByteBuffer setOverflowPage(PageChannel pageChannel, int pageNumber,
-                                      int rowNumber)
+    public ByteBuffer setOverflowRow(int pageNumber, int rowNumber)
       throws IOException
     {
+      checkForModification();
       _overflow = true;
-      _finalRowBuffer = _overflowRowBufferH.setPage(pageChannel, pageNumber);
       _finalRowNumber = rowNumber;
+      _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(),
+                                                    pageNumber);
       return _finalRowBuffer;
     }
 
diff --git a/test/src/java/com/healthmarketscience/jackcess/CursorTest.java b/test/src/java/com/healthmarketscience/jackcess/CursorTest.java
new file mode 100644 (file)
index 0000000..9e6c187
--- /dev/null
@@ -0,0 +1,118 @@
+// Copyright (c) 2007 Health Market Science, Inc.
+
+package com.healthmarketscience.jackcess;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
+
+/**
+ * @author James Ahlborn
+ */
+public class CursorTest extends TestCase {
+
+  public CursorTest(String name) throws Exception {
+    super(name);
+  }
+
+  private static List<Map<String,Object>> createTestTableData()
+    throws Exception
+  {
+    List<Map<String,Object>> expectedRows =
+      new ArrayList<Map<String,Object>>();
+    for(int i = 0; i < 10; ++i) {
+      expectedRows.add(createExpectedRow("id", i, "value", "data" + i));
+    }
+    return expectedRows;
+  }
+  
+  private static Database createTestTable() throws Exception {
+    Database db = create();
+
+    List<Column> columns = new ArrayList<Column>();
+    Column col = new Column();
+    col.setName("id");
+    col.setType(DataType.LONG);
+    columns.add(col);
+    col = new Column();
+    col.setName("value");
+    col.setType(DataType.TEXT);
+    columns.add(col);
+    db.createTable("test", columns);
+
+    Table table = db.getTable("test");
+    for(int i = 0; i < 10; ++i) {
+      table.addRow(i, "data" + i);
+    }
+
+    return db;
+  }
+  
+  public void testSimple() throws Exception {
+    Database db = createTestTable();
+
+    Table table = db.getTable("test");
+    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");
+    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());
+    assertEquals(3, cursor.skipRows(3));
+    while(cursor.moveToNextRow()) {
+      foundRows.add(cursor.getCurrentRow());
+    }
+    assertEquals(expectedRows, foundRows);
+
+    assertEquals(0, cursor.skipRows(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);
+
+    assertTrue(cursor.moveToRow(table.getColumn("id"), 3));
+    assertEquals(createExpectedRow("id", 3,
+                                   "value", "data" + 3),
+                 cursor.getCurrentRow());
+
+    assertTrue(cursor.moveToRow(createExpectedRow(
+                                    "id", 6,
+                                    "value", "data" + 6)));
+    assertEquals(createExpectedRow("id", 6,
+                                   "value", "data" + 6),
+                 cursor.getCurrentRow());
+    
+    db.close();
+  }
+
+  
+}
index 5235ef20c1622cd287879b6b08762282763c8b5f..a962c46a27b58ec01de336c1ff547ec1e4c7fa93 100644 (file)
@@ -821,7 +821,7 @@ public class DatabaseTest extends TestCase {
   
   static int countRows(Table table) throws Exception {
     int rtn = 0;
-    for(Map<String, Object> row : table) {
+    for(Map<String, Object> row : Cursor.createCursor(table)) {
       rtn++;
     }
     return rtn;
@@ -831,7 +831,7 @@ public class DatabaseTest extends TestCase {
   {
     List<Map<String, Object>> foundTable =
       new ArrayList<Map<String, Object>>();
-    for(Map<String, Object> row : table) {
+    for(Map<String, Object> row : Cursor.createCursor(table)) {
       foundTable.add(row);
     }
     assertEquals(expectedTable, foundTable);
@@ -873,7 +873,7 @@ public class DatabaseTest extends TestCase {
       colNames.add(col.getName());
     }
     writer.println("COLUMNS: " + colNames);
-    for(Map<String, Object> row : table) {
+    for(Map<String, Object> row : Cursor.createCursor(table)) {
 
       // make byte[] printable
       for(Map.Entry<String, Object> entry : row.entrySet()) {