Skip to content

Commit fcae380

Browse files
committed
Convert query to pipeline
1 parent 8d049e0 commit fcae380

File tree

19 files changed

+1241
-65
lines changed

19 files changed

+1241
-65
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java

Lines changed: 889 additions & 0 deletions
Large diffs are not rendered by default.

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import com.google.firebase.firestore.FirebaseFirestoreSettings;
3939
import com.google.firebase.firestore.ListenerRegistration;
4040
import com.google.firebase.firestore.MetadataChanges;
41+
import com.google.firebase.firestore.PipelineResult;
42+
import com.google.firebase.firestore.PipelineSnapshot;
4143
import com.google.firebase.firestore.Query;
4244
import com.google.firebase.firestore.QuerySnapshot;
4345
import com.google.firebase.firestore.Source;
@@ -465,6 +467,15 @@ public static List<Map<String, Object>> querySnapshotToValues(QuerySnapshot quer
465467
return res;
466468
}
467469

470+
public static List<Map<String, Object>> pipelineSnapshotToValues(
471+
PipelineSnapshot pipelineSnapshot) {
472+
List<Map<String, Object>> res = new ArrayList<>();
473+
for (PipelineResult result : pipelineSnapshot) {
474+
res.add(result.getData());
475+
}
476+
return res;
477+
}
478+
468479
public static List<String> querySnapshotToIds(QuerySnapshot querySnapshot) {
469480
List<String> res = new ArrayList<>();
470481
for (DocumentSnapshot doc : querySnapshot) {
@@ -473,6 +484,15 @@ public static List<String> querySnapshotToIds(QuerySnapshot querySnapshot) {
473484
return res;
474485
}
475486

487+
public static List<String> pipelineSnapshotToIds(PipelineSnapshot pipelineResults) {
488+
List<String> res = new ArrayList<>();
489+
for (PipelineResult result : pipelineResults) {
490+
DocumentReference ref = result.getRef();
491+
res.add(ref == null ? null : ref.getId());
492+
}
493+
return res;
494+
}
495+
476496
public static void disableNetwork(FirebaseFirestore firestore) {
477497
if (firestoreStatus.get(firestore)) {
478498
waitFor(firestore.disableNetwork());
@@ -537,4 +557,23 @@ public static void checkOnlineAndOfflineResultsMatch(Query query, String... expe
537557
assertEquals(expected, querySnapshotToIds(docsFromCache));
538558
}
539559
}
560+
561+
/**
562+
* Checks that running the query while online (against the backend/emulator) results in the same
563+
* documents as running the query while offline. If `expectedDocs` is provided, it also checks
564+
* that both online and offline query result is equal to the expected documents.
565+
*
566+
* @param query The query to check
567+
* @param expectedDocs Ordered list of document keys that are expected to match the query
568+
*/
569+
public static void checkQueryAndPipelineResultsMatch(Query query, String... expectedDocs) {
570+
QuerySnapshot docsFromQuery = waitFor(query.get(Source.SERVER));
571+
PipelineSnapshot docsFromPipeline = waitFor(query.pipeline().execute());
572+
573+
assertEquals(querySnapshotToIds(docsFromQuery), pipelineSnapshotToIds(docsFromPipeline));
574+
List<String> expected = asList(expectedDocs);
575+
if (!expected.isEmpty()) {
576+
assertEquals(expected, querySnapshotToIds(docsFromQuery));
577+
}
578+
}
540579
}

firebase-firestore/src/main/java/com/google/firebase/firestore/CollectionReference.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public Task<DocumentReference> add(@NonNull Object data) {
130130
}
131131

132132
@NonNull
133+
@Override
133134
public Pipeline pipeline() {
134135
return new Pipeline(firestore, firestore.getUserDataReader(), new CollectionSource(getPath()));
135136
}

firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.google.common.collect.FluentIterable
2020
import com.google.common.collect.ImmutableList
2121
import com.google.firebase.firestore.model.DocumentKey
2222
import com.google.firebase.firestore.model.SnapshotVersion
23+
import com.google.firebase.firestore.model.Values
2324
import com.google.firebase.firestore.pipeline.AddFieldsStage
2425
import com.google.firebase.firestore.pipeline.AggregateStage
2526
import com.google.firebase.firestore.pipeline.AggregateWithAlias
@@ -123,9 +124,9 @@ internal constructor(
123124

124125
fun where(condition: BooleanExpr): Pipeline = append(WhereStage(condition))
125126

126-
fun offset(offset: Long): Pipeline = append(OffsetStage(offset))
127+
fun offset(offset: Int): Pipeline = append(OffsetStage(offset))
127128

128-
fun limit(limit: Long): Pipeline = append(LimitStage(limit))
129+
fun limit(limit: Int): Pipeline = append(LimitStage(limit))
129130

130131
fun distinct(vararg groups: Selectable): Pipeline = append(DistinctStage(groups))
131132

@@ -175,12 +176,15 @@ internal constructor(
175176
append(UnnestStage(selectable))
176177

177178
private inner class ObserverSnapshotTask : PipelineResultObserver {
179+
private val userDataWriter =
180+
UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT)
178181
private val taskCompletionSource = TaskCompletionSource<PipelineSnapshot>()
179182
private val results: ImmutableList.Builder<PipelineResult> = ImmutableList.builder()
180183
override fun onDocument(key: DocumentKey?, data: Map<String, Value>, version: SnapshotVersion) {
181184
results.add(
182185
PipelineResult(
183186
firestore,
187+
userDataWriter,
184188
if (key == null) null else DocumentReference(key, firestore),
185189
data,
186190
version
@@ -249,20 +253,52 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto
249253
}
250254

251255
class PipelineSnapshot
252-
internal constructor(private val executionTime: SnapshotVersion, val results: List<PipelineResult>)
256+
internal constructor(
257+
private val executionTime: SnapshotVersion,
258+
val results: List<PipelineResult>
259+
) : Iterable<PipelineResult> {
260+
override fun iterator() = results.iterator()
261+
}
253262

254263
class PipelineResult
255264
internal constructor(
256265
private val firestore: FirebaseFirestore,
266+
private val userDataWriter: UserDataWriter,
257267
val ref: DocumentReference?,
258268
private val fields: Map<String, Value>,
259269
private val version: SnapshotVersion,
260270
) {
261271

262-
fun getData(): Map<String, Any?> = userDataWriter().convertObject(fields)
272+
/**
273+
* Returns the ID of the document represented by this result. Returns null if this result is not
274+
* corresponding to a Firestore document.
275+
*/
276+
fun getId(): String? = ref?.id
277+
278+
fun getData(): Map<String, Any?> = userDataWriter.convertObject(fields)
279+
280+
private fun extractNestedValue(fieldPath: FieldPath): Value? {
281+
val segments = fieldPath.internalPath.iterator()
282+
if (!segments.hasNext()) {
283+
return Values.encodeValue(fields)
284+
}
285+
val firstSegment = segments.next()
286+
if (!fields.containsKey(firstSegment)) {
287+
return null
288+
}
289+
var value: Value? = fields[firstSegment]
290+
for (segment in segments) {
291+
if (value == null || !value.hasMapValue()) {
292+
return null
293+
}
294+
value = value.mapValue.getFieldsOrDefault(segment, null)
295+
}
296+
return value
297+
}
298+
299+
fun get(field: String): Any? = get(FieldPath.fromDotSeparatedPath(field))
263300

264-
private fun userDataWriter(): UserDataWriter =
265-
UserDataWriter(firestore, DocumentSnapshot.ServerTimestampBehavior.DEFAULT)
301+
fun get(fieldPath: FieldPath): Any? = userDataWriter.convertValue(extractNestedValue(fieldPath))
266302

267303
override fun toString() = "PipelineResult{ref=$ref, version=$version}, data=${getData()}"
268304
}

firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,11 @@ public AggregateQuery aggregate(
12411241
return new AggregateQuery(this, fields);
12421242
}
12431243

1244+
@NonNull
1245+
public Pipeline pipeline() {
1246+
return query.toPipeline(firestore, firestore.getUserDataReader());
1247+
}
1248+
12441249
@Override
12451250
public boolean equals(Object o) {
12461251
if (this == o) {

firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte
230230

231231
Object converted = CustomClassMapper.convertToPlainJavaTypes(input);
232232
Value parsedValue = parseData(converted, context);
233-
if (parsedValue.getValueTypeCase() != Value.ValueTypeCase.MAP_VALUE) {
233+
if (!parsedValue.hasMapValue()) {
234234
throw new IllegalArgumentException(badDocReason + "of type: " + Util.typeName(input));
235235
}
236236
return new ObjectValue(parsedValue);

firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataWriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public Object convertValue(Value value) {
7878
case TYPE_ORDER_BOOLEAN:
7979
return value.getBooleanValue();
8080
case TYPE_ORDER_NUMBER:
81-
return value.getValueTypeCase().equals(Value.ValueTypeCase.INTEGER_VALUE)
81+
return value.hasIntegerValue()
8282
? (Object) value.getIntegerValue() // Cast to Object to prevent type coercion to double
8383
: (Object) value.getDoubleValue();
8484
case TYPE_ORDER_STRING:

firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import android.text.TextUtils;
1818
import androidx.annotation.Nullable;
1919
import com.google.firebase.firestore.model.Document;
20+
import com.google.firebase.firestore.pipeline.BooleanExpr;
2021
import com.google.firebase.firestore.util.Function;
2122
import java.util.ArrayList;
2223
import java.util.Collections;
@@ -167,6 +168,19 @@ public String getCanonicalId() {
167168
return builder.toString();
168169
}
169170

171+
@Override
172+
BooleanExpr toPipelineExpr() {
173+
BooleanExpr[] booleanExprs = filters.stream().map(Filter::toPipelineExpr).toArray(BooleanExpr[]::new);
174+
switch (operator) {
175+
case AND:
176+
return new BooleanExpr("and", booleanExprs);
177+
case OR:
178+
return new BooleanExpr("or", booleanExprs);
179+
}
180+
// Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed
181+
throw new IllegalArgumentException("Unsupported operator: " + operator);
182+
}
183+
170184
@Override
171185
public String toString() {
172186
return getCanonicalId();

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414

1515
package com.google.firebase.firestore.core;
1616

17+
import static com.google.firebase.firestore.pipeline.Function.and;
1718
import static com.google.firebase.firestore.util.Assert.hardAssert;
19+
import static java.lang.Double.isNaN;
1820

1921
import com.google.firebase.firestore.model.Document;
2022
import com.google.firebase.firestore.model.FieldPath;
2123
import com.google.firebase.firestore.model.Values;
24+
import com.google.firebase.firestore.pipeline.BooleanExpr;
25+
import com.google.firebase.firestore.pipeline.Field;
2226
import com.google.firebase.firestore.util.Assert;
2327
import com.google.firestore.v1.Value;
2428
import java.util.Arrays;
@@ -172,6 +176,49 @@ public List<Filter> getFilters() {
172176
return Collections.singletonList(this);
173177
}
174178

179+
@Override
180+
BooleanExpr toPipelineExpr() {
181+
Field x = new Field(field);
182+
BooleanExpr exists = x.exists();
183+
switch (operator) {
184+
case LESS_THAN:
185+
return and(exists, x.lt(value));
186+
case LESS_THAN_OR_EQUAL:
187+
return and(exists, x.lte(value));
188+
case EQUAL:
189+
if (value.hasNullValue()) {
190+
return and(exists, x.isNull());
191+
} else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) {
192+
return and(exists, x.isNan());
193+
} else {
194+
return and(exists, x.eq(value));
195+
}
196+
case NOT_EQUAL:
197+
if (value.hasNullValue()) {
198+
return and(exists, x.isNotNull());
199+
} else if (value.hasDoubleValue() && isNaN(value.getDoubleValue())) {
200+
return and(exists, x.isNotNan());
201+
} else {
202+
return and(exists, x.neq(value));
203+
}
204+
case GREATER_THAN:
205+
return and(exists, x.gt(value));
206+
case GREATER_THAN_OR_EQUAL:
207+
return and(exists, x.gte(value));
208+
case ARRAY_CONTAINS:
209+
return and(exists, x.arrayContains(value));
210+
case ARRAY_CONTAINS_ANY:
211+
return and(exists, x.arrayContainsAny(value.getArrayValue().getValuesList()));
212+
case IN:
213+
return and(exists, x.eqAny(value.getArrayValue().getValuesList()));
214+
case NOT_IN:
215+
return and(exists, x.notEqAny(value.getArrayValue().getValuesList()));
216+
default:
217+
// Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed
218+
throw new IllegalArgumentException("Unsupported operator: " + operator);
219+
}
220+
}
221+
175222
@Override
176223
public String toString() {
177224
return getCanonicalId();

firebase-firestore/src/main/java/com/google/firebase/firestore/core/Filter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.firestore.core;
1616

1717
import com.google.firebase.firestore.model.Document;
18+
import com.google.firebase.firestore.pipeline.BooleanExpr;
1819
import java.util.List;
1920

2021
public abstract class Filter {
@@ -29,4 +30,6 @@ public abstract class Filter {
2930

3031
/** Returns a list of all filters that are contained within this filter */
3132
public abstract List<Filter> getFilters();
33+
34+
abstract BooleanExpr toPipelineExpr();
3235
}

0 commit comments

Comments
 (0)