Skip to content
This repository was archived by the owner on Aug 17, 2020. It is now read-only.

Commit d0eb0e4

Browse files
committed
Merge pull request #99 from square/jw/nully
Formalize behavior around Query.run() returning null.
2 parents f91ca98 + 03c497b commit d0eb0e4

File tree

5 files changed

+116
-16
lines changed

5 files changed

+116
-16
lines changed

sqlbrite/src/androidTest/java/com/squareup/sqlbrite/QueryTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
package com.squareup.sqlbrite;
1717

1818
import android.database.Cursor;
19+
import android.support.annotation.Nullable;
1920
import android.support.test.InstrumentationRegistry;
2021
import com.squareup.sqlbrite.SqlBrite.Query;
2122
import com.squareup.sqlbrite.TestDb.Employee;
2223
import java.util.List;
2324
import org.junit.Before;
2425
import org.junit.Test;
26+
import rx.Observable;
2527
import rx.functions.Func1;
2628
import rx.observables.BlockingObservable;
29+
import rx.observers.TestSubscriber;
2730
import rx.schedulers.Schedulers;
2831

2932
import static com.google.common.truth.Truth.assertThat;
@@ -89,6 +92,22 @@ public final class QueryTest {
8992
}
9093
}
9194

95+
@Test public void mapToOneIgnoresNullCursor() {
96+
Query nully = new Query() {
97+
@Nullable @Override public Cursor run() {
98+
return null;
99+
}
100+
};
101+
102+
TestSubscriber<Employee> subscriber = new TestSubscriber<>();
103+
Observable.just(nully)
104+
.lift(Query.mapToOne(Employee.MAPPER))
105+
.subscribe(subscriber);
106+
107+
subscriber.assertNoValues();
108+
subscriber.assertCompleted();
109+
}
110+
92111
@Test public void mapToOneOrDefault() {
93112
Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1")
94113
.lift(Query.mapToOneOrDefault(Employee.MAPPER, null))
@@ -146,6 +165,23 @@ public final class QueryTest {
146165
}
147166
}
148167

168+
@Test public void mapToOneOrDefaultReturnsDefaultWhenNullCursor() {
169+
Employee defaultEmployee = new Employee("bob", "Bob Bobberson");
170+
Query nully = new Query() {
171+
@Nullable @Override public Cursor run() {
172+
return null;
173+
}
174+
};
175+
176+
TestSubscriber<Employee> subscriber = new TestSubscriber<>();
177+
Observable.just(nully)
178+
.lift(Query.mapToOneOrDefault(Employee.MAPPER, defaultEmployee))
179+
.subscribe(subscriber);
180+
181+
subscriber.assertValues(defaultEmployee);
182+
subscriber.assertCompleted();
183+
}
184+
149185
@Test public void mapToList() {
150186
List<Employee> employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES)
151187
.lift(Query.mapToList(Employee.MAPPER))
@@ -184,4 +220,20 @@ public final class QueryTest {
184220
"OnError while emitting onNext value: SELECT username, name FROM employee");
185221
}
186222
}
223+
224+
@Test public void mapToListIgnoresNullCursor() {
225+
Query nully = new Query() {
226+
@Nullable @Override public Cursor run() {
227+
return null;
228+
}
229+
};
230+
231+
TestSubscriber<List<Employee>> subscriber = new TestSubscriber<>();
232+
Observable.just(nully)
233+
.lift(Query.mapToList(Employee.MAPPER))
234+
.subscribe(subscriber);
235+
236+
subscriber.assertNoValues();
237+
subscriber.assertCompleted();
238+
}
187239
}

sqlbrite/src/androidTest/java/com/squareup/sqlbrite/SqlBriteTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import android.database.Cursor;
44
import android.database.MatrixCursor;
5+
import android.support.annotation.Nullable;
56
import android.support.test.runner.AndroidJUnit4;
67
import com.squareup.sqlbrite.SqlBrite.Query;
78
import java.util.List;
89
import java.util.concurrent.atomic.AtomicInteger;
910
import org.junit.Test;
1011
import org.junit.runner.RunWith;
1112
import rx.functions.Func1;
13+
import rx.observers.TestSubscriber;
1214

1315
import static com.google.common.truth.Truth.assertThat;
1416

@@ -51,6 +53,28 @@ public final class SqlBriteTest {
5153
assertThat(count.get()).isEqualTo(1);
5254
}
5355

