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

Commit db9ba63

Browse files
committed
Provide convenience operators for mapping to a single item.
Queries returning single items is very common. It's not too terrible to do this yourself with Query#asRows, but we can provide something even more readable at little inconvenience.
1 parent 5c4fdff commit db9ba63

4 files changed

Lines changed: 283 additions & 40 deletions

File tree

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

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@
3434
import java.util.concurrent.TimeUnit;
3535
import org.junit.After;
3636
import org.junit.Before;
37+
import org.junit.Ignore;
3738
import org.junit.Test;
3839
import org.junit.runner.RunWith;
3940
import rx.Observable;
4041
import rx.Subscription;
42+
import rx.functions.Func1;
43+
import rx.observables.BlockingObservable;
4144

4245
import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE;
4346
import static com.google.common.truth.Truth.assertThat;
@@ -124,11 +127,134 @@ public final class BriteDatabaseTest {
124127
.toBlocking()
125128
.first();
126129
assertThat(employees).containsExactly( //
127-
new Employee("alice", "Alice Allison"),
128-
new Employee("bob", "Bob Bobberson"),
130+
new Employee("alice", "Alice Allison"), //
131+
new Employee("bob", "Bob Bobberson"), //
129132
new Employee("eve", "Eve Evenson"));
130133
}
131134

135+
@Test public void queryMapToListEmpty() {
136+
List<Employee> employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE 1=2")
137+
.mapToList(Employee.MAPPER)
138+
.toBlocking()
139+
.first();
140+
assertThat(employees).isEmpty();
141+
}
142+
143+
@Test public void queryMapToListMapperReturnNullThrows() {
144+
BlockingObservable<List<Employee>> employees =
145+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) //
146+
.mapToList(new Func1<Cursor, Employee>() {
147+
private int count;
148+
@Override public Employee call(Cursor cursor) {
149+
return count++ == 2 ? null : Employee.MAPPER.call(cursor);
150+
}
151+
}) //
152+
.toBlocking();
153+
try {
154+
employees.first();
155+
} catch (NullPointerException e) {
156+
assertThat(e).hasMessage("Mapper returned null for row 3");
157+
assertThat(e.getCause()).hasMessage(
158+
"OnError while emitting onNext value: SELECT username, name FROM employee");
159+
}
160+
}
161+
162+
@Test public void queryMapToOne() {
163+
Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1")
164+
.mapToOne(Employee.MAPPER)
165+
.toBlocking()
166+
.first();
167+
assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison"));
168+
}
169+
170+
@Ignore("How to test in black box way? Can't take(1).mapToOne() to trigger complete.") // TODO
171+
@Test public void queryMapToOneEmpty() {
172+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE 1=2")
173+
.mapToOne(Employee.MAPPER)
174+
.toBlocking()
175+
.first();
176+
}
177+
178+
@Test public void queryMapToOneMapperReturnNullThrows() {
179+
BlockingObservable<Employee> employees =
180+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) //
181+
.mapToOne(new Func1<Cursor, Employee>() {
182+
@Override public Employee call(Cursor cursor) {
183+
return null;
184+
}
185+
}) //
186+
.toBlocking();
187+
try {
188+
employees.first();
189+
} catch (NullPointerException e) {
190+
assertThat(e).hasMessage("Mapper returned null for row 1");
191+
assertThat(e.getCause()).hasMessage(
192+
"OnError while emitting onNext value: SELECT username, name FROM employee");
193+
}
194+
}
195+
196+
@Test public void queryMapToOneMultipleRowsThrows() {
197+
BlockingObservable<Employee> employees =
198+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") //
199+
.mapToOne(Employee.MAPPER) //
200+
.toBlocking();
201+
try {
202+
employees.first();
203+
} catch (IllegalStateException e) {
204+
assertThat(e).hasMessage("Cursor returned more than 1 row");
205+
assertThat(e.getCause()).hasMessage(
206+
"OnError while emitting onNext value: SELECT username, name FROM employee LIMIT 2");
207+
}
208+
}
209+
210+
@Test public void queryMapToOneOrNull() {
211+
Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1")
212+
.mapToOneOrNull(Employee.MAPPER)
213+
.toBlocking()
214+
.first();
215+
assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison"));
216+
}
217+
218+
@Test public void queryMapToOneOrNullEmpty() {
219+
Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE 1=2")
220+
.mapToOneOrNull(Employee.MAPPER)
221+
.toBlocking()
222+
.first();
223+
assertThat(employees).isNull();
224+
}
225+
226+
@Test public void queryMapToOneOrNullMapperReturnNullThrows() {
227+
BlockingObservable<Employee> employees =
228+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) //
229+
.mapToOneOrNull(new Func1<Cursor, Employee>() {
230+
@Override public Employee call(Cursor cursor) {
231+
return null;
232+
}
233+
}) //
234+
.toBlocking();
235+
try {
236+
employees.first();
237+
} catch (NullPointerException e) {
238+
assertThat(e).hasMessage("Mapper returned null for row 1");
239+
assertThat(e.getCause()).hasMessage(
240+
"OnError while emitting onNext value: SELECT username, name FROM employee");
241+
}
242+
}
243+
244+
@Test public void queryMapToOneOrNullMultipleRowsThrows() {
245+
BlockingObservable<Employee> employees =
246+
db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") //
247+
.mapToOneOrNull(Employee.MAPPER) //
248+
.toBlocking();
249+
try {
250+
employees.first();
251+
} catch (IllegalStateException e) {
252+
assertThat(e).hasMessage("Cursor returned more than 1 row");
253+
assertThat(e.getCause()).hasMessage(
254+
"OnError while emitting onNext value: SELECT username, name FROM employee LIMIT 2");
255+
}
256+
}
257+
132258
@Test public void badQueryCallsError() {
133259
db.createQuery(TABLE_EMPLOYEE, "SELECT * FROM missing").subscribe(o);
134260
o.assertErrorContains("no such table: missing");

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

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@
44
import android.support.annotation.CheckResult;
55
import android.support.annotation.NonNull;
66
import com.squareup.sqlbrite.SqlBrite.Query;
7-
import java.util.ArrayList;
87
import java.util.List;
98
import rx.Observable;
109
import rx.Subscriber;
11-
import rx.exceptions.Exceptions;
12-
import rx.exceptions.OnErrorThrowable;
1310
import rx.functions.Func1;
1411

1512
/** An {@link Observable} of {@link Query} which offers query-specific convenience operators. */
@@ -22,6 +19,46 @@ public final class QueryObservable extends Observable<Query> {
2219
});
2320
}
2421

