private final RowId _firstRowId;
/** the last (exclusive) row id for this iterator */
private final RowId _lastRowId;
+ /** the previous row */
+ private RowId _previousRowId;
/** the current row */
private RowId _currentRowId;
_firstRowId = firstRowId;
_lastRowId = lastRowId;
_currentRowId = firstRowId;
+ _previousRowId = firstRowId;
}
/**
* Returns {@code true} if the cursor is currently positioned before the
* first row, {@code false} otherwise.
*/
- public boolean isBeforeFirst() {
- return getFirstRowId().equals(_currentRowId);
+ public boolean isBeforeFirst()
+ throws IOException
+ {
+ if(getFirstRowId().equals(_currentRowId)) {
+ return !recheckPosition(false);
+ }
+ return false;
}
/**
* Returns {@code true} if the cursor is currently positioned after the
* last row, {@code false} otherwise.
*/
- public boolean isAfterLast() {
- return getLastRowId().equals(_currentRowId);
+ public boolean isAfterLast()
+ throws IOException
+ {
+ if(getLastRowId().equals(_currentRowId)) {
+ return !recheckPosition(true);
+ }
+ return false;
}
/**
throws IOException
{
// we need to ensure that the "deleted" flag has been read for this row
+ // (or re-read if the table has been recently modified)
Table.positionAtRowData(_rowState, _currentRowId);
return _rowState.isDeleted();
}
*/
protected void reset(boolean moveForward) {
_currentRowId = getDirHandler(moveForward).getBeginningRowId();
+ _previousRowId = _currentRowId;
_rowState.reset();
}
{
RowId endRowId = getDirHandler(moveForward).getEndRowId();
if(_currentRowId.equals(endRowId)) {
- // already at end
+ // already at end, make sure nothing has changed
+ return recheckPosition(moveForward);
+ }
+
+ return moveToAnotherRowImpl(moveForward);
+ }
+
+ /**
+ * Restores the current position to the previous position.
+ */
+ protected void restorePreviousPosition()
+ throws IOException
+ {
+ // essentially swap current and previous
+ RowId tmp = _previousRowId;
+ _previousRowId = _currentRowId;
+ _currentRowId = tmp;
+ }
+
+ /**
+ * Rechecks the current position if the underlying data structures have been
+ * modified.
+ * @return {@code true} if the cursor ended up in a new position,
+ * {@code false} otherwise.
+ */
+ private boolean recheckPosition(boolean moveForward)
+ throws IOException
+ {
+ if(isUpToDate()) {
+ // nothing has changed
return false;
}
-
+
+ // move the cursor back to the previous position
+ restorePreviousPosition();
+ return moveToAnotherRowImpl(moveForward);
+ }
+
+ /**
+ * Does the grunt work of moving the cursor to another position in the given
+ * direction.
+ */
+ private boolean moveToAnotherRowImpl(boolean moveForward)
+ throws IOException
+ {
_rowState.reset();
+ _previousRowId = _currentRowId;
_currentRowId = findAnotherRowId(_rowState, _currentRowId, moveForward);
Table.positionAtRowHeader(_rowState, _currentRowId);
- return(!_currentRowId.equals(endRowId));
+ return(!_currentRowId.equals(getDirHandler(moveForward).getEndRowId()));
}
-
+
/**
* 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
/**
* Returns the given column from the current row.
*/
+ @SuppressWarnings("foo")
public Object getCurrentRowValue(Column column)
throws IOException
{
return _table.getRowValue(_rowState, _currentRowId, column);
}
+ /**
+ * Returns {@code true} if this cursor is up-to-date with respect to the
+ * relevant table and related table objects, {@code false} otherwise.
+ */
+ protected boolean isUpToDate() {
+ return _rowState.isUpToDate();
+ }
+
/**
* Finds the next non-deleted row after the given row (as defined by this
* cursor) and returns the id of the row, where "next" may be backwards if
*/
private static class TableScanCursor extends Cursor
{
- /** ScanDirHandler for forward iteration */
+ /** ScanDirHandler for forward traversal */
private final ScanDirHandler _forwardDirHandler =
new ForwardScanDirHandler();
- /** ScanDirHandler for backward iteration */
+ /** ScanDirHandler for backward traversal */
private final ScanDirHandler _reverseDirHandler =
new ReverseScanDirHandler();
- /** Iterator over the pages that this table owns */
- private final UsageMap.PageIterator _ownedPagesIterator;
+ /** Cursor over the pages that this table owns */
+ private final UsageMap.PageCursor _ownedPagesCursor;
private TableScanCursor(Table table) {
super(table, RowId.FIRST_ROW_ID, RowId.LAST_ROW_ID);
- _ownedPagesIterator = table.getOwnedPagesIterator();
+ _ownedPagesCursor = table.getOwnedPagesCursor();
}
@Override
protected ScanDirHandler getDirHandler(boolean moveForward) {
return (moveForward ? _forwardDirHandler : _reverseDirHandler);
}
+
+ @Override
+ protected boolean isUpToDate() {
+ return(super.isUpToDate() && _ownedPagesCursor.isUpToDate());
+ }
@Override
protected void reset(boolean moveForward) {
- _ownedPagesIterator.reset(moveForward);
+ _ownedPagesCursor.reset(moveForward);
super.reset(moveForward);
}
+ @Override
+ protected void restorePreviousPosition()
+ throws IOException
+ {
+ super.restorePreviousPosition();
+ _ownedPagesCursor.setCurrentPage(getCurrentRowId().getPageNumber());
+ }
+
/**
* Position the buffer at the next row in the table
* @return a ByteBuffer narrowed to the next row, or null if none
// figure out how many rows are left on this page so we can find the
// next row
Table.positionAtRowHeader(rowState, currentRowId);
- int rowInc = handler.getRowIncrement();
int currentRowNumber = currentRowId.getRowNumber();
// loop until we find the next valid row or run out of pages
while(true) {
- currentRowNumber += rowInc;
+ currentRowNumber = handler.getAnotherRowNumber(currentRowNumber);
currentRowId = new RowId(currentRowId.getPageNumber(),
currentRowNumber);
- ByteBuffer rowBuffer =
- Table.positionAtRowHeader(rowState, currentRowId);
+ Table.positionAtRowHeader(rowState, currentRowId);
if(!rowState.isValid()) {
* cursor logic from value storage.
*/
private abstract class ScanDirHandler extends DirHandler {
- public abstract int getRowIncrement();
+ public abstract int getAnotherRowNumber(int curRowNumber);
public abstract int getAnotherPageNumber();
public abstract int getInitialRowNumber(int rowsOnPage);
}
* Handles moving the table scan cursor forward.
*/
private final class ForwardScanDirHandler extends ScanDirHandler {
+ @Override
public RowId getBeginningRowId() {
return getFirstRowId();
}
+ @Override
public RowId getEndRowId() {
return getLastRowId();
}
- public int getRowIncrement() {
- return 1;
+ @Override
+ public int getAnotherRowNumber(int curRowNumber) {
+ return curRowNumber + 1;
}
+ @Override
public int getAnotherPageNumber() {
- return _ownedPagesIterator.getNextPage();
+ return _ownedPagesCursor.getNextPage();
}
+ @Override
public int getInitialRowNumber(int rowsOnPage) {
return -1;
}
* Handles moving the table scan cursor backward.
*/
private final class ReverseScanDirHandler extends ScanDirHandler {
+ @Override
public RowId getBeginningRowId() {
return getLastRowId();
}
+ @Override
public RowId getEndRowId() {
return getFirstRowId();
}
- public int getRowIncrement() {
- return -1;
+ @Override
+ public int getAnotherRowNumber(int curRowNumber) {
+ return curRowNumber - 1;
}
+ @Override
public int getAnotherPageNumber() {
- return _ownedPagesIterator.getPreviousPage();
+ return _ownedPagesCursor.getPreviousPage();
}
+ @Override
public int getInitialRowNumber(int rowsOnPage) {
return rowsOnPage;
}
private EntryIterator() {
reset();
}
-
+
+ public boolean isUpToDate() {
+ return(Index.this._modCount == _lastModCount);
+ }
+
public void reset() {
beforeFirst();
}
}
private void resyncIndex() {
- if(Index.this._modCount != _lastModCount) {
+ if(!isUpToDate()) {
if(_nextEntryIdx == 0) {
// we were at the beginning of the list
_nextEntry = _entries.get(_nextEntryIdx);
* Returns {@code true} if this rowId potentially represents an actual row
* of data, {@code false} otherwise.
*/
- public boolean isValidRowId() {
+ public boolean isValid() {
return((getRowNumber() >= 0) && (getPageNumber() >= 0));
}
return new RowState(true);
}
- protected UsageMap.PageIterator getOwnedPagesIterator() {
- return _ownedPages.iterator();
+ protected UsageMap.PageCursor getOwnedPagesCursor() {
+ return _ownedPages.cursor();
}
/**
public void deleteCurrentRow() throws IOException {
_cursor.deleteCurrentRow();
}
-
+
/**
* Delete the row on which the given rowState is currently positioned.
*/
public void deleteRow(RowState rowState, RowId rowId) throws IOException {
+ requireValidRowId(rowId);
+
// ensure that the relevant row state is up-to-date
ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId);
- if(!rowState.isValid()) {
- throw new IllegalArgumentException(
- "Given rowId is invalid for this table " + rowId);
- }
- if(rowState.isDeleted()) {
- throw new IllegalStateException("Deleting already deleted row");
- }
+ requireNonDeletedRow(rowState, rowId);
// delete flag always gets set in the "header" row (even if data is on
// overflow row)
throw new IllegalArgumentException(
"Given column " + column + " is not from this table");
}
+ requireValidRowId(rowId);
// position at correct row
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
- if(!rowState.isValid()) {
- // this was a bogus rowId
- throw new IllegalArgumentException(
- "Given rowId is not valid for this table " + rowId);
- }
- if(rowState.isDeleted()) {
- // note, row state will indicate that row was deleted
- return null;
- }
+ requireNonDeletedRow(rowState, rowId);
Object value = getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column);
RowState rowState, RowId rowId, Collection<String> columnNames)
throws IOException
{
+ requireValidRowId(rowId);
+
// position at correct row
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
- if(!rowState.isValid()) {
- // this was a bogus rowId
- throw new IllegalArgumentException(
- "Given rowId is not valid for this table " + rowId);
- }
- if(rowState.isDeleted()) {
- // note, row state will indicate that row was deleted
- return null;
- }
+ requireNonDeletedRow(rowState, rowId);
return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
columnNames);
// find last data page (Not bothering to check other pages for free
// space.)
- for(UsageMap.PageIterator revPageIter = _ownedPages.iteratorAtEnd();
- revPageIter.hasPreviousPage(); )
- {
- int tmpPageNumber = revPageIter.getPreviousPage();
+ UsageMap.PageCursor revPageCursor = _ownedPages.cursorAtEnd();
+ while(true) {
+ int tmpPageNumber = revPageCursor.getPreviousPage();
+ if(tmpPageNumber < 0) {
+ break;
+ }
getPageChannel().readPage(dataPage, tmpPageNumber);
if(dataPage.get() == PageTypes.DATA) {
// found last data page
* Returns the row count for the current page. If the page is invalid
* ({@code null}) or the page is not a DATA page, 0 is returned.
*/
- public static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format)
+ private static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format)
throws IOException
{
int rowsOnPage = 0;
}
/**
- * Returns {@code true} if the row is marked as deleted, {@code false}
- * otherwise.
+ * @throws IllegalStateException if the given rowId is invalid
*/
- public static boolean isDeletedRow(ByteBuffer rowBuffer, int rowNum,
- JetFormat format)
- throws IOException
- {
- // note, we don't use findRowStart here cause we need the unmasked value
- return isDeletedRow(
- rowBuffer.getShort(Table.getRowStartOffset(rowNum, format)));
+ private static void requireValidRowId(RowId rowId) {
+ if(!rowId.isValid()) {
+ throw new IllegalArgumentException("Given rowId is invalid: " + rowId);
+ }
+ }
+
+ /**
+ * @throws IllegalStateException if the given row is invalid or deleted
+ */
+ private static void requireNonDeletedRow(RowState rowState, RowId rowId) {
+ if(!rowState.isValid()) {
+ throw new IllegalArgumentException(
+ "Given rowId is invalid for this table: " + rowId);
+ }
+ if(rowState.isDeleted()) {
+ throw new IllegalStateException("Row is deleted: " + rowId);
+ }
}
public static boolean isDeletedRow(short rowStart) {
}
}
+ public boolean isUpToDate() {
+ return(Table.this._modCount == _lastModCount);
+ }
+
private void checkForModification() {
- if(Table.this._modCount != _lastModCount) {
+ if(!isUpToDate()) {
reset();
_headerRowBufferH.invalidate();
_overflowRowBufferH.invalidate();
{
// this should never see modifications because it only happens within
// the positionAtRowData method
- if(_lastModCount != Table.this._modCount) {
+ if(!isUpToDate()) {
throw new IllegalStateException("Table modified while searching?");
}
if(_rowStatus != RowStatus.OVERFLOW) {
*/
public class UsageMap
{
-
private static final Log LOG = LogFactory.getLog(UsageMap.class);
/** Inline map type */
/** Reference map type, for maps that are too large to fit inline */
public static final byte MAP_TYPE_REFERENCE = 0x1;
+ /** bit index value for an invalid page number */
+ private static final int INVALID_BIT_INDEX = -1;
+
/** owning database */
private final Database _database;
/** Page number of the map table declaration */
private BitSet _pageNumbers = new BitSet();
/** Buffer that contains the usage map table declaration page */
private final ByteBuffer _tableBuffer;
- /** modification count on the usage map, used to keep the iterators in
+ /** modification count on the usage map, used to keep the cursors in
sync */
- private int _modCount = 0;
+ private int _modCount;
/** the current handler implementation for reading/writing the specific
usage map type. note, this may change over time. */
private Handler _handler;
}
}
- public PageIterator iterator() {
- return new PageIterator();
+ public PageCursor cursor() {
+ return new PageCursor();
}
- public PageIterator iteratorAtEnd() {
- PageIterator iterator = new PageIterator();
- iterator.afterLast();
- return iterator;
+ public PageCursor cursorAtEnd() {
+ PageCursor cursor = new PageCursor();
+ cursor.afterLast();
+ return cursor;
}
protected short getRowStart() {
}
protected int pageNumberToBitIndex(int pageNumber) {
- return((pageNumber != PageChannel.INVALID_PAGE_NUMBER) ?
- (pageNumber - _startPage) : -1);
+ return((pageNumber >= 0) ? (pageNumber - _startPage) :
+ INVALID_BIT_INDEX);
}
protected void clearTableAndPages()
public String toString() {
StringBuilder builder = new StringBuilder("page numbers: [");
- for(PageIterator iter = iterator(); iter.hasNextPage(); ) {
- builder.append(iter.getNextPage());
- if(iter.hasNextPage()) {
- builder.append(", ");
+ PageCursor pCursor = cursor();
+ while(true) {
+ int nextPage = pCursor.getNextPage();
+ if(nextPage < 0) {
+ break;
}
+ builder.append(nextPage).append(", ");
}
builder.append("]");
return builder.toString();
(pageIndex * 4);
}
}
-
-
+
/**
- * Utility class to iterate over the pages in the UsageMap. Note, since the
- * iterators hold on to page numbers, they should stay valid even as the
- * usage map handlers shift around the bits.
+ * Utility class to traverse over the pages in the UsageMap. Remains valid
+ * in the face of usage map modifications.
*/
- public class PageIterator
+ public class PageCursor
{
- /** handler for moving the page iterator forward */
+ /** handler for moving the page cursor forward */
private final DirHandler _forwardDirHandler = new ForwardDirHandler();
- /** handler for moving the page iterator backward */
+ /** handler for moving the page cursor backward */
private final DirHandler _reverseDirHandler = new ReverseDirHandler();
- /** the next used page number */
- private int _nextPageNumber;
+ /** the current used page number */
+ private int _curPageNumber;
/** the previous used page number */
private int _prevPageNumber;
/** the last read modification count on the UsageMap. we track this so
- that the iterator can detect updates to the usage map while iterating
+ that the cursor can detect updates to the usage map while traversing
and act accordingly */
private int _lastModCount;
- private PageIterator() {
+ private PageCursor() {
reset();
}
private DirHandler getDirHandler(boolean moveForward) {
return (moveForward ? _forwardDirHandler : _reverseDirHandler);
}
-
+
/**
- * @return {@code true} if there is another valid page after the current
- * page, {@code false} otherwise.
+ * Returns {@code true} if this cursor is up-to-date with respect to its
+ * usage map.
*/
- public final boolean hasNextPage() {
- return hasAnotherPage(true);
- }
-
+ public boolean isUpToDate() {
+ return(UsageMap.this._modCount == _lastModCount);
+ }
+
/**
- * @return {@code true} if there is another valid page before the current
- * page, {@code false} otherwise.
+ * Returns the current page number.
*/
- public final boolean hasPreviousPage() {
- return hasAnotherPage(false);
- }
+ public int getCurrentPage() {
+ return _curPageNumber;
+ }
- private boolean hasAnotherPage(boolean moveForward) {
- DirHandler handler = getDirHandler(moveForward);
- int curPageNumber = handler.getCurrentPageNumber();
- int otherPageNumber = handler.getOtherPageNumber();
-
- if((curPageNumber == PageChannel.INVALID_PAGE_NUMBER) &&
- (_lastModCount != UsageMap.this._modCount)) {
- // recheck the last page, in case more showed up
- if(otherPageNumber == PageChannel.INVALID_PAGE_NUMBER) {
- // we were at the beginning
- reset(moveForward);
- } else {
- // we were at the end
- _lastModCount = UsageMap.this._modCount;
- handler.setCurrentPageNumber(
- handler.getAnotherPageNumber(otherPageNumber));
- }
- curPageNumber = handler.getCurrentPageNumber();
+ /**
+ * 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;
}
- return(curPageNumber != PageChannel.INVALID_PAGE_NUMBER);
- }
+ restorePosition(curPageNumber);
+ }
/**
* @return valid page number if there was another page to read,
- * {@link PageChannel#INVALID_PAGE_NUMBER} otherwise
+ * {@link RowId#LAST_PAGE_NUMBER} otherwise
*/
- public final int getNextPage() {
+ public int getNextPage() {
return getAnotherPage(true);
}
/**
* @return valid page number if there was another page to read,
- * {@link PageChannel#INVALID_PAGE_NUMBER} otherwise
+ * {@link RowId#FIRST_PAGE_NUMBER} otherwise
*/
- public final int getPreviousPage() {
+ public int getPreviousPage() {
return getAnotherPage(false);
}
* Gets another page in the given direction, returning the current page.
*/
private int getAnotherPage(boolean moveForward) {
- if (hasAnotherPage(moveForward)) {
- _lastModCount = UsageMap.this._modCount;
- DirHandler handler = getDirHandler(moveForward);
- int anotherPage = handler.getCurrentPageNumber();
- handler.setOtherPageNumber(anotherPage);
- handler.setCurrentPageNumber(
- handler.getAnotherPageNumber(anotherPage));
- return anotherPage;
+ DirHandler handler = getDirHandler(moveForward);
+ if(_curPageNumber == handler.getEndPageNumber()) {
+ if(!isUpToDate()) {
+ restorePosition(_prevPageNumber);
+ // drop through and retry moving to another page
+ } else {
+ // at end, no more
+ return _curPageNumber;
+ }
}
- return PageChannel.INVALID_PAGE_NUMBER;
+
+ _prevPageNumber = _curPageNumber;
+ _curPageNumber = handler.getAnotherPageNumber(_curPageNumber);
+ _lastModCount = UsageMap.this._modCount;
+ return _curPageNumber;
}
-
+
/**
* After calling this method, getNextPage will return the first page in
* the map
}
/**
- * Resets this page iterator for iterating the given direction.
+ * Resets this page cursor for traversing the given direction.
*/
protected void reset(boolean moveForward) {
_lastModCount = UsageMap.this._modCount;
- getDirHandler(moveForward).reset();
+ _curPageNumber = getDirHandler(moveForward).getBeginningPageNumber();
+ _prevPageNumber = _curPageNumber;
}
/**
- * Handles moving the iterator in a given direction. Separates iterator
+ * Restores a previous position for the cursor.
+ */
+ private void restorePosition(int curPageNumber) {
+ _prevPageNumber = _curPageNumber;
+ _curPageNumber = curPageNumber;
+ _lastModCount = UsageMap.this._modCount;
+ }
+
+ /**
+ * Handles moving the cursor in a given direction. Separates cursor
* logic from value storage.
*/
private abstract class DirHandler {
- public abstract int getCurrentPageNumber();
- public abstract void setCurrentPageNumber(int newPageNumber);
- public abstract int getOtherPageNumber();
- public abstract void setOtherPageNumber(int newPageNumber);
public abstract int getAnotherPageNumber(int curPageNumber);
- public abstract void reset();
+ public abstract int getBeginningPageNumber();
+ public abstract int getEndPageNumber();
}
/**
- * Handles moving the iterator forward.
+ * Handles moving the cursor forward.
*/
private final class ForwardDirHandler extends DirHandler {
- public int getCurrentPageNumber() {
- return _nextPageNumber;
- }
- public void setCurrentPageNumber(int newPageNumber) {
- _nextPageNumber = newPageNumber;
- }
- public int getOtherPageNumber() {
- return _prevPageNumber;
- }
- public void setOtherPageNumber(int newPageNumber) {
- _prevPageNumber = newPageNumber;
- }
+ @Override
public int getAnotherPageNumber(int curPageNumber) {
- return UsageMap.this.getNextPageNumber(curPageNumber);
+ if(curPageNumber == RowId.FIRST_PAGE_NUMBER) {
+ return UsageMap.this.getFirstPageNumber();
+ }
+ int anotherPageNumber = UsageMap.this.getNextPageNumber(curPageNumber);
+ return ((anotherPageNumber >= 0) ? anotherPageNumber :
+ RowId.LAST_PAGE_NUMBER);
+ }
+ @Override
+ public int getBeginningPageNumber() {
+ return RowId.FIRST_PAGE_NUMBER;
}
- public void reset() {
- _nextPageNumber = UsageMap.this.getFirstPageNumber();
- _prevPageNumber = PageChannel.INVALID_PAGE_NUMBER;
+ @Override
+ public int getEndPageNumber() {
+ return RowId.LAST_PAGE_NUMBER;
}
}
/**
- * Handles moving the iterator backward.
+ * Handles moving the cursor backward.
*/
private final class ReverseDirHandler extends DirHandler {
- public int getCurrentPageNumber() {
- return _prevPageNumber;
- }
- public void setCurrentPageNumber(int newPageNumber) {
- _prevPageNumber = newPageNumber;
- }
- public int getOtherPageNumber() {
- return _nextPageNumber;
- }
- public void setOtherPageNumber(int newPageNumber) {
- _nextPageNumber = newPageNumber;
- }
+ @Override
public int getAnotherPageNumber(int curPageNumber) {
- return UsageMap.this.getPrevPageNumber(curPageNumber);
+ if(curPageNumber == RowId.LAST_PAGE_NUMBER) {
+ return UsageMap.this.getLastPageNumber();
+ }
+ int anotherPageNumber = UsageMap.this.getPrevPageNumber(curPageNumber);
+ return ((anotherPageNumber >= 0) ? anotherPageNumber :
+ RowId.FIRST_PAGE_NUMBER);
+ }
+ @Override
+ public int getBeginningPageNumber() {
+ return RowId.LAST_PAGE_NUMBER;
}
- public void reset() {
- _nextPageNumber = PageChannel.INVALID_PAGE_NUMBER;
- _prevPageNumber = UsageMap.this.getLastPageNumber();
+ @Override
+ public int getEndPageNumber() {
+ return RowId.FIRST_PAGE_NUMBER;
}
}
db.close();
}
+ public void testLiveAddition() throws Exception {
+ Database db = createTestTable();
+
+ Table table = db.getTable("test");
+
+ Cursor cursor1 = Cursor.createCursor(table);
+ Cursor cursor2 = Cursor.createCursor(table);
+ cursor1.skipNextRows(11);
+ cursor2.skipNextRows(11);
+
+ assertTrue(cursor1.isAfterLast());
+ assertTrue(cursor2.isAfterLast());
+
+ int newRowNum = 11;
+ table.addRow(newRowNum, "data" + newRowNum);
+ Map<String,Object> expectedRow =
+ createExpectedRow("id", newRowNum, "value", "data" + newRowNum);
+
+ assertFalse(cursor1.isAfterLast());
+ assertFalse(cursor2.isAfterLast());
+
+ assertEquals(expectedRow, cursor1.getCurrentRow());
+ assertEquals(expectedRow, cursor2.getCurrentRow());
+ assertFalse(cursor1.moveToNextRow());
+ assertFalse(cursor2.moveToNextRow());
+ assertTrue(cursor1.isAfterLast());
+ assertTrue(cursor2.isAfterLast());
+
+ db.close();
+ }
+
+ public void testLiveDeletion() throws Exception {
+ Database db = createTestTable();
+
+ Table table = db.getTable("test");
+
+ Cursor cursor1 = Cursor.createCursor(table);
+ Cursor cursor2 = Cursor.createCursor(table);
+ Cursor cursor3 = Cursor.createCursor(table);
+ Cursor cursor4 = Cursor.createCursor(table);
+ cursor1.skipNextRows(2);
+ cursor2.skipNextRows(3);
+ cursor3.skipNextRows(3);
+ cursor4.skipNextRows(4);
+
+ Map<String,Object> expectedPrevRow =
+ createExpectedRow("id", 1, "value", "data" + 1);
+ Map<String,Object> expectedDeletedRow =
+ createExpectedRow("id", 2, "value", "data" + 2);
+ Map<String,Object> expectedNextRow =
+ createExpectedRow("id", 3, "value", "data" + 3);
+
+ assertEquals(expectedDeletedRow, cursor2.getCurrentRow());
+ assertEquals(expectedDeletedRow, cursor3.getCurrentRow());
+
+ assertFalse(cursor2.isCurrentRowDeleted());
+ assertFalse(cursor3.isCurrentRowDeleted());
+
+ cursor2.deleteCurrentRow();
+
+ assertTrue(cursor2.isCurrentRowDeleted());
+ assertTrue(cursor3.isCurrentRowDeleted());
+
+ assertEquals(expectedNextRow, cursor1.getNextRow());
+ assertEquals(expectedNextRow, cursor2.getNextRow());
+ assertEquals(expectedNextRow, cursor3.getNextRow());
+
+ assertEquals(expectedPrevRow, cursor3.getPreviousRow());
+
+ db.close();
+ }
+
}