56+
@Test public void asRowsEmptyWhenNullCursor() {
57+
Query nully = new Query() {
58+
@Nullable @Override public Cursor run() {
59+
return null;
60+
}
61+
};
62+
63+
TestSubscriber<Name> subscriber = new TestSubscriber<>();
64+
final AtomicInteger count = new AtomicInteger();
65+
nully.asRows(new Func1<Cursor, Name>() {
66+
@Override public Name call(Cursor cursor) {
67+
count.incrementAndGet();
68+
return Name.MAP.call(cursor);
69+
}
70+
}).subscribe(subscriber);
71+
72+
subscriber.assertNoValues();
73+
subscriber.assertCompleted();
74+
75+
assertThat(count.get()).isEqualTo(0);
76+
}
77+
5478
static final class Name {
5579
static final Func1<Cursor, Name> MAP = new Func1<Cursor, Name>() {
5680
@Override public Name call(Cursor cursor) {

sqlbrite/src/main/java/com/squareup/sqlbrite/QueryToListOperator.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public Subscriber<? super SqlBrite.Query> call(final Subscriber<? super List<T>>
2222
@Override public void onNext(SqlBrite.Query query) {
2323
try {
2424
Cursor cursor = query.run();
25+
if (cursor == null) {
26+
return;
27+
}
2528
List<T> items = new ArrayList<>(cursor.getCount());
2629
try {
2730
for (int i = 1; cursor.moveToNext() && !subscriber.isUnsubscribed(); i++) {

sqlbrite/src/main/java/com/squareup/sqlbrite/QueryToOneOperator.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ final class QueryToOneOperator<T> implements Observable.Operator<T, SqlBrite.Que
2424
try {
2525
T item = null;
2626
Cursor cursor = query.run();
27-
try {
28-
if (cursor.moveToNext()) {
29-
item = mapper.call(cursor);
30-
if (item == null) {
31-
throw new NullPointerException("Mapper returned null for row 1");
32-
}
27+
if (cursor != null) {
28+
try {
3329
if (cursor.moveToNext()) {
34-
throw new IllegalStateException("Cursor returned more than 1 row");
30+
item = mapper.call(cursor);
31+
if (item == null) {
32+
throw new NullPointerException("Mapper returned null for row 1");
33+
}
34+
if (cursor.moveToNext()) {
35+
throw new IllegalStateException("Cursor returned more than 1 row");
36+
}
3537
}
38+
} finally {
39+
cursor.close();
3640
}
37-
} finally {
38-
cursor.close();
3941
}
4042
if (!subscriber.isUnsubscribed()) {
4143
if (item != null) {

sqlbrite/src/main/java/com/squareup/sqlbrite/SqlBrite.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import android.database.sqlite.SQLiteOpenHelper;
2121
import android.support.annotation.CheckResult;
2222
import android.support.annotation.NonNull;
23+
import android.support.annotation.Nullable;
2324
import android.util.Log;
2425
import java.util.List;
2526
import rx.Observable;
@@ -89,6 +90,8 @@ public static abstract class Query {
8990
* It is an error for a query to pass through this operator with more than 1 row in its result
9091
* set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows
9192
* do not emit an item.
93+
* <p>
94+
* This operator ignores {@code null} cursors returned from {@link #run()}.
9295
*
9396
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
9497
*/
@@ -104,6 +107,8 @@ public static <T> Operator<T, Query> mapToOne(@NonNull Func1<Cursor, T> mapper)
104107
* It is an error for a query to pass through this operator with more than 1 row in its result
105108
* set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows
106109
* emit {@code defaultValue}.
110+
* <p>
111+
* This operator emits {@code defaultValue} if {@code null} is returned from {@link #run()}.
107112
*
108113
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
109114
* @param defaultValue Value returned if result set is empty
@@ -121,6 +126,8 @@ public static <T> Operator<T, Query> mapToOneOrDefault(@NonNull Func1<Cursor, T>
121126
* Be careful using this operator as it will always consume the entire cursor and create objects
122127
* for each row, every time this observable emits a new query. On tables whose queries update
123128
* frequently or very large result sets this can result in the creation of many objects.
129+
* <p>
130+
* This operator ignores {@code null} cursors returned from {@link #run()}.
124131
*
125132
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
126133
*/
@@ -129,9 +136,17 @@ public static <T> Operator<List<T>, Query> mapToList(@NonNull Func1<Cursor, T> m
129136
return new QueryToListOperator<>(mapper);
130137
}
131138

132-
/** Execute the query on the underlying database and return the resulting cursor. */
139+
/**
140+
* Execute the query on the underlying database and return the resulting cursor.
141+
*
142+
* @return A {@link Cursor} with query results, or {@code null} when the query could not be
143+
* executed due to a problem with the underlying store. Unfortunately it is not well documented
144+
* when {@code null} is returned. It usually involves a problem in communicating with the
145+
* underlying store and should either be treated as failure or ignored for retry at a later
146+
* time.
147+
*/
133148
@CheckResult // TODO @WorkerThread
134-
// TODO Implementations might return null, which is gross. Throw?
149+
@Nullable
135150
public abstract Cursor run();
136151

137152
/**
@@ -153,18 +168,22 @@ public static <T> Operator<List<T>, Query> mapToList(@NonNull Func1<Cursor, T> m
153168
* <p>
154169
* Note: Limiting results or filtering will almost always be faster in the database as part of
155170
* a query and should be preferred, where possible.
171+
* <p>
172+
* The resulting observable will be empty if {@code null} is returned from {@link #run()}.
156173
*/
157174
@CheckResult @NonNull
158175
public final <T> Observable<T> asRows(final Func1<Cursor, T> mapper) {
159176
return Observable.create(new Observable.OnSubscribe<T>() {
160177
@Override public void call(Subscriber<? super T> subscriber) {
161178
Cursor cursor = run();
162-
try {
163-
while (cursor.moveToNext() && !subscriber.isUnsubscribed()) {
164-
subscriber.onNext(mapper.call(cursor));
179+
if (cursor != null) {
180+
try {
181+
while (cursor.moveToNext() && !subscriber.isUnsubscribed()) {
182+
subscriber.onNext(mapper.call(cursor));
183+
}
184+
} finally {
185+
cursor.close();
165186
}
166-
} finally {
167-
cursor.close();
168187
}
169188
if (!subscriber.isUnsubscribed()) {
170189
subscriber.onCompleted();

0 commit comments

Comments
 (0)