Checkpoint commit for OPENDJ-1471 File based changelog : improve cursor behavior
First step : improve cursor behavior for file-based implementation only
* Log.java, LogFile.java, FileReplicaDBCursor.java : change cursors to behave
like java.sql.ResultSet, ie cursor is positionned before the first record
* FileReplicaDBCursor.java : fix behavior for case when record with
start key is not available when cursor is created
* FileChangeNumberIndexDBCursor.java : update to adapt to new behavior
of underlying cursor, but still behave differently than java.sql.ResultSet
(to be changed in the next step)
* LogTest.java, LogFileTest.java : adapt tests to new behavior
* FileReplicaDBTest.java : update test related to exhausted cursor
for better coverage of edge cases
| | |
| | | * |
| | | * @param cursor |
| | | * The underlying cursor to read log. |
| | | * @throws ChangelogException |
| | | * If an error occurs. |
| | | */ |
| | | FileChangeNumberIndexDBCursor(final DBCursor<Record<Long, ChangeNumberIndexRecord>> cursor) { |
| | | FileChangeNumberIndexDBCursor(final DBCursor<Record<Long, ChangeNumberIndexRecord>> cursor) |
| | | throws ChangelogException |
| | | { |
| | | this.cursor = cursor; |
| | | // cursor is positioned to first record at start |
| | | next(); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | |
| | | import org.opends.server.replication.server.changelog.file.Log.RepositionableCursor; |
| | | |
| | | /** |
| | | * A cursor on ReplicaDB. |
| | | * A cursor on ReplicaDB, which can re-initialize itself after exhaustion. |
| | | * <p> |
| | | * This cursor behaves specially in two ways : |
| | | * <ul> |
| | | * <li>The cursor initially points to a {@code null} value: the |
| | | * {@code getRecord()} method return {@code null} if called before any call to |
| | | * {@code next()} method.</li> |
| | | * <li>The cursor automatically re-initializes itself if it is exhausted: when |
| | | * exhausted, the cursor re-position itself to the last non null CSN previously |
| | | * read. |
| | | * <li> |
| | | * </ul> |
| | | * The cursor provides a java.sql.ResultSet like API : |
| | | * <pre> |
| | | * {@code |
| | | * FileReplicaDBCursor cursor = ...; |
| | | * try { |
| | | * while (cursor.next()) { |
| | | * Record record = cursor.getRecord(); |
| | | * // ... can call cursor.getRecord() again: it will return the same result |
| | | * } |
| | | * } |
| | | * finally { |
| | | * close(cursor); |
| | | * } |
| | | * } |
| | | * </pre> |
| | | * <p> |
| | | * The cursor automatically re-initializes itself if it is exhausted: if a |
| | | * record is newly available, a subsequent call to the {@code next()} method will |
| | | * return {@code true} and the record will be available by calling {@code getRecord()} |
| | | * method. |
| | | */ |
| | | class FileReplicaDBCursor implements DBCursor<UpdateMsg> |
| | | { |
| | |
| | | /** The next record to return. */ |
| | | private Record<CSN, UpdateMsg> nextRecord; |
| | | |
| | | /** The CSN to re-start with in case the cursor is exhausted. */ |
| | | /** The CSN to re-start with in case the cursor is exhausted. */ |
| | | private CSN lastNonNullCurrentCSN; |
| | | |
| | | /** |
| | |
| | | @Override |
| | | public boolean next() throws ChangelogException |
| | | { |
| | | nextRecord = cursor.getRecord(); |
| | | if (nextRecord != null) |
| | | if (cursor.next()) |
| | | { |
| | | nextRecord = cursor.getRecord(); |
| | | if (nextRecord.getKey().compareTo(lastNonNullCurrentCSN) > 0) |
| | | { |
| | | lastNonNullCurrentCSN = nextRecord.getKey(); |
| | | return true; |
| | | } |
| | | } |
| | | return nextWhenCursorIsExhaustedOrNotCorrectlyPositionned(); |
| | | } |
| | | |
| | | /** Re-initialize the cursor after the last non null CSN. */ |
| | | private boolean nextWhenCursorIsExhaustedOrNotCorrectlyPositionned() throws ChangelogException |
| | | { |
| | | final boolean found = cursor.positionTo(lastNonNullCurrentCSN, true); |
| | | if (found && cursor.next()) |
| | | { |
| | | nextRecord = cursor.getRecord(); |
| | | lastNonNullCurrentCSN = nextRecord.getKey(); |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | // Exhausted cursor must be able to reinitialize itself |
| | | cursor.positionTo(lastNonNullCurrentCSN, true); |
| | | |
| | | nextRecord = cursor.getRecord(); |
| | | if (nextRecord != null) |
| | | { |
| | | lastNonNullCurrentCSN = nextRecord.getKey(); |
| | | } |
| | | nextRecord = null; |
| | | return false; |
| | | } |
| | | // the underlying cursor is one record in advance |
| | | cursor.next(); |
| | | return nextRecord != null; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | |
| | | * Returns a cursor that allows to retrieve the records from this log, |
| | | * starting at the first position. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the first |
| | | * key, that is {@code cursor.getRecord()} is equals to the record |
| | | * corresponding to the first key before any call to {@code cursor.next()} |
| | | * method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. |
| | | * |
| | | * @return a cursor on the log records, which is never {@code null} |
| | | * @throws ChangelogException |
| | |
| | | * Returns a cursor that allows to retrieve the records from this log, |
| | | * starting at the position defined by the provided key. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the key, |
| | | * that is {@code cursor.getRecord()} is equals to the record corresponding to |
| | | * the key before any call to {@code cursor.next()} method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. |
| | | * |
| | | * @param key |
| | | * Key to use as a start position for the cursor. If key is |
| | |
| | | * starting at the position defined by the smallest key that is higher than |
| | | * the provided key. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the key |
| | | * found, that is {@code cursor.getRecord()} is equals to the record |
| | | * corresponding to the key found before any call to {@code cursor.next()} |
| | | * method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. After the first call to {@code cursor.next()} |
| | | * the cursor points to the record corresponding to the key found. |
| | | * |
| | | * @param key |
| | | * Key to use as a start position for the cursor. If key is |
| | |
| | | if (logFile != null) |
| | | { |
| | | switchToLogFile(logFile); |
| | | return true; |
| | | return currentCursor.next(); |
| | | } |
| | | return false; |
| | | } |
| | |
| | | { |
| | | switchToLogFile(logFile); |
| | | } |
| | | if (key != null) |
| | | { |
| | | boolean isFound = currentCursor.positionTo(key, findNearest); |
| | | if (isFound && getRecord() == null && !log.isHeadLogFile(currentLogFile)) |
| | | { |
| | | // The key to position is probably in the next file, force the switch |
| | | isFound = next(); |
| | | } |
| | | return isFound; |
| | | } |
| | | return true; |
| | | return (key == null) ? true : currentCursor.positionTo(key, findNearest); |
| | | } |
| | | finally |
| | | { |
| | |
| | | * Returns a cursor that allows to retrieve the records from this log, |
| | | * starting at the first position. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the first |
| | | * key, that is {@code cursor.getRecord()} is equals to the record |
| | | * corresponding to the first key before any call to {@code cursor.next()} |
| | | * method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals {@code null} before any call to |
| | | * {@code cursor.next()} method. |
| | | * |
| | | * @return a cursor on the log records, which is never {@code null} |
| | | * @throws ChangelogException |
| | |
| | | * Returns a cursor that allows to retrieve the records from this log, |
| | | * starting at the position defined by the provided key. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the key, |
| | | * that is {@code cursor.getRecord()} is equals to the record corresponding to |
| | | * the key before any call to {@code cursor.next()} method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. After the first call to {@code cursor.next()} |
| | | * the cursor points to the record corresponding to the key. |
| | | * |
| | | * @param key |
| | | * Key to use as a start position for the cursor. If key is |
| | |
| | | * starting at the position defined by the smallest key that is higher than |
| | | * the provided key. |
| | | * <p> |
| | | * The returned cursor initially points to record corresponding to the key |
| | | * found, that is {@code cursor.getRecord()} is equals to the record |
| | | * corresponding to the key found before any call to {@code cursor.next()} |
| | | * method. |
| | | * The returned cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. After the first call to {@code cursor.next()} |
| | | * the cursor points to the record corresponding to the key found. |
| | | * |
| | | * @param key |
| | | * Key to use as a start position for the cursor. If key is |
| | |
| | | try |
| | | { |
| | | cursor = getCursor(); |
| | | return cursor.getRecord(); |
| | | return cursor.next() ? cursor.getRecord() : null; |
| | | } |
| | | finally |
| | | { |
| | |
| | | try |
| | | { |
| | | cursor = getCursor(); |
| | | Record<K, V> record = cursor.getRecord(); |
| | | Record<K, V> record = null; |
| | | while (cursor.next()) |
| | | { |
| | | record = cursor.getRecord(); |
| | |
| | | try |
| | | { |
| | | cursor = getCursor(); |
| | | Record<K, V> record = cursor.getRecord(); |
| | | if (record == null) |
| | | { |
| | | return 0L; |
| | | } |
| | | long counter = 1L; |
| | | long counter = 0L; |
| | | while (cursor.next()) |
| | | { |
| | | record = cursor.getRecord(); |
| | | counter++; |
| | | } |
| | | return counter; |
| | |
| | | /** |
| | | * Implements a repositionable cursor on the log file. |
| | | * <p> |
| | | * The cursor initially points to a record, that is {@code cursor.getRecord()} |
| | | * is equals to the first record available from the cursor before any call to |
| | | * The cursor initially points to no record, that is |
| | | * {@code cursor.getRecord()} is equals to {@code null} before any call to |
| | | * {@code cursor.next()} method. |
| | | */ |
| | | static final class LogFileCursor<K extends Comparable<K>, V> implements RepositionableCursor<K,V> |
| | |
| | | private Record<K,V> currentRecord; |
| | | |
| | | /** |
| | | * The initial record when starting from a given key. It must be |
| | | * stored because it is read in advance. |
| | | */ |
| | | private Record<K,V> initialRecord; |
| | | |
| | | /** |
| | | * Creates a cursor on the provided log. |
| | | * |
| | | * @param logFile |
| | |
| | | { |
| | | this.logFile = logFile; |
| | | this.reader = logFile.getReader(); |
| | | try |
| | | { |
| | | // position to the first record. |
| | | next(); |
| | | } |
| | | catch (ChangelogException e) |
| | | { |
| | | close(); |
| | | throw e; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | @Override |
| | | public boolean next() throws ChangelogException |
| | | { |
| | | if (initialRecord != null) |
| | | { |
| | | // initial record is used only once |
| | | currentRecord = initialRecord; |
| | | initialRecord = null; |
| | | return true; |
| | | } |
| | | currentRecord = reader.readRecord(); |
| | | return currentRecord != null; |
| | | } |
| | |
| | | public boolean positionTo(final K key, boolean findNearest) throws ChangelogException { |
| | | final Pair<Boolean, Record<K, V>> result = reader.seekToRecord(key, findNearest); |
| | | final boolean found = result.getFirst(); |
| | | currentRecord = found ? result.getSecond() : null; |
| | | initialRecord = found ? result.getSecond() : null; |
| | | return found; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | public void testGenerateCursorFromWithCursorReinitialization() throws Exception |
| | | @DataProvider |
| | | Object[][] dataForTestsWithCursorReinitialization() |
| | | { |
| | | return new Object[][] { |
| | | // the index to use in CSN array for the start key of the cursor |
| | | { 0 }, |
| | | { 1 }, |
| | | { 4 }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider="dataForTestsWithCursorReinitialization") |
| | | public void testGenerateCursorFromWithCursorReinitialization(int csnIndexForStartKey) throws Exception |
| | | { |
| | | ReplicationServer replicationServer = null; |
| | | DBCursor<UpdateMsg> cursor = null; |
| | |
| | | |
| | | CSN[] csns = generateCSNs(1, System.currentTimeMillis(), 6); |
| | | |
| | | cursor = replicaDB.generateCursorFrom(csns[0]); |
| | | cursor = replicaDB.generateCursorFrom(csns[csnIndexForStartKey]); |
| | | assertFalse(cursor.next()); |
| | | |
| | | for (int i = 0; i < 5; i++) |
| | | int[] indicesToAdd = new int[] { 0, 1, 2, 4 }; |
| | | for (int i : indicesToAdd) |
| | | { |
| | | if (i != 3) |
| | | { |
| | | replicaDB.add(new DeleteMsg(TEST_ROOT_DN, csns[i], "uid")); |
| | | } |
| | | replicaDB.add(new DeleteMsg(TEST_ROOT_DN, csns[i], "uid")); |
| | | } |
| | | waitChangesArePersisted(replicaDB, 4); |
| | | |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getRecord().getCSN(), csns[1]); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getRecord().getCSN(), csns[2]); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getRecord().getCSN(), csns[4]); |
| | | for (int i = csnIndexForStartKey+1; i < 5; i++) |
| | | { |
| | | if (i != 3) |
| | | { |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getRecord().getCSN(), csns[i], "index i=" + i); |
| | | } |
| | | } |
| | | assertFalse(cursor.next()); |
| | | StaticUtils.close(cursor); |
| | | |
| | | } |
| | | finally |
| | | { |
| | |
| | | try { |
| | | cursor = changelog.getCursor(); |
| | | |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key01", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | try { |
| | | cursor = changelog.getCursor("key05"); |
| | | |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key05", "value5")); |
| | | assertThatCursorCanBeFullyRead(cursor, 6, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 5, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | try { |
| | | cursor = changelog.getCursor(null); |
| | | |
| | | // should start from start |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key01", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | try { |
| | | cursor = changelog.getNearestCursor("key01"); |
| | | |
| | | // lowest higher key is key2 |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key02", "value2")); |
| | | assertThatCursorCanBeFullyRead(cursor, 3, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | try { |
| | | cursor = changelog.getNearestCursor("key00"); |
| | | |
| | | // lowest higher key is key1 |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key01", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | try { |
| | | cursor = changelog.getNearestCursor(null); |
| | | |
| | | // should start from start |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key01", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyRead(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, changelog); |
| | |
| | | |
| | | // ensure log can be fully read including the new record |
| | | cursor = logFile.getCursor("key05"); |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key05", "value5")); |
| | | assertThatCursorCanBeFullyRead(cursor, 6, 11); |
| | | assertThatCursorCanBeFullyRead(cursor, 5, 11); |
| | | } |
| | | finally |
| | | { |
| | |
| | | private void assertThatCursorCanBeFullyRead(DBCursor<Record<String, String>> cursor, int fromIndex, int endIndex) |
| | | throws Exception |
| | | { |
| | | assertThat(cursor.getRecord()).isNull(); |
| | | for (int i = fromIndex; i <= endIndex; i++) |
| | | { |
| | | assertThat(cursor.next()).as("next() value when i=" + i).isTrue(); |
| | |
| | | try { |
| | | cursor = log.getCursor(); |
| | | |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key001", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, log); |
| | |
| | | try { |
| | | cursor = log.getCursor("key005"); |
| | | |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key005", "value5")); |
| | | assertThatCursorCanBeFullyRead(cursor, 6, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor, 5, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, log); |
| | |
| | | try { |
| | | cursor = log.getCursor(null); |
| | | |
| | | // should start from first record |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key001", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, log); |
| | |
| | | try { |
| | | // this key is the first key of the log file "key1_key2.log" |
| | | cursor1 = log.getNearestCursor("key001"); |
| | | // lowest higher key is key2 |
| | | assertThat(cursor1.getRecord()).isEqualTo(Record.from("key002", "value2")); |
| | | assertThatCursorCanBeFullyRead(cursor1, 3, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor1, 2, 10); |
| | | |
| | | // this key is the last key of the log file "key3_key4.log" |
| | | cursor2 = log.getNearestCursor("key004"); |
| | | // lowest higher key is key5 |
| | | assertThat(cursor2.getRecord()).isEqualTo(Record.from("key005", "value5")); |
| | | assertThatCursorCanBeFullyRead(cursor2, 6, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor2, 5, 10); |
| | | |
| | | cursor3 = log.getNearestCursor("key009"); |
| | | // lowest higher key is key10 |
| | | assertThat(cursor3.getRecord()).isEqualTo(Record.from("key010", "value10")); |
| | | assertThatCursorIsExhausted(cursor3); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor3, 10, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor1, cursor2, cursor3, log); |
| | |
| | | // key is between key005 and key006 |
| | | cursor = log.getNearestCursor("key005000"); |
| | | |
| | | // lowest higher key is key006 |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key006", "value6")); |
| | | assertThatCursorCanBeFullyRead(cursor, 7, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor, 6, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, log); |
| | |
| | | try { |
| | | cursor = log.getNearestCursor(null); |
| | | |
| | | // should start from start |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key001", "value1")); |
| | | assertThatCursorCanBeFullyRead(cursor, 2, 10); |
| | | assertThatCursorCanBeFullyReadFromStart(cursor, 1, 10); |
| | | } |
| | | finally { |
| | | StaticUtils.close(cursor, log); |
| | |
| | | } |
| | | writeLog1.syncToFileSystem(); |
| | | cursor = writeLog1.getCursor("key020"); |
| | | for (int i = 1; i <= 60; i++) |
| | | for (int i = 1; i <= 61; i++) |
| | | { |
| | | assertThat(cursor.next()).isTrue(); |
| | | } |
| | |
| | | log = openLog(LogFileTest.RECORD_PARSER); |
| | | cursor = log.getCursor(); |
| | | // advance cursor to last record to ensure it is pointing to ahead log file |
| | | advanceCursorFromFirstRecordTo(cursor, 10); |
| | | advanceCursorUpTo(cursor, 1, 10); |
| | | |
| | | // add new records to ensure the ahead log file is rotated |
| | | for (int i = 11; i <= 20; i++) |
| | |
| | | { |
| | | log = openLog(LogFileTest.RECORD_PARSER); |
| | | cursor1 = log.getCursor(); |
| | | advanceCursorFromFirstRecordTo(cursor1, 1); |
| | | advanceCursorUpTo(cursor1, 1, 1); |
| | | cursor2 = log.getCursor(); |
| | | advanceCursorFromFirstRecordTo(cursor2, 4); |
| | | advanceCursorUpTo(cursor2, 1, 4); |
| | | cursor3 = log.getCursor(); |
| | | advanceCursorFromFirstRecordTo(cursor3, 9); |
| | | advanceCursorUpTo(cursor3, 1, 9); |
| | | cursor4 = log.getCursor(); |
| | | advanceCursorFromFirstRecordTo(cursor4, 10); |
| | | advanceCursorUpTo(cursor4, 1, 10); |
| | | |
| | | // add new records to ensure the ahead log file is rotated |
| | | for (int i = 11; i <= 20; i++) |
| | |
| | | log.purgeUpTo(purgeKey); |
| | | |
| | | cursor = log.getCursor(); |
| | | assertThat(cursor.next()).isTrue(); |
| | | assertThat(cursor.getRecord()).isEqualTo(firstRecordExpectedAfterPurge); |
| | | assertThatCursorCanBeFullyRead(cursor, cursorStartIndex, cursorEndIndex); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private void advanceCursorFromFirstRecordTo(DBCursor<Record<String, String>> cursor, int endIndex) |
| | | throws Exception |
| | | { |
| | | assertThat(cursor.getRecord()).isEqualTo(Record.from("key001", "value1")); |
| | | advanceCursorUpTo(cursor, 2, endIndex); |
| | | } |
| | | |
| | | private void advanceCursorUpTo(DBCursor<Record<String, String>> cursor, int fromIndex, int endIndex) |
| | | throws Exception |
| | | { |
| | |
| | | assertThatCursorIsExhausted(cursor); |
| | | } |
| | | |
| | | /** |
| | | * Read the cursor until exhaustion, beginning at start of cursor. |
| | | */ |
| | | private void assertThatCursorCanBeFullyReadFromStart(DBCursor<Record<String, String>> cursor, int fromIndex, int endIndex) |
| | | throws Exception |
| | | { |
| | | assertThat(cursor.getRecord()).isNull(); |
| | | assertThatCursorCanBeFullyRead(cursor, fromIndex, endIndex); |
| | | } |
| | | |
| | | private void assertThatCursorIsExhausted(DBCursor<Record<String, String>> cursor) throws Exception |
| | | { |
| | | assertThat(cursor.next()).isFalse(); |