Skip to content

Commit 36984dc

Browse files
committed
Prevent null element from stopping cursor iteration
The modifier change (i.e. `private` -> `protected`) is to avoid synthetic method generation.
1 parent a382a68 commit 36984dc

File tree

3 files changed

+114
-12
lines changed

3 files changed

+114
-12
lines changed

src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class DefaultCursor<T> implements Cursor<T> {
4141
private final ResultMap resultMap;
4242
private final ResultSetWrapper rsw;
4343
private final RowBounds rowBounds;
44-
private final ObjectWrapperResultHandler<T> objectWrapperResultHandler = new ObjectWrapperResultHandler<>();
44+
protected final ObjectWrapperResultHandler<T> objectWrapperResultHandler = new ObjectWrapperResultHandler<>();
4545

4646
private final CursorIterator cursorIterator = new CursorIterator();
4747
private boolean iteratorRetrieved;
@@ -123,7 +123,7 @@ public void close() {
123123

124124
protected T fetchNextUsingRowBound() {
125125
T result = fetchNextObjectFromDatabase();
126-
while (result != null && indexWithRowBound < rowBounds.getOffset()) {
126+
while (objectWrapperResultHandler.fetched && indexWithRowBound < rowBounds.getOffset()) {
127127
result = fetchNextObjectFromDatabase();
128128
}
129129
return result;
@@ -135,6 +135,7 @@ protected T fetchNextObjectFromDatabase() {
135135
}
136136

137137
try {
138+
objectWrapperResultHandler.fetched = false;
138139
status = CursorStatus.OPEN;
139140
if (!rsw.getResultSet().isClosed()) {
140141
resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, null);
@@ -144,11 +145,11 @@ protected T fetchNextObjectFromDatabase() {
144145
}
145146

146147
T next = objectWrapperResultHandler.result;
147-
if (next != null) {
148+
if (objectWrapperResultHandler.fetched) {
148149
indexWithRowBound++;
149150
}
150151
// No more object or limit reached
151-
if (next == null || getReadItemsCount() == rowBounds.getOffset() + rowBounds.getLimit()) {
152+
if (!objectWrapperResultHandler.fetched || getReadItemsCount() == rowBounds.getOffset() + rowBounds.getLimit()) {
152153
close();
153154
status = CursorStatus.CONSUMED;
154155
}
@@ -165,18 +166,20 @@ private int getReadItemsCount() {
165166
return indexWithRowBound + 1;
166167
}
167168

168-
private static class ObjectWrapperResultHandler<T> implements ResultHandler<T> {
169+
protected static class ObjectWrapperResultHandler<T> implements ResultHandler<T> {
169170

170-
private T result;
171+
protected T result;
172+
protected boolean fetched;
171173

172174
@Override
173175
public void handleResult(ResultContext<? extends T> context) {
174176
this.result = context.getResultObject();
175177
context.stop();
178+
fetched = true;
176179
}
177180
}
178181

179-
private class CursorIterator implements Iterator<T> {
182+
protected class CursorIterator implements Iterator<T> {
180183

181184
/**
182185
* Holder for the next object to be returned.
@@ -190,22 +193,23 @@ private class CursorIterator implements Iterator<T> {
190193

191194
@Override
192195
public boolean hasNext() {
193-
if (object == null) {
196+
if (!objectWrapperResultHandler.fetched) {
194197
object = fetchNextUsingRowBound();
195198
}
196-
return object != null;
199+
return objectWrapperResultHandler.fetched;
197200
}
198201

199202
@Override
200203
public T next() {
201204
// Fill next with object fetched from hasNext()
202205
T next = object;
203206

204-
if (next == null) {
207+
if (!objectWrapperResultHandler.fetched) {
205208
next = fetchNextUsingRowBound();
206209
}
207210

208-
if (next != null) {
211+
if (objectWrapperResultHandler.fetched) {
212+
objectWrapperResultHandler.fetched = false;
209213
object = null;
210214
iteratorIndex++;
211215
return next;

src/test/java/org/apache/ibatis/submitted/cursor_simple/CursorSimpleTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
*/
1616
package org.apache.ibatis.submitted.cursor_simple;
1717

18+
import static org.junit.Assert.assertFalse;
19+
import static org.junit.Assert.assertNull;
20+
import static org.junit.Assert.assertTrue;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
1823
import org.apache.ibatis.BaseDataTest;
1924
import org.apache.ibatis.cursor.Cursor;
2025
import org.apache.ibatis.io.Resources;
@@ -387,4 +392,85 @@ void shouldThrowIllegalStateExceptionUsingIteratorOnSessionClosed() {
387392

388393
}
389394

395+
@Test
396+
void shouldNullItemNotStopIteration() {
397+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
398+
Mapper mapper = sqlSession.getMapper(Mapper.class);
399+
Cursor<User> cursor = mapper.getNullUsers(new RowBounds());
400+
Iterator<User> iterator = cursor.iterator();
401+
402+
assertFalse(cursor.isOpen());
403+
404+
// Cursor is just created, current index is -1
405+
assertEquals(-1, cursor.getCurrentIndex());
406+
407+
// Check if hasNext, fetching is started
408+
assertTrue(iterator.hasNext());
409+
// Re-invoking hasNext() should not fetch the next row
410+
assertTrue(iterator.hasNext());
411+
assertTrue(cursor.isOpen());
412+
assertFalse(cursor.isConsumed());
413+
414+
// next() has not been called, index is still -1
415+
assertEquals(-1, cursor.getCurrentIndex());
416+
417+
User user;
418+
user = iterator.next();
419+
assertNull(user);
420+
assertEquals(0, cursor.getCurrentIndex());
421+
422+
assertTrue(iterator.hasNext());
423+
user = iterator.next();
424+
assertEquals("Kate", user.getName());
425+
assertEquals(1, cursor.getCurrentIndex());
426+
427+
assertTrue(iterator.hasNext());
428+
user = iterator.next();
429+
assertNull(user);
430+
assertEquals(2, cursor.getCurrentIndex());
431+
432+
assertTrue(iterator.hasNext());
433+
user = iterator.next();
434+
assertNull(user);
435+
assertEquals(3, cursor.getCurrentIndex());
436+
437+
// Check no more elements
438+
assertFalse(iterator.hasNext());
439+
assertFalse(cursor.isOpen());
440+
assertTrue(cursor.isConsumed());
441+
}
442+
}
443+
444+
@Test
445+
void shouldRowBoundsCountNullItem() {
446+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
447+
Mapper mapper = sqlSession.getMapper(Mapper.class);
448+
Cursor<User> cursor = mapper.getNullUsers(new RowBounds(1, 2));
449+
Iterator<User> iterator = cursor.iterator();
450+
451+
assertFalse(cursor.isOpen());
452+
453+
// Check if hasNext, fetching is started
454+
assertTrue(iterator.hasNext());
455+
// Re-invoking hasNext() should not fetch the next row
456+
assertTrue(iterator.hasNext());
457+
assertTrue(cursor.isOpen());
458+
assertFalse(cursor.isConsumed());
459+
460+
User user;
461+
user = iterator.next();
462+
assertEquals("Kate", user.getName());
463+
assertEquals(1, cursor.getCurrentIndex());
464+
465+
assertTrue(iterator.hasNext());
466+
user = iterator.next();
467+
assertNull(user);
468+
assertEquals(2, cursor.getCurrentIndex());
469+
470+
// Check no more elements
471+
assertFalse(iterator.hasNext());
472+
assertFalse(cursor.isOpen());
473+
assertTrue(cursor.isConsumed());
474+
}
475+
}
390476
}

src/test/java/org/apache/ibatis/submitted/cursor_simple/Mapper.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2015 the original author or authors.
2+
* Copyright 2009-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,10 +15,22 @@
1515
*/
1616
package org.apache.ibatis.submitted.cursor_simple;
1717

18+
import org.apache.ibatis.annotations.Select;
1819
import org.apache.ibatis.cursor.Cursor;
20+
import org.apache.ibatis.session.RowBounds;
1921

2022
public interface Mapper {
2123

2224
Cursor<User> getAllUsers();
2325

26+
@Select({
27+
"select null id, null name from (values (0))",
28+
"union all",
29+
"select 99 id, 'Kate' name from (values (0))",
30+
"union all",
31+
"select null id, null name from (values (0))",
32+
"union all",
33+
"select null id, null name from (values (0))"
34+
})
35+
Cursor<User> getNullUsers(RowBounds rowBounds);
2436
}

0 commit comments

Comments
 (0)