22+
/**
23+
* Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each
24+
* emitted {@link Query} which returns a single row to {@code T}.
25+
* <p>
26+
* It is an error for a query to pass through this operator with more than 1 row in its result
27+
* set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows
28+
* do not emit an item.
29+
* <p>
30+
* This method is equivalent to:
31+
* <pre>{@code
32+
* flatMap(q -> q.asRows(mapper).take(1))
33+
* }</pre>
34+
*
35+
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
36+
*/
37+
@CheckResult @NonNull
38+
public final <T> Observable<T> mapToOne(@NonNull final Func1<Cursor, T> mapper) {
39+
return lift(new QueryToOneOperator<>(mapper, false));
40+
}
41+
42+
/**
43+
* Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each
44+
* emitted {@link Query} which returns a single row to {@code T}.
45+
* <p>
46+
* It is an error for a query to pass through this operator with more than 1 row in its result
47+
* set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows
48+
* emit {@code null}.
49+
* <p>
50+
* This method is equivalent to:
51+
* <pre>{@code
52+
* flatMap(q -> q.asRows(mapper).take(1).defaultIfEmpty(null))
53+
* }</pre>
54+
*
55+
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
56+
*/
57+
@CheckResult @NonNull
58+
public final <T> Observable<T> mapToOneOrNull(@NonNull final Func1<Cursor, T> mapper) {
59+
return lift(new QueryToOneOperator<>(mapper, true));
60+
}
61+
2562
/**
2663
* Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each
2764
* emitted {@link Query} to a {@code List<T>}.
@@ -32,45 +69,14 @@ public final class QueryObservable extends Observable<Query> {
3269
* <p>
3370
* This method is equivalent to:
3471
* <pre>{@code
35-
* flatMap(q -> q.asRows(Item.MAPPER).toList())
72+
* flatMap(q -> q.asRows(mapper).toList())
3673
* }</pre>
3774
* Consider using {@link Query#asRows} if you need to limit or filter in memory.
75+
*
76+
* @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null.
3877
*/
3978
@CheckResult @NonNull
4079
public final <T> Observable<List<T>> mapToList(@NonNull final Func1<Cursor, T> mapper) {
41-
return lift(new Operator<List<T>, Query>() {
42-
@Override
43-
public Subscriber<? super Query> call(final Subscriber<? super List<T>> subscriber) {
44-
return new Subscriber<Query>(subscriber) {
45-
@Override public void onNext(Query query) {
46-
try {
47-
Cursor cursor = query.run();
48-
List<T> items = new ArrayList<>(cursor.getCount());
49-
try {
50-
while (cursor.moveToNext() && !subscriber.isUnsubscribed()) {
51-
items.add(mapper.call(cursor));
52-
}
53-
} finally {
54-
cursor.close();
55-
}
56-
if (!subscriber.isUnsubscribed()) {
57-
subscriber.onNext(items);
58-
}
59-
} catch (Throwable e) {
60-
Exceptions.throwIfFatal(e);
61-
onError(OnErrorThrowable.addValueAsLastCause(e, query));
62-
}
63-
}
64-
65-
@Override public void onCompleted() {
66-
subscriber.onCompleted();
67-
}
68-
69-
@Override public void onError(Throwable e) {
70-
subscriber.onError(e);
71-
}
72-
};
73-
}
74-
});
80+
return lift(new QueryToListOperator<>(mapper));
7581
}
7682
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.squareup.sqlbrite;
2+
3+
import android.database.Cursor;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import rx.Observable;
7+
import rx.Subscriber;
8+
import rx.exceptions.Exceptions;
9+
import rx.exceptions.OnErrorThrowable;
10+
import rx.functions.Func1;
11+
12+
final class QueryToListOperator<T> implements Observable.Operator<List<T>, SqlBrite.Query> {
13+
private final Func1<Cursor, T> mapper;
14+
15+
QueryToListOperator(Func1<Cursor, T> mapper) {
16+
this.mapper = mapper;
17+
}
18+
19+
@Override
20+
public Subscriber<? super SqlBrite.Query> call(final Subscriber<? super List<T>> subscriber) {
21+
return new Subscriber<SqlBrite.Query>(subscriber) {
22+
@Override public void onNext(SqlBrite.Query query) {
23+
try {
24+
Cursor cursor = query.run();
25+
List<T> items = new ArrayList<>(cursor.getCount());
26+
try {
27+
for (int i = 1; cursor.moveToNext() && !subscriber.isUnsubscribed(); i++) {
28+
T item = mapper.call(cursor);
29+
if (item == null) {
30+
throw new NullPointerException("Mapper returned null for row " + i);
31+
}
32+
items.add(item);
33+
}
34+
} finally {
35+
cursor.close();
36+
}
37+
if (!subscriber.isUnsubscribed()) {
38+
subscriber.onNext(items);
39+
}
40+
} catch (Throwable e) {
41+
Exceptions.throwIfFatal(e);
42+
onError(OnErrorThrowable.addValueAsLastCause(e, query.toString()));
43+
}
44+
}
45+
46+
@Override public void onCompleted() {
47+
subscriber.onCompleted();
48+
}
49+
50+
@Override public void onError(Throwable e) {
51+
subscriber.onError(e);
52+
}
53+
};
54+
}
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.squareup.sqlbrite;
2+
3+
import android.database.Cursor;
4+
import rx.Observable;
5+
import rx.Subscriber;
6+
import rx.exceptions.Exceptions;
7+
import rx.exceptions.OnErrorThrowable;
8+
import rx.functions.Func1;
9+
10+
final class QueryToOneOperator<T> implements Observable.Operator<T, SqlBrite.Query> {
11+
private final Func1<Cursor, T> mapper;
12+
private final boolean emitNull;
13+
14+
QueryToOneOperator(Func1<Cursor, T> mapper, boolean emitNull) {
15+
this.mapper = mapper;
16+
this.emitNull = emitNull;
17+
}
18+
19+
@Override public Subscriber<? super SqlBrite.Query> call(final Subscriber<? super T> subscriber) {
20+
return new Subscriber<SqlBrite.Query>(subscriber) {
21+
@Override public void onNext(SqlBrite.Query query) {
22+
try {
23+
T item = null;
24+
Cursor cursor = query.run();
25+
try {
26+
if (cursor.moveToNext()) {
27+
item = mapper.call(cursor);
28+
if (item == null) {
29+
throw new NullPointerException("Mapper returned null for row 1");
30+
}
31+
if (cursor.moveToNext()) {
32+
throw new IllegalStateException("Cursor returned more than 1 row");
33+
}
34+
}
35+
} finally {
36+
cursor.close();
37+
}
38+
if (!subscriber.isUnsubscribed() && (item != null || emitNull)) {
39+
subscriber.onNext(item);
40+
}
41+
} catch (Throwable e) {
42+
Exceptions.throwIfFatal(e);
43+
onError(OnErrorThrowable.addValueAsLastCause(e, query.toString()));
44+
}
45+
}
46+
47+
@Override public void onCompleted() {
48+
subscriber.onCompleted();
49+
}
50+
51+
@Override public void onError(Throwable e) {
52+
subscriber.onError(e);
53+
}
54+
};
55+
}
56+
}

0 commit comments

Comments
 (0)