</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.
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);
+ }
+ }
+ }
+
+ }
+
}
* @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));
}
/**
* @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;
*/
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>();
* @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();
_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);
}
/**
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() {
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) {
return i;
}
}
- return new CompareToBuilder().append(_page, other.getPage())
- .append(_row, other.getRow()).toComparison();
+ return _rowId.compareTo(other.getRowId());
}
*/
public class RowId implements Comparable<RowId>
{
+ public static final int INVALID_ROW_NUMBER = -1;
+
private final int _pageNumber;
private final int _rowNumber;
return _rowNumber;
}
+ public boolean isValidRow() {
+ return(getRowNumber() != INVALID_ROW_NUMBER);
+ }
+
public int compareTo(RowId other) {
return new CompareToBuilder()
.append(getPageNumber(), other.getPageNumber())
/**
* A single database table
+ * <p>
+ * Is not thread-safe.
+ *
* @author Tim McCune
*/
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;
/** 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 */
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 */
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
readTableDefinition(tableBuffer);
tableBuffer = null;
- _rowState = new RowState(true, _maxColumnCount);
+ // setup common cursor
+ _cursor = Cursor.createCursor(this);
}
/**
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)
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
*/
* 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
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;
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);
}
{
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()))) {
// 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;
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
* @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
// page/row
int overflowRowNum = rowBuffer.get(rowStart);
int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1);
- rowState.setOverflowPage(pageChannel, overflowPageNum,
- overflowRowNum);
+ rowState.setOverflowRow(overflowPageNum, overflowRowNum);
} else {
*/
public Iterator<Map<String, Object>> iterator(Collection<String> columnNames)
{
- return new RowIterator(columnNames);
+ return _cursor.iterator(columnNames);
}
/**
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);
// 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;
}
/**
// 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);
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)
{
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)
/**
* 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;
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() {
}
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;
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;
}
--- /dev/null
+// 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();
+ }
+
+
+}
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;
{
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);
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()) {