diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java index 8b40a1b98ea..93f54924ec9 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryToPipelineTest.java @@ -28,8 +28,6 @@ import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; -import static com.google.firebase.firestore.testutil.TestUtil.expectError; -import static com.google.firebase.firestore.testutil.TestUtil.map; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index 0509e9a94c5..79002aacf88 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -31,6 +31,7 @@ import com.google.firebase.firestore.core.AsyncEventListener; import com.google.firebase.firestore.core.EventManager.ListenOptions; import com.google.firebase.firestore.core.QueryListener; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.UserData.ParsedSetData; import com.google.firebase.firestore.core.UserData.ParsedUpdateData; import com.google.firebase.firestore.core.ViewSnapshot; @@ -542,7 +543,8 @@ private ListenerRegistration addSnapshotListenerInternal( return firestore.callClient( client -> { - QueryListener queryListener = client.listen(query, options, asyncListener); + QueryListener queryListener = + client.listen(new QueryOrPipeline.QueryWrapper(query), options, asyncListener); return ActivityScope.bind( activity, () -> { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt index 1954e04cc16..e0094a46fa8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Pipeline.kt @@ -21,6 +21,7 @@ import com.google.firebase.firestore.core.Canonicalizable import com.google.firebase.firestore.model.Document import com.google.firebase.firestore.model.DocumentKey import com.google.firebase.firestore.model.MutableDocument +import com.google.firebase.firestore.model.ResourcePath import com.google.firebase.firestore.model.Values import com.google.firebase.firestore.pipeline.AddFieldsStage import com.google.firebase.firestore.pipeline.AggregateFunction @@ -43,7 +44,6 @@ import com.google.firebase.firestore.pipeline.InternalOptions import com.google.firebase.firestore.pipeline.LimitStage import com.google.firebase.firestore.pipeline.OffsetStage import com.google.firebase.firestore.pipeline.Ordering -import com.google.firebase.firestore.pipeline.PipelineOptions import com.google.firebase.firestore.pipeline.RawStage import com.google.firebase.firestore.pipeline.RemoveFieldsStage import com.google.firebase.firestore.pipeline.ReplaceStage @@ -55,17 +55,28 @@ import com.google.firebase.firestore.pipeline.Stage import com.google.firebase.firestore.pipeline.UnionStage import com.google.firebase.firestore.pipeline.UnnestStage import com.google.firebase.firestore.pipeline.WhereStage +import com.google.firebase.firestore.remote.RemoteSerializer import com.google.firebase.firestore.util.Assert.fail import com.google.firestore.v1.ExecutePipelineRequest import com.google.firestore.v1.StructuredPipeline import com.google.firestore.v1.Value -open class AbstractPipeline +class Pipeline internal constructor( - internal val firestore: FirebaseFirestore, - internal val userDataReader: UserDataReader, - internal val stages: List> + private val firestore: FirebaseFirestore, + private val userDataReader: UserDataReader, + private val stages: List> ) { + internal constructor( + firestore: FirebaseFirestore, + userDataReader: UserDataReader, + stage: Stage<*> + ) : this(firestore, userDataReader, listOf(stage)) + + private fun append(stage: Stage<*>): Pipeline { + return Pipeline(firestore, userDataReader, stages.plus(stage)) + } + private fun toStructuredPipelineProto(options: InternalOptions?): StructuredPipeline { val builder = StructuredPipeline.newBuilder() builder.pipeline = toPipelineProto() @@ -79,17 +90,17 @@ internal constructor( .build() private fun toExecutePipelineRequest(options: InternalOptions?): ExecutePipelineRequest { - val database = firestore.databaseId + val database = firestore!!.databaseId val builder = ExecutePipelineRequest.newBuilder() builder.database = "projects/${database.projectId}/databases/${database.databaseId}" builder.structuredPipeline = toStructuredPipelineProto(options) return builder.build() } - protected fun execute(options: InternalOptions?): Task { + fun execute(options: InternalOptions?): Task { val request = toExecutePipelineRequest(options) val observerTask = ObserverSnapshotTask() - firestore.callClient { call -> call!!.executePipeline(request, observerTask) } + firestore?.callClient { call -> call!!.executePipeline(request, observerTask) } return observerTask.task } @@ -106,7 +117,7 @@ internal constructor( ) { results.add( PipelineResult( - firestore, + firestore!!, userDataWriter, if (key == null) null else DocumentReference(key, firestore), data, @@ -127,28 +138,9 @@ internal constructor( val task: Task get() = taskCompletionSource.task } -} - -class Pipeline -private constructor( - firestore: FirebaseFirestore, - userDataReader: UserDataReader, - stages: List> -) : AbstractPipeline(firestore, userDataReader, stages) { - internal constructor( - firestore: FirebaseFirestore, - userDataReader: UserDataReader, - stage: Stage<*> - ) : this(firestore, userDataReader, listOf(stage)) - - private fun append(stage: Stage<*>): Pipeline { - return Pipeline(firestore, userDataReader, stages.plus(stage)) - } fun execute(): Task = execute(null) - fun execute(options: PipelineOptions): Task = execute(options.options) - internal fun documentReference(key: DocumentKey): DocumentReference { return DocumentReference(key, firestore) } @@ -627,7 +619,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @param path A path to a collection that will be the source of this pipeline. * @return A new [Pipeline] object with documents from target collection. */ - fun collection(path: String): Pipeline = collection(CollectionSource.of(path)) + fun collection(path: String): Pipeline = collection(firestore.collection(path)) /** * Set the pipeline's source to the collection specified by the given [CollectionReference]. @@ -637,7 +629,8 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @throws [IllegalArgumentException] Thrown if the [ref] provided targets a different project or * database than the pipeline. */ - fun collection(ref: CollectionReference): Pipeline = collection(CollectionSource.of(ref)) + fun collection(ref: CollectionReference): Pipeline = + collection(CollectionSource.of(ref, firestore.databaseId)) /** * Set the pipeline's source to the collection specified by CollectionSource. @@ -648,7 +641,7 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * or database than the pipeline. */ fun collection(stage: CollectionSource): Pipeline { - if (stage.firestore != null && stage.firestore.databaseId != firestore.databaseId) { + if (stage.serializer.databaseId() != firestore.databaseId) { throw IllegalArgumentException("Provided collection is from a different Firestore instance.") } return Pipeline(firestore, firestore.userDataReader, stage) @@ -661,9 +654,9 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto * @return A new [Pipeline] object with documents from target collection group. */ fun collectionGroup(collectionId: String): Pipeline = - pipeline(CollectionGroupSource.of((collectionId))) + collectionGroup(CollectionGroupSource.of((collectionId))) - fun pipeline(stage: CollectionGroupSource): Pipeline = + internal fun collectionGroup(stage: CollectionGroupSource): Pipeline = Pipeline(firestore, firestore.userDataReader, stage) /** @@ -706,12 +699,26 @@ class PipelineSource internal constructor(private val firestore: FirebaseFiresto return Pipeline( firestore, firestore.userDataReader, - DocumentsSource(documents.map { docRef -> "/" + docRef.path }.toTypedArray()) + DocumentsSource(documents.map { ResourcePath.fromString(it.path) }.toTypedArray()) ) } } class RealtimePipelineSource internal constructor(private val firestore: FirebaseFirestore) { + /** + * Convert the given Query into an equivalent Pipeline. + * + * @param query A Query to be converted into a Pipeline. + * @return A new [Pipeline] object that is equivalent to [query] + * @throws [IllegalArgumentException] Thrown if the [query] provided targets a different project + * or database than the pipeline. + */ + fun convertFrom(query: Query): RealtimePipeline { + if (query.firestore.databaseId != firestore.databaseId) { + throw IllegalArgumentException("Provided query is from a different Firestore instance.") + } + return query.query.toRealtimePipeline(firestore, firestore.userDataReader) + } /** * Set the pipeline's source to the collection specified by the given path. @@ -719,7 +726,7 @@ class RealtimePipelineSource internal constructor(private val firestore: Firebas * @param path A path to a collection that will be the source of this pipeline. * @return A new [RealtimePipeline] object with documents from target collection. */ - fun collection(path: String): RealtimePipeline = collection(CollectionSource.of(path)) + fun collection(path: String): RealtimePipeline = collection(firestore.collection(path)) /** * Set the pipeline's source to the collection specified by the given [CollectionReference]. @@ -729,7 +736,8 @@ class RealtimePipelineSource internal constructor(private val firestore: Firebas * @throws [IllegalArgumentException] Thrown if the [ref] provided targets a different project or * database than the pipeline. */ - fun collection(ref: CollectionReference): RealtimePipeline = collection(CollectionSource.of(ref)) + fun collection(ref: CollectionReference): RealtimePipeline = + collection(CollectionSource.of(ref, firestore.databaseId)) /** * Set the pipeline's source to the collection specified by CollectionSource. @@ -740,10 +748,10 @@ class RealtimePipelineSource internal constructor(private val firestore: Firebas * or database than the pipeline. */ fun collection(stage: CollectionSource): RealtimePipeline { - if (stage.firestore != null && stage.firestore.databaseId != firestore.databaseId) { + if (stage.serializer.databaseId() != firestore.databaseId) { throw IllegalArgumentException("Provided collection is from a different Firestore instance.") } - return RealtimePipeline(firestore, firestore.userDataReader, stage) + return RealtimePipeline(RemoteSerializer(firestore.databaseId), firestore.userDataReader, stage) } /** @@ -753,26 +761,26 @@ class RealtimePipelineSource internal constructor(private val firestore: Firebas * @return A new [RealtimePipeline] object with documents from target collection group. */ fun collectionGroup(collectionId: String): RealtimePipeline = - pipeline(CollectionGroupSource.of((collectionId))) + collectionGroup(CollectionGroupSource.of((collectionId))) - fun pipeline(stage: CollectionGroupSource): RealtimePipeline = - RealtimePipeline(firestore, firestore.userDataReader, stage) + fun collectionGroup(stage: CollectionGroupSource): RealtimePipeline = + RealtimePipeline(RemoteSerializer(firestore.databaseId), firestore.userDataReader, stage) } class RealtimePipeline internal constructor( - firestore: FirebaseFirestore, - userDataReader: UserDataReader, - stages: List> -) : AbstractPipeline(firestore, userDataReader, stages), Canonicalizable { + internal val serializer: RemoteSerializer, + internal val userDataReader: UserDataReader, + internal val stages: List> +) : Canonicalizable { internal constructor( - firestore: FirebaseFirestore, + serializer: RemoteSerializer, userDataReader: UserDataReader, stage: Stage<*> - ) : this(firestore, userDataReader, listOf(stage)) + ) : this(serializer, userDataReader, listOf(stage)) private fun with(stages: List>): RealtimePipeline = - RealtimePipeline(firestore, userDataReader, stages) + RealtimePipeline(serializer, userDataReader, stages) private fun append(stage: Stage<*>): RealtimePipeline = with(stages.plus(stage)) @@ -820,14 +828,17 @@ internal constructor( return rewrittenStages.joinToString("|") { stage -> (stage as Canonicalizable).canonicalId() } } + override fun toString(): String = canonicalId() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is RealtimePipeline) return false - return stages == other.stages + if (serializer.databaseId() != other.serializer.databaseId()) return false + return rewrittenStages == other.rewrittenStages } override fun hashCode(): Int { - return stages.hashCode() + return serializer.databaseId().hashCode() * 31 + stages.hashCode() } internal fun evaluate(inputs: List): List { @@ -883,6 +894,15 @@ internal constructor( internal fun comparator(): Comparator = getLastEffectiveSortStage().comparator(evaluateContext()) + internal fun toStructurePipelineProto(): StructuredPipeline { + val builder = StructuredPipeline.newBuilder() + builder.pipeline = + com.google.firestore.v1.Pipeline.newBuilder() + .addAllStages(rewrittenStages.map { it.toProtoStage(userDataReader) }) + .build() + return builder.build() + } + private fun getLastEffectiveSortStage(): SortStage { for (stage in rewrittenStages.asReversed()) { if (stage is SortStage) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index d5fb8a4399b..d4419319d1b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -35,6 +35,7 @@ import com.google.firebase.firestore.core.FieldFilter.Operator; import com.google.firebase.firestore.core.OrderBy; import com.google.firebase.firestore.core.QueryListener; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.ViewSnapshot; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; @@ -964,7 +965,8 @@ public Task get(@NonNull Source source) { validateHasExplicitOrderByForLimitToLast(); if (source == Source.CACHE) { return firestore - .callClient(client -> client.getDocumentsFromLocalCache(query)) + .callClient( + client -> client.getDocumentsFromLocalCache(new QueryOrPipeline.QueryWrapper(query))) .continueWith( Executors.DIRECT_EXECUTOR, (Task viewSnap) -> @@ -1182,7 +1184,8 @@ private ListenerRegistration addSnapshotListenerInternal( return firestore.callClient( client -> { - QueryListener queryListener = client.listen(query, options, asyncListener); + QueryListener queryListener = + client.listen(new QueryOrPipeline.QueryWrapper(query), options, asyncListener); return ActivityScope.bind( activity, () -> { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java index 97c9d9d33e1..ec630125b38 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java @@ -65,6 +65,10 @@ public UserDataReader(DatabaseId databaseId) { this.databaseId = databaseId; } + public DatabaseId getDatabaseId() { + return databaseId; + } + /** * Parse document data from a non-merge {@code set()} call. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java index afb8c66278a..d5f00222bd0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java @@ -71,7 +71,7 @@ public static class ListenOptions { private final SyncEngine syncEngine; - private final Map queries; + private final Map queries; private final Set> snapshotsInSyncListeners = new HashSet<>(); @@ -105,7 +105,7 @@ private enum ListenerRemovalAction { * @return the targetId of the listen call in the SyncEngine. */ public int addQueryListener(QueryListener queryListener) { - Query query = queryListener.getQuery(); + QueryOrPipeline query = queryListener.getQuery(); ListenerSetupAction listenerAction = ListenerSetupAction.NO_ACTION_REQUIRED; QueryListenersInfo queryInfo = queries.get(query); @@ -163,7 +163,7 @@ public int addQueryListener(QueryListener queryListener) { /** Removes a previously added listener. It's a no-op if the listener is not found. */ public void removeQueryListener(QueryListener listener) { - Query query = listener.getQuery(); + QueryOrPipeline query = listener.getQuery(); QueryListenersInfo queryInfo = queries.get(query); ListenerRemovalAction listenerAction = ListenerRemovalAction.NO_ACTION_REQUIRED; if (queryInfo == null) return; @@ -223,7 +223,7 @@ private void raiseSnapshotsInSyncEvent() { public void onViewSnapshots(List snapshotList) { boolean raisedEvent = false; for (ViewSnapshot viewSnapshot : snapshotList) { - Query query = viewSnapshot.getQuery(); + QueryOrPipeline query = viewSnapshot.getQuery(); QueryListenersInfo info = queries.get(query); if (info != null) { for (QueryListener listener : info.listeners) { @@ -240,7 +240,7 @@ public void onViewSnapshots(List snapshotList) { } @Override - public void onError(Query query, Status error) { + public void onError(QueryOrPipeline query, Status error) { QueryListenersInfo info = queries.get(query); if (info != null) { for (QueryListener listener : info.listeners) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index d54e3458d52..f4c2aa59bb6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -172,7 +172,7 @@ public boolean isTerminated() { /** Starts listening to a query. */ public QueryListener listen( - Query query, ListenOptions options, EventListener listener) { + QueryOrPipeline query, ListenOptions options, EventListener listener) { this.verifyNotTerminated(); QueryListener queryListener = new QueryListener(query, options, listener); asyncQueue.enqueueAndForget(() -> eventManager.addQueryListener(queryListener)); @@ -208,7 +208,7 @@ public Task getDocumentFromLocalCache(DocumentKey docKey) { }); } - public Task getDocumentsFromLocalCache(Query query) { + public Task getDocumentsFromLocalCache(QueryOrPipeline query) { this.verifyNotTerminated(); return asyncQueue.enqueue( () -> { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/PipelineUtil.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/PipelineUtil.kt index 095f10822ed..1d215be76f8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/PipelineUtil.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/PipelineUtil.kt @@ -116,6 +116,28 @@ sealed class TargetOrPipeline { return (this as PipelineWrapper).pipeline } + val singleDocPath: ResourcePath? + get() { + return when (this) { + is PipelineWrapper -> { + if (getPipelineSourceType(pipeline) == PipelineSourceType.DOCUMENTS) { + val docs = getPipelineDocuments(pipeline) + if (docs != null && docs.size == 1) { + return ResourcePath.fromString(docs[0]) + } + } + return null + } + is TargetWrapper -> { + if (target.isDocumentQuery) { + return target.path + } + + return null + } + } + } + fun canonicalId(): String { return when (this) { is PipelineWrapper -> pipeline.canonicalId() @@ -201,7 +223,7 @@ fun getPipelineCollection(pipeline: RealtimePipeline): String? { ) val firstStage = pipeline.stages.first() if (firstStage is CollectionSource) { - return firstStage.path + return firstStage.path.canonicalString() } } return null @@ -216,7 +238,7 @@ fun getPipelineDocuments(pipeline: RealtimePipeline): Array? { ) val firstStage = pipeline.stages.first() if (firstStage is DocumentsSource) { - return firstStage.documents + return firstStage.documents.map { it.canonicalString() }.toTypedArray() } } return null @@ -231,7 +253,7 @@ fun asCollectionPipelineAtPath( val newStages = pipeline.stages.map { stagePtr -> if (stagePtr is CollectionGroupSource) { - CollectionSource(path.canonicalString(), pipeline.firestore, InternalOptions.EMPTY) + CollectionSource(path, pipeline.serializer, InternalOptions.EMPTY) } else { stagePtr } @@ -240,7 +262,7 @@ fun asCollectionPipelineAtPath( // Construct a new RealtimePipeline with the (potentially) modified stages // and the original user_data_reader. return RealtimePipeline( - pipeline.firestore, + pipeline.serializer, pipeline.userDataReader, newStages, ) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index f4ecf8fcbc7..8b7eee25a81 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -22,8 +22,10 @@ import androidx.annotation.Nullable; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.Pipeline; +import com.google.firebase.firestore.RealtimePipeline; import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.core.OrderBy.Direction; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldPath; @@ -36,8 +38,12 @@ import com.google.firebase.firestore.pipeline.Field; import com.google.firebase.firestore.pipeline.FunctionExpr; import com.google.firebase.firestore.pipeline.InternalOptions; +import com.google.firebase.firestore.pipeline.LimitStage; import com.google.firebase.firestore.pipeline.Ordering; +import com.google.firebase.firestore.pipeline.SortStage; import com.google.firebase.firestore.pipeline.Stage; +import com.google.firebase.firestore.pipeline.WhereStage; +import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.util.BiFunction; import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.IntFunction; @@ -524,11 +530,25 @@ private synchronized Target toTarget(List orderBys) { @NonNull public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataReader) { - Pipeline p = new Pipeline(firestore, userDataReader, pipelineSource(firestore)); + return new Pipeline(firestore, userDataReader, convertToStages(userDataReader)); + } + + @NonNull + public RealtimePipeline toRealtimePipeline( + FirebaseFirestore firestore, UserDataReader userDataReader) { + return new RealtimePipeline( + new RemoteSerializer(userDataReader.getDatabaseId()), + userDataReader, + convertToStages(userDataReader)); + } + + private List> convertToStages(UserDataReader userDataReader) { + List> stages = new ArrayList<>(); + stages.add(pipelineSource(userDataReader.getDatabaseId())); // Filters for (Filter filter : filters) { - p = p.where(filter.toPipelineExpr()); + stages.add(new WhereStage(filter.toPipelineExpr(), InternalOptions.EMPTY)); } // Orders @@ -547,40 +567,46 @@ public Pipeline toPipeline(FirebaseFirestore firestore, UserDataReader userDataR } if (fields.size() == 1) { - p = p.where(fields.get(0).exists()); + stages.add(new WhereStage(fields.get(0).exists(), InternalOptions.EMPTY)); } else { BooleanExpr[] conditions = skipFirstToArray(fields, BooleanExpr[]::new, Expr.Companion::exists); - p = p.where(and(fields.get(0).exists(), conditions)); + stages.add(new WhereStage(and(fields.get(0).exists(), conditions), InternalOptions.EMPTY)); } if (startAt != null) { - p = p.where(whereConditionsFromCursor(startAt, fields, FunctionExpr::gt)); + stages.add( + new WhereStage( + whereConditionsFromCursor(startAt, fields, FunctionExpr::gt), InternalOptions.EMPTY)); } if (endAt != null) { - p = p.where(whereConditionsFromCursor(endAt, fields, FunctionExpr::lt)); + stages.add( + new WhereStage( + whereConditionsFromCursor(endAt, fields, FunctionExpr::lt), InternalOptions.EMPTY)); } // Cursors, Limit, Offset if (hasLimit()) { // TODO: Handle situation where user enters limit larger than integer. if (limitType == LimitType.LIMIT_TO_FIRST) { - p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); - p = p.limit((int) limit); + stages.add(new SortStage(orderings.toArray(new Ordering[0]), InternalOptions.EMPTY)); + stages.add(new LimitStage((int) limit, InternalOptions.EMPTY)); } else { - p = - p.sort( - orderings.get(0).reverse(), - skipFirstToArray(orderings, Ordering[]::new, Ordering::reverse)); - p = p.limit((int) limit); - p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); + List reversedOrderings = new ArrayList<>(); + for (Ordering ordering : orderings) { + reversedOrderings.add(ordering.reverse()); + } + stages.add( + new SortStage(reversedOrderings.toArray(new Ordering[0]), InternalOptions.EMPTY)); + stages.add(new LimitStage((int) limit, InternalOptions.EMPTY)); + stages.add(new SortStage(orderings.toArray(new Ordering[0]), InternalOptions.EMPTY)); } } else { - p = p.sort(orderings.get(0), skipFirstToArray(orderings, Ordering[]::new)); + stages.add(new SortStage(orderings.toArray(new Ordering[0]), InternalOptions.EMPTY)); } - return p; + return stages; } // Many Pipelines require first parameter to be separated out from rest. @@ -623,13 +649,13 @@ private static BooleanExpr whereConditionsFromCursor( } @NonNull - private Stage pipelineSource(FirebaseFirestore firestore) { + private Stage pipelineSource(DatabaseId databaseId) { if (isDocumentQuery()) { return new DocumentsSource(path.canonicalString()); } else if (isCollectionGroupQuery()) { return CollectionGroupSource.of(collectionGroup); } else { - return new CollectionSource(path.canonicalString(), firestore, InternalOptions.EMPTY); + return new CollectionSource(path, new RemoteSerializer(databaseId), InternalOptions.EMPTY); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java index 045fe43119c..9ab268e51eb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java @@ -33,7 +33,7 @@ * only from our worker thread. */ public class QueryListener { - private final Query query; + private final QueryOrPipeline query; private final EventManager.ListenOptions options; @@ -50,13 +50,15 @@ public class QueryListener { private @Nullable ViewSnapshot snapshot; public QueryListener( - Query query, EventManager.ListenOptions options, EventListener listener) { + QueryOrPipeline query, + EventManager.ListenOptions options, + EventListener listener) { this.query = query; this.listener = listener; this.options = options; } - public Query getQuery() { + public QueryOrPipeline getQuery() { return query; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryView.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryView.java index 3f6a87478b2..1959b66a75a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryView.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryView.java @@ -19,17 +19,17 @@ * view. */ final class QueryView { - private final Query query; + private final QueryOrPipeline query; private final int targetId; private final View view; - QueryView(Query query, int targetId, View view) { + QueryView(QueryOrPipeline query, int targetId, View view) { this.query = query; this.targetId = targetId; this.view = view; } - public Query getQuery() { + public QueryOrPipeline getQuery() { return query; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index e9bb7bafea6..ee0bc898971 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -110,7 +110,7 @@ interface SyncEngineCallback { void onViewSnapshots(List snapshotList); /** Handles the failure of a query. */ - void onError(Query query, Status error); + void onError(QueryOrPipeline query, Status error); /** Handles a change in online state. */ void handleOnlineStateChange(OnlineState onlineState); @@ -123,10 +123,10 @@ interface SyncEngineCallback { private final RemoteStore remoteStore; /** QueryViews for all active queries, indexed by query. */ - private final Map queryViewsByQuery; + private final Map queryViewsByQuery; /** Queries mapped to active targets, indexed by target id. */ - private final Map> queriesByTarget; + private final Map> queriesByTarget; private final int maxConcurrentLimboResolutions; @@ -200,11 +200,11 @@ private void assertCallback(String method) { * * @return the target ID assigned to the query. */ - public int listen(Query query, boolean shouldListenToRemote) { + public int listen(QueryOrPipeline query, boolean shouldListenToRemote) { assertCallback("listen"); hardAssert(!queryViewsByQuery.containsKey(query), "We already listen to query: %s", query); - TargetData targetData = localStore.allocateTarget(query.toTarget()); + TargetData targetData = localStore.allocateTarget(query.toTargetOrPipeline()); ViewSnapshot viewSnapshot = initializeViewAndComputeSnapshot( @@ -219,7 +219,7 @@ public int listen(Query query, boolean shouldListenToRemote) { } private ViewSnapshot initializeViewAndComputeSnapshot( - Query query, int targetId, ByteString resumeToken) { + QueryOrPipeline query, int targetId, ByteString resumeToken) { QueryResult queryResult = localStore.executeQuery(query, /* usePreviousResults= */ true); SyncState currentTargetSyncState = SyncState.NONE; @@ -228,7 +228,7 @@ private ViewSnapshot initializeViewAndComputeSnapshot( // If there are already queries mapped to the target id, create a synthesized target change to // apply the sync state from those queries to the new query. if (this.queriesByTarget.get(targetId) != null) { - Query mirrorQuery = this.queriesByTarget.get(targetId).get(0); + QueryOrPipeline mirrorQuery = this.queriesByTarget.get(targetId).get(0); currentTargetSyncState = this.queryViewsByQuery.get(mirrorQuery).getView().getSyncState(); } synthesizedCurrentChange = @@ -260,12 +260,12 @@ private ViewSnapshot initializeViewAndComputeSnapshot( * Sends the listen to the RemoteStore to get remote data. Invoked when a Query starts listening * to the remote store, while already listening to the cache. */ - public void listenToRemoteStore(Query query) { + public void listenToRemoteStore(QueryOrPipeline query) { assertCallback("listenToRemoteStore"); hardAssert( queryViewsByQuery.containsKey(query), "This is the first listen to query: %s", query); - TargetData targetData = localStore.allocateTarget(query.toTarget()); + TargetData targetData = localStore.allocateTarget(query.toTargetOrPipeline()); remoteStore.listen(targetData); } @@ -273,7 +273,7 @@ public void listenToRemoteStore(Query query) { * Stops listening to a query previously listened. Un-listen to remote store if there is a watch * connection established and stayed open. */ - void stopListening(Query query, boolean shouldUnlistenToRemote) { + void stopListening(QueryOrPipeline query, boolean shouldUnlistenToRemote) { assertCallback("stopListening"); QueryView queryView = queryViewsByQuery.get(query); @@ -282,7 +282,7 @@ void stopListening(Query query, boolean shouldUnlistenToRemote) { queryViewsByQuery.remove(query); int targetId = queryView.getTargetId(); - List targetQueries = queriesByTarget.get(targetId); + List targetQueries = queriesByTarget.get(targetId); targetQueries.remove(query); if (targetQueries.isEmpty()) { @@ -298,13 +298,13 @@ void stopListening(Query query, boolean shouldUnlistenToRemote) { * Stops listening to a query from watch. Invoked when a Query stops listening to the remote * store, while still listening to the cache. */ - void stopListeningToRemoteStore(Query query) { + void stopListeningToRemoteStore(QueryOrPipeline query) { assertCallback("stopListeningToRemoteStore"); QueryView queryView = queryViewsByQuery.get(query); hardAssert(queryView != null, "Trying to stop listening to a query not found"); int targetId = queryView.getTargetId(); - List targetQueries = queriesByTarget.get(targetId); + List targetQueries = queriesByTarget.get(targetId); targetQueries.remove(query); if (targetQueries.isEmpty()) { @@ -409,7 +409,7 @@ public void handleRemoteEvent(RemoteEvent event) { public void handleOnlineStateChange(OnlineState onlineState) { assertCallback("handleOnlineStateChange"); ArrayList newViewSnapshots = new ArrayList<>(); - for (Map.Entry entry : queryViewsByQuery.entrySet()) { + for (Map.Entry entry : queryViewsByQuery.entrySet()) { View view = entry.getValue().getView(); ViewChange viewChange = view.applyOnlineStateChange(onlineState); hardAssert( @@ -430,7 +430,7 @@ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { } else { ImmutableSortedSet remoteKeys = DocumentKey.emptyKeySet(); if (queriesByTarget.containsKey(targetId)) { - for (Query query : queriesByTarget.get(targetId)) { + for (QueryOrPipeline query : queriesByTarget.get(targetId)) { if (queryViewsByQuery.containsKey(query)) { remoteKeys = remoteKeys.unionWith(queryViewsByQuery.get(query).getView().getSyncedDocuments()); @@ -636,7 +636,7 @@ private void notifyUser(int batchId, @Nullable Status status) { } private void removeAndCleanupTarget(int targetId, Status status) { - for (Query query : queriesByTarget.get(targetId)) { + for (QueryOrPipeline query : queriesByTarget.get(targetId)) { queryViewsByQuery.remove(query); if (!status.isOk()) { syncEngineListener.onError(query, status); @@ -677,7 +677,7 @@ private void emitNewSnapsAndNotifyLocalStore( List newSnapshots = new ArrayList<>(); List documentChangesInAllViews = new ArrayList<>(); - for (Map.Entry entry : queryViewsByQuery.entrySet()) { + for (Map.Entry entry : queryViewsByQuery.entrySet()) { QueryView queryView = entry.getValue(); View view = queryView.getView(); View.DocumentChanges viewDocChanges = view.computeDocChanges(changes); @@ -762,7 +762,7 @@ private void pumpEnqueuedLimboResolutions() { activeLimboTargetsByKey.put(key, limboTargetId); remoteStore.listen( new TargetData( - Query.atPath(key.getPath()).toTarget(), + new TargetOrPipeline.TargetWrapper(Query.atPath(key.getPath()).toTarget()), limboTargetId, ListenSequence.INVALID, QueryPurpose.LIMBO_RESOLUTION)); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java index 849f0b10f9e..057eb56e62e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java @@ -14,18 +14,19 @@ package com.google.firebase.firestore.core; -import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_FIRST; -import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_LAST; +import static com.google.firebase.firestore.core.PipelineUtilKt.getLastEffectiveLimit; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.DocumentViewChange.Type; +import com.google.firebase.firestore.core.Query.LimitType; import com.google.firebase.firestore.core.ViewSnapshot.SyncState; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.DocumentSet; +import com.google.firebase.firestore.model.MutableDocument; import com.google.firebase.firestore.remote.TargetChange; import java.util.ArrayList; import java.util.Collections; @@ -70,7 +71,21 @@ public boolean needsRefill() { } } - private final Query query; + /** A pair of documents that represent the edges of a limit query. */ + // TODO(pipeline): This is a direct port from C++. Ideally this should be a sumtype with variances + // of + // endAt(doc), startAt(doc), and noLimit(). + private static class LimitEdges { + @Nullable final Document first; + @Nullable final Document second; + + LimitEdges(@Nullable Document first, @Nullable Document second) { + this.first = first; + this.second = second; + } + } + + private final QueryOrPipeline query; private SyncState syncState; @@ -92,7 +107,7 @@ public boolean needsRefill() { /** Documents that have local changes */ private ImmutableSortedSet mutatedKeys; - public View(Query query, ImmutableSortedSet remoteDocuments) { + public View(QueryOrPipeline query, ImmutableSortedSet remoteDocuments) { this.query = query; syncState = SyncState.NONE; documentSet = DocumentSet.emptySet(query.comparator()); @@ -147,14 +162,9 @@ public DocumentChanges computeDocChanges( // // Note that this should never get used in a refill (when previousChanges is set), because there // will only be adds -- no deletes or updates. - Document lastDocInLimit = - (query.getLimitType().equals(LIMIT_TO_FIRST) && oldDocumentSet.size() == query.getLimit()) - ? oldDocumentSet.getLastDocument() - : null; - Document firstDocInLimit = - (query.getLimitType().equals(LIMIT_TO_LAST) && oldDocumentSet.size() == query.getLimit()) - ? oldDocumentSet.getFirstDocument() - : null; + LimitEdges limitEdges = getLimitEdges(this.query, oldDocumentSet); + Document lastDocInLimit = limitEdges.first; + Document firstDocInLimit = limitEdges.second; for (Map.Entry entry : docChanges) { DocumentKey key = entry.getKey(); @@ -223,15 +233,42 @@ public DocumentChanges computeDocChanges( } // Drop documents out to meet limitToFirst/limitToLast requirement. - if (query.hasLimit()) { - for (long i = newDocumentSet.size() - query.getLimit(); i > 0; --i) { - Document oldDoc = - query.getLimitType().equals(LIMIT_TO_FIRST) - ? newDocumentSet.getLastDocument() - : newDocumentSet.getFirstDocument(); - newDocumentSet = newDocumentSet.remove(oldDoc.getKey()); - newMutatedKeys = newMutatedKeys.remove(oldDoc.getKey()); - changeSet.addChange(DocumentViewChange.create(Type.REMOVED, oldDoc)); + Long limit = getLimit(this.query); + if (limit != null) { + if (this.query.isPipeline()) { + // TODO(pipeline): Not very efficient obviously, but should be fine for now. + // Longer term, limit queries should be evaluated from query engine as well. + List candidates = new ArrayList<>(); + for (Document doc : newDocumentSet) { + candidates.add((MutableDocument) doc); + } + List results = + this.query.pipeline().evaluate$com_google_firebase_firebase_firestore(candidates); + DocumentSet newResults = DocumentSet.emptySet(query.comparator()); + for (MutableDocument doc : results) { + newResults = newResults.add(doc); + } + + for (Document doc : newDocumentSet) { + if (!newResults.contains(doc.getKey())) { + newMutatedKeys = newMutatedKeys.remove(doc.getKey()); + changeSet.addChange(DocumentViewChange.create(Type.REMOVED, doc)); + } + } + + newDocumentSet = newResults; + } else { + long absLimit = Math.abs(limit); + LimitType limitType = getLimitType(this.query); + for (long i = newDocumentSet.size() - absLimit; i > 0; --i) { + Document oldDoc = + limitType == LimitType.LIMIT_TO_FIRST + ? newDocumentSet.getLastDocument() + : newDocumentSet.getFirstDocument(); + newDocumentSet = newDocumentSet.remove(oldDoc.getKey()); + newMutatedKeys = newMutatedKeys.remove(oldDoc.getKey()); + changeSet.addChange(DocumentViewChange.create(Type.REMOVED, oldDoc)); + } } } @@ -460,4 +497,57 @@ private static int changeTypeOrder(DocumentViewChange change) { } throw new IllegalArgumentException("Unknown change type: " + change.getType()); } + + @Nullable + private static Long getLimit(QueryOrPipeline query) { + if (query.isPipeline()) { + Integer limit = getLastEffectiveLimit(query.pipeline()); + if (limit == null) { + return null; + } + return Long.valueOf(limit); + } else { + Query q = query.query(); + if (!q.hasLimit()) { + return null; + } + return q.getLimit(); + } + } + + private static LimitType getLimitType(QueryOrPipeline query) { + if (query.isPipeline()) { + Long limit = getLimit(query); + // Note: A limit of 0 is not a valid pipeline limit. + return limit != null && limit > 0 ? LimitType.LIMIT_TO_FIRST : LimitType.LIMIT_TO_LAST; + } else { + return query.query().getLimitType(); + } + } + + private static LimitEdges getLimitEdges(QueryOrPipeline query, DocumentSet oldDocumentSet) { + Long limit = getLimit(query); + if (limit == null) { + return new LimitEdges(null, null); + } + + if (query.isPipeline()) { + // The GetLimit function already encodes this as a negative number. + if (limit > 0 && oldDocumentSet.size() == limit) { + return new LimitEdges(oldDocumentSet.getLastDocument(), null); + } else if (limit < 0 && oldDocumentSet.size() == (-limit)) { + return new LimitEdges(null, oldDocumentSet.getFirstDocument()); + } + } else { + Query q = query.query(); + if (q.getLimitType() == Query.LimitType.LIMIT_TO_FIRST + && oldDocumentSet.size() == q.getLimit()) { + return new LimitEdges(oldDocumentSet.getLastDocument(), null); + } else if (q.getLimitType() == Query.LimitType.LIMIT_TO_LAST + && oldDocumentSet.size() == q.getLimit()) { + return new LimitEdges(null, oldDocumentSet.getFirstDocument()); + } + } + return new LimitEdges(null, null); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java index 2741815c1d4..669195187d6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java @@ -31,7 +31,7 @@ public enum SyncState { SYNCED } - private final Query query; + private final QueryOrPipeline query; private final DocumentSet documents; private final DocumentSet oldDocuments; private final List changes; @@ -42,7 +42,7 @@ public enum SyncState { private boolean hasCachedResults; public ViewSnapshot( - Query query, + QueryOrPipeline query, DocumentSet documents, DocumentSet oldDocuments, List changes, @@ -64,7 +64,7 @@ public ViewSnapshot( /** Returns a view snapshot as if all documents in the snapshot were added. */ public static ViewSnapshot fromInitialDocuments( - Query query, + QueryOrPipeline query, DocumentSet documents, ImmutableSortedSet mutatedKeys, boolean fromCache, @@ -86,7 +86,7 @@ public static ViewSnapshot fromInitialDocuments( hasCachedResults); } - public Query getQuery() { + public QueryOrPipeline getQuery() { return query; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java index 04955f4f0c2..9b88066022d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java @@ -14,6 +14,11 @@ package com.google.firebase.firestore.local; +import static com.google.firebase.firestore.core.PipelineUtilKt.asCollectionPipelineAtPath; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineCollection; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineCollectionGroup; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineDocuments; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineSourceType; import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap; import static com.google.firebase.firestore.util.Assert.hardAssert; @@ -21,7 +26,10 @@ import androidx.annotation.VisibleForTesting; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedMap; +import com.google.firebase.firestore.RealtimePipeline; +import com.google.firebase.firestore.core.PipelineSourceType; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex; @@ -33,6 +41,8 @@ import com.google.firebase.firestore.model.mutation.MutationBatch; import com.google.firebase.firestore.model.mutation.Overlay; import com.google.firebase.firestore.model.mutation.PatchMutation; +import com.google.firebase.firestore.util.Function; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -262,14 +272,17 @@ void recalculateAndSaveOverlays(Set documentKeys) { * query execution. */ ImmutableSortedMap getDocumentsMatchingQuery( - Query query, IndexOffset offset, @Nullable QueryContext context) { - ResourcePath path = query.getPath(); - if (query.isDocumentQuery()) { - return getDocumentsMatchingDocumentQuery(path); - } else if (query.isCollectionGroupQuery()) { - return getDocumentsMatchingCollectionGroupQuery(query, offset, context); + QueryOrPipeline query, IndexOffset offset, @Nullable QueryContext context) { + if (query.isQuery()) { + if (query.query().isDocumentQuery()) { + return getDocumentsMatchingDocumentQuery(query.query().getPath()); + } else if (query.query().isCollectionGroupQuery()) { + return getDocumentsMatchingCollectionGroupQuery(query.query(), offset, context); + } else { + return getDocumentsMatchingCollectionQuery(query.query(), offset, context); + } } else { - return getDocumentsMatchingCollectionQuery(query, offset, context); + return getDocumentsMatchingPipeline(query, offset, context); } } @@ -280,7 +293,7 @@ ImmutableSortedMap getDocumentsMatchingQuery( * @param offset Read time and key to start scanning by (exclusive). */ ImmutableSortedMap getDocumentsMatchingQuery( - Query query, IndexOffset offset) { + QueryOrPipeline query, IndexOffset offset) { return getDocumentsMatchingQuery(query, offset, /*context*/ null); } @@ -379,7 +392,75 @@ private ImmutableSortedMap getDocumentsMatchingCollection Map overlays = documentOverlayCache.getOverlays(query.getPath(), offset.getLargestBatchId()); Map remoteDocuments = - remoteDocumentCache.getDocumentsMatchingQuery(query, offset, overlays.keySet(), context); + remoteDocumentCache.getDocumentsMatchingQuery( + new QueryOrPipeline.QueryWrapper(query), offset, overlays.keySet(), context); + + return retrieveMatchingLocalDocuments(overlays, remoteDocuments, query::matches); + } + + private ImmutableSortedMap getDocumentsMatchingPipeline( + QueryOrPipeline queryOrPipeline, IndexOffset offset, @Nullable QueryContext context) { + RealtimePipeline pipeline = queryOrPipeline.pipeline(); + if (getPipelineSourceType(pipeline) == PipelineSourceType.COLLECTION_GROUP) { + String collectionGroup = getPipelineCollectionGroup(pipeline); + hardAssert( + collectionGroup != null, "Pipeline source type is COLLECTION_GROUP but is missing"); + + ImmutableSortedMap results = emptyDocumentMap(); + List parents = indexManager.getCollectionParents(collectionGroup); + + for (ResourcePath parent : parents) { + RealtimePipeline collectionPipeline = + asCollectionPipelineAtPath(pipeline, parent.append(collectionGroup)); + ImmutableSortedMap collectionResults = + getDocumentsMatchingPipeline( + new QueryOrPipeline.PipelineWrapper(collectionPipeline), offset, context); + for (Map.Entry kv : collectionResults) { + results = results.insert(kv.getKey(), kv.getValue()); + } + } + return results; + + } else { + Map overlays = + getOverlaysForPipeline(pipeline, offset.getLargestBatchId()); + + Map remoteDocuments; + switch (getPipelineSourceType(pipeline)) { + case COLLECTION: + remoteDocuments = + remoteDocumentCache.getDocumentsMatchingQuery( + queryOrPipeline, offset, overlays.keySet(), context); + break; + case DOCUMENTS: + List documentPaths = Arrays.asList(getPipelineDocuments(pipeline)); + Set keySet = new HashSet<>(); + for (String path : documentPaths) { + keySet.add(DocumentKey.fromPathString(path)); + } + remoteDocuments = remoteDocumentCache.getAll(keySet); + break; + default: + throw new IllegalArgumentException( + "Invalid pipeline source to execute offline: " + pipeline); + } + + return retrieveMatchingLocalDocuments( + overlays, remoteDocuments, pipeline::matches$com_google_firebase_firebase_firestore); + } + } + + /** Returns a base document that can be used to apply `overlay`. */ + private MutableDocument getBaseDocument(DocumentKey key, @Nullable Overlay overlay) { + return (overlay == null || overlay.getMutation() instanceof PatchMutation) + ? remoteDocumentCache.get(key) + : MutableDocument.newInvalidDocument(key); + } + + private ImmutableSortedMap retrieveMatchingLocalDocuments( + Map overlays, + Map remoteDocuments, + Function matcher) { // As documents might match the query because of their overlay we need to include documents // for all overlays in the initial document set. @@ -399,7 +480,7 @@ private ImmutableSortedMap getDocumentsMatchingCollection .applyToLocalView(docEntry.getValue(), FieldMask.EMPTY, Timestamp.now()); } // Finally, insert the documents that still match the query - if (query.matches(docEntry.getValue())) { + if (matcher.apply(docEntry.getValue())) { results = results.insert(docEntry.getKey(), docEntry.getValue()); } } @@ -407,10 +488,29 @@ private ImmutableSortedMap getDocumentsMatchingCollection return results; } - /** Returns a base document that can be used to apply `overlay`. */ - private MutableDocument getBaseDocument(DocumentKey key, @Nullable Overlay overlay) { - return (overlay == null || overlay.getMutation() instanceof PatchMutation) - ? remoteDocumentCache.get(key) - : MutableDocument.newInvalidDocument(key); + private Map getOverlaysForPipeline( + RealtimePipeline pipeline, int largestBatchId) { + switch (getPipelineSourceType(pipeline)) { + case COLLECTION: + { + String collection = getPipelineCollection(pipeline); + hardAssert(collection != null, "Pipeline source type is COLLECTION but is missing"); + return documentOverlayCache.getOverlays( + ResourcePath.fromString(collection), largestBatchId); + } + case DOCUMENTS: + { + List documents = Arrays.asList(getPipelineDocuments(pipeline)); + hardAssert(documents != null, "Pipeline source type is DOCUMENTS but is missing"); + SortedSet keySet = new TreeSet<>(); + for (String keyString : documents) { + keySet.add(DocumentKey.fromPathString(keyString)); + } + return documentOverlayCache.getOverlays(keySet); + } + default: + throw new IllegalArgumentException( + "GetOverlaysForPipeline: Unrecognized pipeline source type for pipeline " + pipeline); + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java index 98fb7230821..b0015c48e9e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java @@ -21,6 +21,7 @@ import com.google.firebase.firestore.bundle.BundledQuery; import com.google.firebase.firestore.core.Query.LimitType; import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex; @@ -221,11 +222,20 @@ com.google.firebase.firestore.proto.Target encodeTargetData(TargetData targetDat .setSnapshotVersion(rpcSerializer.encodeVersion(targetData.getSnapshotVersion())) .setResumeToken(targetData.getResumeToken()); - Target target = targetData.getTarget(); - if (target.isDocumentQuery()) { - result.setDocuments(rpcSerializer.encodeDocumentsTarget(target)); + TargetOrPipeline target = targetData.getTarget(); + if (target.isTarget()) { + if (target.target().isDocumentQuery()) { + result.setDocuments(rpcSerializer.encodeDocumentsTarget(target.target())); + } else { + result.setQuery(rpcSerializer.encodeQueryTarget(target.target())); + } } else { - result.setQuery(rpcSerializer.encodeQueryTarget(target)); + result.setPipelineQuery( + com.google.firestore.v1.Target.PipelineQueryTarget.newBuilder() + .setStructuredPipeline( + target + .pipeline() + .toStructurePipelineProto$com_google_firebase_firebase_firestore())); } return result.build(); @@ -239,14 +249,24 @@ TargetData decodeTargetData(com.google.firebase.firestore.proto.Target targetPro ByteString resumeToken = targetProto.getResumeToken(); long sequenceNumber = targetProto.getLastListenSequenceNumber(); - Target target; + TargetOrPipeline target; switch (targetProto.getTargetTypeCase()) { case DOCUMENTS: - target = rpcSerializer.decodeDocumentsTarget(targetProto.getDocuments()); + target = + new TargetOrPipeline.TargetWrapper( + rpcSerializer.decodeDocumentsTarget(targetProto.getDocuments())); break; case QUERY: - target = rpcSerializer.decodeQueryTarget(targetProto.getQuery()); + target = + new TargetOrPipeline.TargetWrapper( + rpcSerializer.decodeQueryTarget(targetProto.getQuery())); + break; + + case PIPELINE_QUERY: + target = + new TargetOrPipeline.PipelineWrapper( + rpcSerializer.decodePipelineQueryTarget(targetProto.getPipelineQuery())); break; default: diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 9d268a503ba..a214c61e353 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -30,8 +30,10 @@ import com.google.firebase.firestore.bundle.BundleMetadata; import com.google.firebase.firestore.bundle.NamedQuery; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.Target; import com.google.firebase.firestore.core.TargetIdGenerator; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex; @@ -145,7 +147,7 @@ public final class LocalStore implements BundleCallback { private final SparseArray queryDataByTarget; /** Maps a target to its targetID. */ - private final Map targetIdByTarget; + private final Map targetIdByTarget; /** Used to generate targetIds for queries tracked locally. */ private final TargetIdGenerator targetIdGenerator; @@ -660,7 +662,7 @@ public Document readDocument(DocumentKey key) { *

Allocating an already allocated target will return the existing @{code TargetData} for that * target. */ - public TargetData allocateTarget(Target target) { + public TargetData allocateTarget(TargetOrPipeline target) { int targetId; TargetData cached = targetCache.getTargetData(target); if (cached != null) { @@ -698,7 +700,7 @@ public TargetData allocateTarget(Target target) { */ @VisibleForTesting @Nullable - TargetData getTargetData(Target target) { + TargetData getTargetData(TargetOrPipeline target) { Integer targetId = targetIdByTarget.get(target); if (targetId != null) { return queryDataByTarget.get(targetId); @@ -735,7 +737,8 @@ public ImmutableSortedMap applyBundledDocuments( ImmutableSortedMap documents, String bundleId) { // Allocates a target to hold all document keys from the bundle, such that // they will not get garbage collected right away. - TargetData umbrellaTargetData = allocateTarget(newUmbrellaTarget(bundleId)); + TargetData umbrellaTargetData = + allocateTarget(new TargetOrPipeline.TargetWrapper(newUmbrellaTarget(bundleId))); return persistence.runTransaction( "Apply bundle documents", @@ -768,7 +771,9 @@ public void saveNamedQuery(NamedQuery namedQuery, ImmutableSortedSet remoteKeys = DocumentKey.emptyKeySet(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java index 36f028f4309..d15174cc9fe 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java @@ -14,17 +14,19 @@ package com.google.firebase.firestore.local; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineCollection; import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedMap; -import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; import com.google.firebase.firestore.model.MutableDocument; +import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.SnapshotVersion; import java.util.Collection; import java.util.HashMap; @@ -98,7 +100,7 @@ public Map getAll( @Override public Map getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, IndexOffset offset, @NonNull Set mutatedKeys, @Nullable QueryContext context) { @@ -106,7 +108,13 @@ public Map getDocumentsMatchingQuery( // Documents are ordered by key, so we can use a prefix scan to narrow down the documents // we need to match the query against. - DocumentKey prefix = DocumentKey.fromPath(query.getPath().append("")); + ResourcePath path; + if (query.isQuery()) { + path = query.query().getPath(); + } else { + path = ResourcePath.fromString(getPipelineCollection(query.pipeline())); + } + DocumentKey prefix = DocumentKey.fromPath(path.append("")); Iterator> iterator = docs.iteratorFrom(prefix); while (iterator.hasNext()) { @@ -114,12 +122,12 @@ public Map getDocumentsMatchingQuery( Document doc = entry.getValue(); DocumentKey key = entry.getKey(); - if (!query.getPath().isPrefixOf(key.getPath())) { + if (!path.isPrefixOf(key.getPath())) { // We are now scanning the next collection. Abort. break; } - if (key.getPath().length() > query.getPath().length() + 1) { + if (key.getPath().length() > path.length() + 1) { // Exclude entries from subcollections. continue; } @@ -141,7 +149,7 @@ public Map getDocumentsMatchingQuery( @Override public Map getDocumentsMatchingQuery( - Query query, IndexOffset offset, @NonNull Set mutatedKeys) { + QueryOrPipeline query, IndexOffset offset, @NonNull Set mutatedKeys) { return getDocumentsMatchingQuery(query, offset, mutatedKeys, /*context*/ null); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java index 41881f16f04..61e005a4790 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java @@ -17,7 +17,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedSet; -import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.util.Consumer; @@ -32,7 +32,7 @@ final class MemoryTargetCache implements TargetCache { /** Maps a target to the data about that target. */ - private final Map targets = new HashMap<>(); + private final Map targets = new HashMap<>(); /** A ordered bidirectional mapping between documents and the remote target IDs. */ private final ReferenceSet references = new ReferenceSet(); @@ -117,9 +117,9 @@ public void removeTargetData(TargetData targetData) { */ int removeQueries(long upperBound, SparseArray activeTargetIds) { int removed = 0; - for (Iterator> it = targets.entrySet().iterator(); + for (Iterator> it = targets.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); + Map.Entry entry = it.next(); int targetId = entry.getValue().getTargetId(); long sequenceNumber = entry.getValue().getSequenceNumber(); if (sequenceNumber <= upperBound && activeTargetIds.get(targetId) == null) { @@ -133,7 +133,7 @@ int removeQueries(long upperBound, SparseArray activeTargetIds) { @Nullable @Override - public TargetData getTargetData(Target target) { + public TargetData getTargetData(TargetOrPipeline target) { return targets.get(target); } @@ -174,7 +174,7 @@ public boolean containsKey(DocumentKey key) { long getByteSize(LocalSerializer serializer) { long count = 0; - for (Map.Entry entry : targets.entrySet()) { + for (Map.Entry entry : targets.entrySet()) { count += serializer.encodeTargetData(entry.getValue()).getSerializedSize(); } return count; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/QueryEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/QueryEngine.java index e12dc86209a..f5fd3c8f6d0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/QueryEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/QueryEngine.java @@ -20,6 +20,7 @@ import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.Target; import com.google.firebase.firestore.local.IndexManager.IndexType; import com.google.firebase.firestore.model.Document; @@ -93,7 +94,7 @@ public void setIndexAutoCreationEnabled(boolean isEnabled) { } public ImmutableSortedMap getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, SnapshotVersion lastLimboFreeSnapshotVersion, ImmutableSortedSet remoteKeys) { hardAssert(initialized, "initialize() not called"); @@ -120,7 +121,12 @@ public ImmutableSortedMap getDocumentsMatchingQuery( * Decides whether SDK should create a full matched field index for this query based on query * context and query result size. */ - private void createCacheIndexes(Query query, QueryContext context, int resultSize) { + private void createCacheIndexes(QueryOrPipeline query, QueryContext context, int resultSize) { + if (query.isPipeline()) { + Logger.debug(LOG_TAG, "SDK will skip creating cache indexes for pipelines."); + return; + } + if (context.getDocumentReadCount() < indexAutoCreationMinCollectionSize) { Logger.debug( LOG_TAG, @@ -139,7 +145,7 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz resultSize); if (context.getDocumentReadCount() > relativeIndexReadCostPerDocument * resultSize) { - indexManager.createTargetIndexes(query.toTarget()); + indexManager.createTargetIndexes(query.query().toTarget()); Logger.debug( LOG_TAG, "The SDK decides to create cache indexes for query: %s, as using cache indexes " @@ -152,13 +158,19 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz * Performs an indexed query that evaluates the query based on a collection's persisted index * values. Returns {@code null} if an index is not available. */ - private @Nullable ImmutableSortedMap performQueryUsingIndex(Query query) { - if (query.matchesAllDocuments()) { + private @Nullable ImmutableSortedMap performQueryUsingIndex( + QueryOrPipeline query) { + if (query.isPipeline()) { + Logger.debug(LOG_TAG, "Skipping using indexes for pipelines."); + return null; + } + + if (query.query().matchesAllDocuments()) { // Don't use indexes for queries that can be executed by scanning the collection. return null; } - Target target = query.toTarget(); + Target target = query.query().toTarget(); IndexType indexType = indexManager.getIndexType(target); if (indexType.equals(IndexType.NONE)) { @@ -166,14 +178,15 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz return null; } - if (query.hasLimit() && indexType.equals(IndexType.PARTIAL)) { + if (query.query().hasLimit() && indexType.equals(IndexType.PARTIAL)) { // We cannot apply a limit for targets that are served using a partial index. // If a partial index will be used to serve the target, the query may return a superset of // documents that match the target (for example, if the index doesn't include all the target's // filters), or may return the correct set of documents in the wrong order (for example, if // the index doesn't include a segment for one of the orderBys). Therefore a limit should not // be applied in such cases. - return performQueryUsingIndex(query.limitToFirst(Target.NO_LIMIT)); + return performQueryUsingIndex( + new QueryOrPipeline.QueryWrapper(query.query().limitToFirst(Target.NO_LIMIT))); } List keys = indexManager.getDocumentsMatchingTarget(target); @@ -189,7 +202,8 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz // by excluding the limit. This ensures that all documents that match the query's filters are // included in the result set. The SDK can then apply the limit once all local edits are // incorporated. - return performQueryUsingIndex(query.limitToFirst(Target.NO_LIMIT)); + return performQueryUsingIndex( + new QueryOrPipeline.QueryWrapper(query.query().limitToFirst(Target.NO_LIMIT))); } return appendRemainingResults(previousResults, query, offset); @@ -200,7 +214,7 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz * mapping is not available or cannot be used. */ private @Nullable ImmutableSortedMap performQueryUsingRemoteKeys( - Query query, + QueryOrPipeline query, ImmutableSortedSet remoteKeys, SnapshotVersion lastLimboFreeSnapshotVersion) { if (query.matchesAllDocuments()) { @@ -239,7 +253,7 @@ private void createCacheIndexes(Query query, QueryContext context, int resultSiz /** Applies the query filter and sorting to the provided documents. */ private ImmutableSortedSet applyQuery( - Query query, ImmutableSortedMap documents) { + QueryOrPipeline query, ImmutableSortedMap documents) { // Sort the documents and re-apply the query filter since previously matching documents do not // necessarily still match the query. ImmutableSortedSet queryResults = @@ -267,11 +281,18 @@ private ImmutableSortedSet applyQuery( * synchronized. */ private boolean needsRefill( - Query query, + QueryOrPipeline query, int expectedDocumentCount, ImmutableSortedSet sortedPreviousResults, SnapshotVersion limboFreeSnapshotVersion) { - if (!query.hasLimit()) { + if (query.isPipeline()) { + // TODO(pipeline): For pipelines it is simple for now, we refill for all + // limit/offset. we should implement a similar approach like query at some + // point. + return query.hasLimit(); + } + + if (!query.query().hasLimit()) { // Queries without limits do not need to be refilled. return false; } @@ -288,7 +309,7 @@ private boolean needsRefill( // did not change and documents from cache will continue to be "rejected" by this boundary. // Therefore, we can ignore any modifications that don't affect the last document. Document documentAtLimitEdge = - query.getLimitType() == Query.LimitType.LIMIT_TO_FIRST + query.query().getLimitType() == Query.LimitType.LIMIT_TO_FIRST ? sortedPreviousResults.getMaxEntry() : sortedPreviousResults.getMinEntry(); if (documentAtLimitEdge == null) { @@ -300,7 +321,7 @@ private boolean needsRefill( } private ImmutableSortedMap executeFullCollectionScan( - Query query, QueryContext context) { + QueryOrPipeline query, QueryContext context) { if (Logger.isDebugEnabled()) { Logger.debug(LOG_TAG, "Using full collection scan to execute query: %s", query.toString()); } @@ -312,7 +333,7 @@ private ImmutableSortedMap executeFullCollectionScan( * been indexed. */ private ImmutableSortedMap appendRemainingResults( - Iterable indexedResults, Query query, IndexOffset offset) { + Iterable indexedResults, QueryOrPipeline query, IndexOffset offset) { // Retrieve all results for documents that were updated since the offset. ImmutableSortedMap remainingResults = localDocumentsView.getDocumentsMatchingQuery(query, offset); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java index 8ff90864342..5b0601a2ea1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java @@ -14,7 +14,7 @@ package com.google.firebase.firestore.local; -import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; import com.google.firebase.firestore.model.MutableDocument; @@ -89,7 +89,7 @@ interface RemoteDocumentCache { * @return A newly created map with the set of documents in the collection. */ Map getDocumentsMatchingQuery( - Query query, IndexOffset offset, @Nonnull Set mutatedKeys); + QueryOrPipeline query, IndexOffset offset, @Nonnull Set mutatedKeys); /** * Returns the documents that match the given query. @@ -103,7 +103,7 @@ Map getDocumentsMatchingQuery( * @return A newly created map with the set of documents in the collection. */ Map getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, IndexOffset offset, @Nonnull Set mutatedKeys, @Nullable QueryContext context); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java index 850734a14f2..62d9fd35bb6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.local; +import static com.google.firebase.firestore.core.PipelineUtilKt.getPipelineCollection; import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap; import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; @@ -24,7 +25,7 @@ import androidx.annotation.VisibleForTesting; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedMap; -import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; @@ -255,18 +256,28 @@ private void processRowInBackground( @Override public Map getDocumentsMatchingQuery( - Query query, IndexOffset offset, @Nonnull Set mutatedKeys) { + QueryOrPipeline query, IndexOffset offset, @Nonnull Set mutatedKeys) { return getDocumentsMatchingQuery(query, offset, mutatedKeys, /*context*/ null); } @Override public Map getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, IndexOffset offset, @Nonnull Set mutatedKeys, @Nullable QueryContext context) { + ResourcePath path = ResourcePath.EMPTY; + if (query.isQuery()) { + path = query.query().getPath(); + } else { + String pathString = getPipelineCollection(query.pipeline()); + hardAssert( + pathString != null, + "SQLiteRemoteDocumentCache.getDocumentsMatchingQuery receives pipeline without collection source."); + path = ResourcePath.fromString(pathString); + } return getAll( - Collections.singletonList(query.getPath()), + Collections.singletonList(path), offset, Integer.MAX_VALUE, (MutableDocument doc) -> query.matches(doc) || mutatedKeys.contains(doc.getKey()), diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java index ce758195446..0cf7d2fc85c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java @@ -614,7 +614,7 @@ private void rewriteCanonicalIds() { try { Target targetProto = Target.parseFrom(targetProtoBytes); TargetData targetData = serializer.decodeTargetData(targetProto); - String updatedCanonicalId = targetData.getTarget().getCanonicalId(); + String updatedCanonicalId = targetData.getTarget().canonicalId(); db.execSQL( "UPDATE targets SET canonical_id = ? WHERE target_id = ?", new Object[] {updatedCanonicalId, targetId}); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java index 12105419fd6..8b54660c9fc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java @@ -22,7 +22,7 @@ import androidx.annotation.Nullable; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedSet; -import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.util.Consumer; @@ -96,7 +96,7 @@ public void setLastRemoteSnapshotVersion(SnapshotVersion snapshotVersion) { private void saveTargetData(TargetData targetData) { int targetId = targetData.getTargetId(); - String canonicalId = targetData.getTarget().getCanonicalId(); + String canonicalId = targetData.getTarget().canonicalId(); Timestamp version = targetData.getSnapshotVersion().getTimestamp(); com.google.firebase.firestore.proto.Target targetProto = @@ -207,11 +207,11 @@ int removeQueries(long upperBound, SparseArray activeTargetIds) { @Nullable @Override - public TargetData getTargetData(Target target) { + public TargetData getTargetData(TargetOrPipeline target) { // Querying the targets table by canonical_id may yield more than one result because // canonical_id values are not required to be unique per target. This query depends on the // query_targets index to be efficient. - String canonicalId = target.getCanonicalId(); + String canonicalId = target.canonicalId(); TargetDataHolder result = new TargetDataHolder(); db.query("SELECT target_proto FROM targets WHERE canonical_id = ?") .binding(canonicalId) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java index 0b39babfb5f..4b02bc4325d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java @@ -16,7 +16,7 @@ import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedSet; -import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.util.Consumer; @@ -100,7 +100,7 @@ interface TargetCache { * @return The cached TargetData entry, or null if the cache has no entry for the query. */ @Nullable - TargetData getTargetData(Target target); + TargetData getTargetData(TargetOrPipeline target); /** Adds the given document keys to cached query results of the given target ID. */ void addMatchingKeys(ImmutableSortedSet keys, int targetId); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetData.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetData.java index 6d0dda95678..d43a5f6ab1f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetData.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetData.java @@ -17,7 +17,7 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; import androidx.annotation.Nullable; -import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.remote.WatchStream; import com.google.protobuf.ByteString; @@ -25,7 +25,7 @@ /** An immutable set of metadata that the store will need to keep track of for each target. */ public final class TargetData { - private final Target target; + private final TargetOrPipeline target; private final int targetId; private final long sequenceNumber; private final QueryPurpose purpose; @@ -52,8 +52,8 @@ public final class TargetData { * read time. Documents are counted only when making a listen request with resume token or * read time, otherwise, keep it null. */ - TargetData( - Target target, + public TargetData( + TargetOrPipeline target, int targetId, long sequenceNumber, QueryPurpose purpose, @@ -72,7 +72,8 @@ public final class TargetData { } /** Convenience constructor for use when creating a TargetData for the first time. */ - public TargetData(Target target, int targetId, long sequenceNumber, QueryPurpose purpose) { + public TargetData( + TargetOrPipeline target, int targetId, long sequenceNumber, QueryPurpose purpose) { this( target, targetId, @@ -136,7 +137,7 @@ public TargetData withLastLimboFreeSnapshotVersion(SnapshotVersion lastLimboFree expectedCount); } - public Target getTarget() { + public TargetOrPipeline getTarget() { return target; } @@ -181,15 +182,15 @@ public boolean equals(Object o) { return false; } - TargetData targetData = (TargetData) o; - return target.equals(targetData.target) - && targetId == targetData.targetId - && sequenceNumber == targetData.sequenceNumber - && purpose.equals(targetData.purpose) - && snapshotVersion.equals(targetData.snapshotVersion) - && lastLimboFreeSnapshotVersion.equals(targetData.lastLimboFreeSnapshotVersion) - && resumeToken.equals(targetData.resumeToken) - && Objects.equals(expectedCount, targetData.expectedCount); + TargetData that = (TargetData) o; + return target.equals(that.target) + && targetId == that.targetId + && sequenceNumber == that.sequenceNumber + && purpose.equals(that.purpose) + && snapshotVersion.equals(that.snapshotVersion) + && lastLimboFreeSnapshotVersion.equals(that.lastLimboFreeSnapshotVersion) + && resumeToken.equals(that.resumeToken) + && Objects.equals(expectedCount, that.expectedCount); } @Override diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt index 1089a847628..256e882407a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt @@ -34,7 +34,7 @@ import java.util.Date import java.util.TreeMap import kotlin.math.min -internal object Values { +object Values { const val TYPE_KEY: String = "__type__" @JvmField val NAN_VALUE: Value = Value.newBuilder().setDoubleValue(Double.NaN).build() @JvmField val NULL_VALUE: Value = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build() @@ -671,6 +671,10 @@ internal object Values { @JvmStatic fun encodeValue(value: String): Value = Value.newBuilder().setStringValue(value).build() + @JvmStatic + fun encodeValue(value: ResourcePath): Value = + Value.newBuilder().setReferenceValue(value.canonicalString()).build() + @JvmStatic fun encodeValue(date: Date): Value = encodeValue(com.google.firebase.Timestamp((date))) @JvmStatic diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/FunctionRegistry.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/FunctionRegistry.kt new file mode 100644 index 00000000000..510be2a4a3a --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/FunctionRegistry.kt @@ -0,0 +1,110 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.pipeline + +/** + * A registry of all built-in pipeline functions. + * + * This is used internally to look up the evaluation logic for a given function name when + * deserializing a pipeline from a protobuf. + */ +internal object FunctionRegistry { + val functions: Map = + mapOf( + "and" to evaluateAnd, + "or" to evaluateOr, + "xor" to evaluateXor, + "not" to evaluateNot, + "round" to evaluateRound, + "ceil" to evaluateCeil, + "floor" to evaluateFloor, + "pow" to evaluatePow, + "sqrt" to evaluateSqrt, + "add" to evaluateAdd, + "subtract" to evaluateSubtract, + "multiply" to evaluateMultiply, + "divide" to evaluateDivide, + "mod" to evaluateMod, + "eq_any" to evaluateEqAny, + "not_eq_any" to evaluateNotEqAny, + "is_nan" to evaluateIsNaN, + "is_not_nan" to evaluateIsNotNaN, + "is_null" to evaluateIsNull, + "is_not_null" to evaluateIsNotNull, + "replace_first" to evaluateReplaceFirst, + "replace_all" to evaluateReplaceAll, + "char_length" to evaluateCharLength, + "byte_length" to evaluateByteLength, + "like" to evaluateLike, + "regex_contains" to evaluateRegexContains, + "regex_match" to evaluateRegexMatch, + "logical_max" to evaluateLogicalMaximum, + "logical_min" to evaluateLogicalMinimum, + "reverse" to evaluateReverse, + "str_contains" to evaluateStrContains, + "starts_with" to evaluateStartsWith, + "ends_with" to evaluateEndsWith, + "to_lowercase" to evaluateToLowercase, + "to_uppercase" to evaluateToUppercase, + "trim" to evaluateTrim, + "str_concat" to evaluateStrConcat, + "map" to evaluateMap, + "map_get" to evaluateMapGet, + + // Functions that are in evaluation.kt but not yet in expressions.kt + "is_error" to evaluateIsError, + "exists" to evaluateExists, + "cond" to evaluateCond, + "eq" to evaluateEq, + "neq" to evaluateNeq, + "gt" to evaluateGt, + "gte" to evaluateGte, + "lt" to evaluateLt, + "lte" to evaluateLte, + "array" to evaluateArray, + "array_contains" to evaluateArrayContains, + "array_contains_any" to evaluateArrayContainsAny, + "array_contains_all" to evaluateArrayContainsAll, + "array_length" to evaluateArrayLength, + "timestamp_add" to evaluateTimestampAdd, + "timestamp_sub" to evaluateTimestampSub, + "timestamp_to_unix_micros" to evaluateTimestampToUnixMicros, + "timestamp_to_unix_millis" to evaluateTimestampToUnixMillis, + "timestamp_to_unix_seconds" to evaluateTimestampToUnixSeconds, + "unix_micros_to_timestamp" to evaluateUnixMicrosToTimestamp, + "unix_millis_to_timestamp" to evaluateUnixMillisToTimestamp, + "unix_seconds_to_timestamp" to evaluateUnixSecondsToTimestamp, + + // Functions that are not yet implemented + "bit_and" to notImplemented, + "bit_or" to notImplemented, + "bit_xor" to notImplemented, + "bit_not" to notImplemented, + "bit_left_shift" to notImplemented, + "bit_right_shift" to notImplemented, + "is_absent" to notImplemented, + "rand" to notImplemented, + "map_merge" to notImplemented, + "map_remove" to notImplemented, + "cosine_distance" to notImplemented, + "dot_product" to notImplemented, + "timestamp_trunc" to notImplemented, + "split" to evaluateSplit, + "substring" to evaluateSubstring, + "ltrim" to evaluateLTrim, + "rtrim" to evaluateRTrim, + "str_join" to evaluateStrJoin + ) +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt index 756c25c4309..c536d949171 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt @@ -4188,8 +4188,7 @@ class Field internal constructor(internal val fieldPath: ModelFieldPath) : Selec block@{ input: MutableDocument -> EvaluateResultValue( when (fieldPath) { - KEY_PATH -> - encodeValue(DocumentReference.forPath(input.key.path, context.pipeline.firestore)) + KEY_PATH -> Value.newBuilder().setReferenceValue(input.key.path.canonicalString()).build() CREATE_TIME_PATH -> encodeValue(input.createTime.timestamp) UPDATE_TIME_PATH -> encodeValue(input.version.timestamp) else -> input.getField(fieldPath) ?: return@block EvaluateResultUnset @@ -4225,6 +4224,11 @@ internal constructor( internal val params: Array, private val options: InternalOptions = InternalOptions.EMPTY ) : Expr() { + internal constructor( + name: String, + params: List, + options: InternalOptions = InternalOptions.EMPTY + ) : this(name, FunctionRegistry.functions[name] ?: notImplemented, params.toTypedArray(), options) internal constructor( name: String, function: EvaluateFunction @@ -4401,7 +4405,7 @@ internal constructor(name: String, function: EvaluateFunction, params: Array>(internal val name: String, internal val options: InternalOptions) { internal fun toProtoStage(userDataReader: UserDataReader): Pipeline.Stage { @@ -197,51 +195,36 @@ internal constructor(options: InternalOptions = InternalOptions.EMPTY) : class CollectionSource internal constructor( - val path: String, + internal val path: ResourcePath, // We validate [firestore.databaseId] when adding to pipeline. - internal val firestore: FirebaseFirestore?, + internal val serializer: RemoteSerializer, options: InternalOptions ) : Stage("collection", options), Canonicalizable { override fun canonicalId(): String { - return "${name}(${path})" + return "${name}(${path.canonicalString()})" } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CollectionSource) return false if (path != other.path) return false - if (firestore != other.firestore) return false + if (serializer.databaseId() != other.serializer.databaseId()) return false if (options != other.options) return false return true } override fun hashCode(): Int { var result = path.hashCode() - result = 31 * result + (firestore?.hashCode() ?: 0) + result = 31 * result + (serializer.databaseId().hashCode() ?: 0) result = 31 * result + options.hashCode() return result } override fun self(options: InternalOptions): CollectionSource = - CollectionSource(path, firestore, options) + CollectionSource(path, serializer, options) override fun args(userDataReader: UserDataReader): Sequence = - sequenceOf( - Value.newBuilder().setReferenceValue(if (path.startsWith("/")) path else "/" + path).build() - ) + sequenceOf(Value.newBuilder().setReferenceValue(path.canonicalString()).build()) companion object { - /** - * Set the pipeline's source to the collection specified by the given path. - * - * @param path A path to a collection that will be the source of this pipeline. - * @return Pipeline with documents from target collection. - */ - @JvmStatic - fun of(path: String): CollectionSource { - // Validate path by converting to ResourcePath - val resourcePath = ResourcePath.fromString(path) - return CollectionSource(resourcePath.canonicalString(), null, InternalOptions.EMPTY) - } - /** * Set the pipeline's source to the collection specified by the given CollectionReference. * @@ -249,8 +232,12 @@ internal constructor( * @return Pipeline with documents from target collection. */ @JvmStatic - fun of(ref: CollectionReference): CollectionSource { - return CollectionSource(ref.path, ref.firestore, InternalOptions.EMPTY) + internal fun of(ref: CollectionReference, databaseId: DatabaseId): CollectionSource { + return CollectionSource( + ResourcePath.fromString(ref.path), + RemoteSerializer(databaseId), + InternalOptions.EMPTY + ) } } @@ -260,14 +247,11 @@ internal constructor( context: EvaluationContext, inputs: List ): List { - return inputs.filter { input -> - input.isFoundDocument && input.key.collectionPath.canonicalString() == path - } + return inputs.filter { input -> input.isFoundDocument && input.key.collectionPath == path } } } -class CollectionGroupSource -private constructor(val collectionId: String, options: InternalOptions) : +class CollectionGroupSource(val collectionId: String, options: InternalOptions) : Stage("collection_group", options), Canonicalizable { override fun canonicalId(): String { return "${name}(${collectionId})" @@ -322,9 +306,22 @@ private constructor(val collectionId: String, options: InternalOptions) : internal class DocumentsSource @JvmOverloads internal constructor( - val documents: Array, + val documents: Array, options: InternalOptions = InternalOptions.EMPTY ) : Stage("documents", options), Canonicalizable { + private val docKeySet: HashSet by lazy { + documents.map { it.canonicalString() }.toHashSet() + } + + override fun evaluate( + context: EvaluationContext, + inputs: List + ): List { + return inputs.filter { input -> + input.isFoundDocument && docKeySet.contains(input.key.path.canonicalString()) + } + } + override fun canonicalId(): String { val sortedDocuments = documents.sorted() return "${name}(${sortedDocuments.joinToString(",")})" @@ -344,10 +341,10 @@ internal constructor( return result } - internal constructor(document: String) : this(arrayOf(document)) + internal constructor(document: String) : this(arrayOf(ResourcePath.fromString(document))) override fun self(options: InternalOptions) = DocumentsSource(documents, options) override fun args(userDataReader: UserDataReader): Sequence = - documents.asSequence().map { if (it.startsWith("/")) it else "/" + it }.map(::encodeValue) + documents.asSequence().map(::encodeValue) } internal class AddFieldsStage @@ -482,7 +479,7 @@ internal constructor( internal class WhereStage internal constructor( - internal val condition: BooleanExpr, + internal val condition: Expr, options: InternalOptions = InternalOptions.EMPTY ) : Stage("where", options), Canonicalizable { override fun canonicalId(): String { @@ -810,8 +807,6 @@ internal constructor( context: EvaluationContext, inputs: List ): List { - val evaluates: Array = - orders.map { it.expr.evaluateFunction(context) }.toTypedArray() return inputs.sortedWith(comparator(context)) } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index 2e813ac3b98..1329663791b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -21,12 +21,16 @@ import androidx.annotation.VisibleForTesting; import com.google.firebase.Timestamp; import com.google.firebase.firestore.AggregateField; +import com.google.firebase.firestore.RealtimePipeline; +import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.core.Bound; import com.google.firebase.firestore.core.FieldFilter; import com.google.firebase.firestore.core.Filter; import com.google.firebase.firestore.core.OrderBy; import com.google.firebase.firestore.core.OrderBy.Direction; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.local.QueryPurpose; import com.google.firebase.firestore.local.TargetData; import com.google.firebase.firestore.model.DatabaseId; @@ -50,6 +54,18 @@ import com.google.firebase.firestore.model.mutation.SetMutation; import com.google.firebase.firestore.model.mutation.TransformOperation; import com.google.firebase.firestore.model.mutation.VerifyMutation; +import com.google.firebase.firestore.pipeline.CollectionGroupSource; +import com.google.firebase.firestore.pipeline.CollectionSource; +import com.google.firebase.firestore.pipeline.DocumentsSource; +import com.google.firebase.firestore.pipeline.Expr; +import com.google.firebase.firestore.pipeline.Field; +import com.google.firebase.firestore.pipeline.FunctionExpr; +import com.google.firebase.firestore.pipeline.InternalOptions; +import com.google.firebase.firestore.pipeline.LimitStage; +import com.google.firebase.firestore.pipeline.Ordering; +import com.google.firebase.firestore.pipeline.SortStage; +import com.google.firebase.firestore.pipeline.Stage; +import com.google.firebase.firestore.pipeline.WhereStage; import com.google.firebase.firestore.remote.WatchChange.ExistenceFilterWatchChange; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChangeType; @@ -72,8 +88,8 @@ import com.google.firestore.v1.StructuredQuery.FieldReference; import com.google.firestore.v1.StructuredQuery.Order; import com.google.firestore.v1.StructuredQuery.UnaryFilter; -import com.google.firestore.v1.Target; import com.google.firestore.v1.Target.DocumentsTarget; +import com.google.firestore.v1.Target.PipelineQueryTarget; import com.google.firestore.v1.Target.QueryTarget; import com.google.firestore.v1.Value; import com.google.protobuf.Int32Value; @@ -219,6 +235,10 @@ public String databaseName() { return databaseName; } + public DatabaseId databaseId() { + return databaseId; + } + // Documents public com.google.firestore.v1.Document encodeDocument(DocumentKey key, ObjectValue value) { @@ -483,14 +503,19 @@ private String encodeLabel(QueryPurpose purpose) { } } - public Target encodeTarget(TargetData targetData) { - Target.Builder builder = Target.newBuilder(); - com.google.firebase.firestore.core.Target target = targetData.getTarget(); - - if (target.isDocumentQuery()) { - builder.setDocuments(encodeDocumentsTarget(target)); + public com.google.firestore.v1.Target encodeTarget(TargetData targetData) { + com.google.firestore.v1.Target.Builder builder = com.google.firestore.v1.Target.newBuilder(); + TargetOrPipeline target = targetData.getTarget(); + + if (target.isPipeline()) { + PipelineQueryTarget.Builder pipelineBuilder = PipelineQueryTarget.newBuilder(); + builder.setPipelineQuery( + pipelineBuilder.setStructuredPipeline( + target.pipeline().toStructurePipelineProto$com_google_firebase_firebase_firestore())); + } else if (target.target().isDocumentQuery()) { + builder.setDocuments(encodeDocumentsTarget(target.target())); } else { - builder.setQuery(encodeQueryTarget(target)); + builder.setQuery(encodeQueryTarget(target.target())); } builder.setTargetId(targetData.getTargetId()); @@ -513,13 +538,13 @@ public Target encodeTarget(TargetData targetData) { return builder.build(); } - public DocumentsTarget encodeDocumentsTarget(com.google.firebase.firestore.core.Target target) { + public DocumentsTarget encodeDocumentsTarget(Target target) { DocumentsTarget.Builder builder = DocumentsTarget.newBuilder(); builder.addDocuments(encodeQueryPath(target.getPath())); return builder.build(); } - public com.google.firebase.firestore.core.Target decodeDocumentsTarget(DocumentsTarget target) { + public Target decodeDocumentsTarget(DocumentsTarget target) { int count = target.getDocumentsCount(); hardAssert(count == 1, "DocumentsTarget contained other than 1 document %d", count); @@ -527,7 +552,7 @@ public com.google.firebase.firestore.core.Target decodeDocumentsTarget(Documents return Query.atPath(decodeQueryPath(name)).toTarget(); } - public QueryTarget encodeQueryTarget(com.google.firebase.firestore.core.Target target) { + public QueryTarget encodeQueryTarget(Target target) { // Dissect the path into parent, collectionId, and optional key filter. QueryTarget.Builder builder = QueryTarget.newBuilder(); StructuredQuery.Builder structuredQueryBuilder = StructuredQuery.newBuilder(); @@ -582,8 +607,7 @@ public QueryTarget encodeQueryTarget(com.google.firebase.firestore.core.Target t return builder.build(); } - public com.google.firebase.firestore.core.Target decodeQueryTarget( - String parent, StructuredQuery query) { + public Target decodeQueryTarget(String parent, StructuredQuery query) { ResourcePath path = decodeQueryPath(parent); String collectionGroup = null; @@ -618,7 +642,7 @@ public com.google.firebase.firestore.core.Target decodeQueryTarget( orderBy = Collections.emptyList(); } - long limit = com.google.firebase.firestore.core.Target.NO_LIMIT; + long limit = Target.NO_LIMIT; if (query.hasLimit()) { limit = query.getLimit().getValue(); } @@ -633,14 +657,133 @@ public com.google.firebase.firestore.core.Target decodeQueryTarget( endAt = new Bound(query.getEndAt().getValuesList(), !query.getEndAt().getBefore()); } - return new com.google.firebase.firestore.core.Target( - path, collectionGroup, filterBy, orderBy, limit, startAt, endAt); + return new Target(path, collectionGroup, filterBy, orderBy, limit, startAt, endAt); } - public com.google.firebase.firestore.core.Target decodeQueryTarget(QueryTarget target) { + public Target decodeQueryTarget(QueryTarget target) { return decodeQueryTarget(target.getParent(), target.getStructuredQuery()); } + public RealtimePipeline decodePipelineQueryTarget(PipelineQueryTarget proto) { + hardAssert( + proto.getPipelineTypeCase() == PipelineQueryTarget.PipelineTypeCase.STRUCTURED_PIPELINE, + "Unknown pipeline_type in PipelineQueryTarget: " + proto.getPipelineTypeCase()); + + com.google.firestore.v1.Pipeline pipelineProto = proto.getStructuredPipeline().getPipeline(); + List> decodedStages = new ArrayList<>(); + for (com.google.firestore.v1.Pipeline.Stage stageProto : pipelineProto.getStagesList()) { + decodedStages.add(decodeStage(stageProto)); + } + + return new RealtimePipeline(this, new UserDataReader(this.databaseId()), decodedStages); + } + + private Stage decodeStage(com.google.firestore.v1.Pipeline.Stage protoStage) { + String stageName = protoStage.getName(); + List args = protoStage.getArgsList(); + + switch (stageName) { + case "collection": + hardAssert( + args.size() >= 1 + && args.get(0).getValueTypeCase() == Value.ValueTypeCase.REFERENCE_VALUE, + "Invalid 'collection' stage: missing or invalid arguments"); + return new CollectionSource( + ResourcePath.fromString(args.get(0).getReferenceValue()), this, InternalOptions.EMPTY); + case "collection_group": + hardAssert( + args.size() >= 1 && args.get(0).getValueTypeCase() == Value.ValueTypeCase.STRING_VALUE, + "Invalid 'collection_group' stage: missing or invalid arguments"); + return new CollectionGroupSource(args.get(0).getStringValue(), InternalOptions.EMPTY); + case "documents": + List documentPaths = new ArrayList<>(); + for (Value arg : args) { + hardAssert( + arg.getValueTypeCase() == Value.ValueTypeCase.REFERENCE_VALUE, + "Invalid argument type for 'documents' stage: expected reference_value"); + documentPaths.add(ResourcePath.fromString(arg.getReferenceValue())); + } + return new DocumentsSource( + documentPaths.toArray(new ResourcePath[0]), InternalOptions.EMPTY); + case "where": + hardAssert(args.size() >= 1, "Invalid 'where' stage: missing or invalid arguments"); + return new WhereStage(decodeExpression(args.get(0)), InternalOptions.EMPTY); + case "limit": + hardAssert( + args.size() >= 1 && args.get(0).getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE, + "Invalid 'limit' stage: missing or invalid arguments"); + return new LimitStage((int) args.get(0).getIntegerValue(), InternalOptions.EMPTY); + case "sort": + hardAssert(args.size() > 0, "Invalid 'sort' stage: missing arguments"); + List orderings = new ArrayList<>(); + for (Value arg : args) { + orderings.add(decodeOrdering(arg)); + } + return new SortStage(orderings.toArray(new Ordering[0]), InternalOptions.EMPTY); + default: + throw new IllegalArgumentException("Unsupported stage type: " + stageName); + } + } + + private Expr decodeExpression(Value protoValue) { + switch (protoValue.getValueTypeCase()) { + case FIELD_REFERENCE_VALUE: + return new Field(FieldPath.fromServerFormat(protoValue.getFieldReferenceValue())); + case FUNCTION_VALUE: + return decodeFunctionExpression(protoValue.getFunctionValue()); + default: + return new Expr.Constant(protoValue); + } + } + + private FunctionExpr decodeFunctionExpression(com.google.firestore.v1.Function protoFunction) { + String funcName = protoFunction.getName(); + List decodedArgs = new ArrayList<>(); + for (Value arg : protoFunction.getArgsList()) { + decodedArgs.add(decodeExpression(arg)); + } + return new FunctionExpr(funcName, decodedArgs, InternalOptions.EMPTY); + } + + private Ordering decodeOrdering(Value protoValue) { + hardAssert( + protoValue.getValueTypeCase() == Value.ValueTypeCase.MAP_VALUE, + "Invalid proto_value type for Ordering, expected map_value."); + + Expr decodedExpr = null; + Ordering.Direction decodedDirection = null; + + for (Map.Entry entry : protoValue.getMapValue().getFieldsMap().entrySet()) { + String key = entry.getKey(); + Value value = entry.getValue(); + if (key.equals("expression")) { + hardAssert(decodedExpr == null, "Duplicate 'expression' field in Ordering proto."); + decodedExpr = decodeExpression(value); + } else if (key.equals("direction")) { + hardAssert(decodedDirection == null, "Duplicate 'direction' field in Ordering proto."); + hardAssert( + value.getValueTypeCase() == Value.ValueTypeCase.STRING_VALUE, + "Invalid type for 'direction' field in Ordering proto, expected string_value."); + String directionStr = value.getStringValue(); + if (directionStr.equals("ascending")) { + decodedDirection = Ordering.Direction.ASCENDING; + } else if (directionStr.equals("descending")) { + decodedDirection = Ordering.Direction.DESCENDING; + } else { + throw new IllegalArgumentException( + "Invalid string value '" + + directionStr + + "' for 'direction' field in Ordering proto."); + } + } + } + + hardAssert(decodedExpr != null, "Missing 'expression' field in Ordering proto."); + hardAssert(decodedDirection != null, "Missing 'direction' field in Ordering proto."); + + return new Ordering(decodedExpr, decodedDirection); + } + StructuredAggregationQuery encodeStructuredAggregationQuery( QueryTarget encodedQueryTarget, List aggregateFields, diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java index 02953dcef7d..ead10fd0eeb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java @@ -20,12 +20,12 @@ import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.DocumentViewChange; -import com.google.firebase.firestore.core.Target; import com.google.firebase.firestore.local.QueryPurpose; import com.google.firebase.firestore.local.TargetData; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.MutableDocument; +import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.remote.WatchChange.DocumentChange; import com.google.firebase.firestore.remote.WatchChange.ExistenceFilterWatchChange; @@ -198,14 +198,14 @@ public void handleExistenceFilter(ExistenceFilterWatchChange watchChange) { TargetData targetData = queryDataForActiveTarget(targetId); if (targetData != null) { - Target target = targetData.getTarget(); - if (target.isDocumentQuery()) { + ResourcePath singleDocPath = targetData.getTarget().getSingleDocPath(); + if (singleDocPath != null) { if (expectedCount == 0) { // The existence filter told us the document does not exist. We deduce that this document // does not exist and apply a deleted document to our updates. Without applying this // deleted document there might be another query that will raise this document as part of // a snapshot until it is resolved, essentially exposing inconsistency between queries. - DocumentKey key = DocumentKey.fromPath(target.getPath()); + DocumentKey key = DocumentKey.fromPath(singleDocPath); MutableDocument result = MutableDocument.newNoDocument(key, SnapshotVersion.NONE); removeDocumentFromTarget(targetId, key, result); } else { @@ -329,12 +329,13 @@ public RemoteEvent createRemoteEvent(SnapshotVersion snapshotVersion) { TargetData targetData = queryDataForActiveTarget(targetId); if (targetData != null) { - if (targetState.isCurrent() && targetData.getTarget().isDocumentQuery()) { + ResourcePath singleDocPath = targetData.getTarget().getSingleDocPath(); + if (targetState.isCurrent() && singleDocPath != null) { // Document queries for document that don't exist can produce an empty result set. To // update our local cache, we synthesize a document delete if we have not previously // received the document. This resolves the limbo state of the document, removing it from // limboDocumentRefs. - DocumentKey key = DocumentKey.fromPath(targetData.getTarget().getPath()); + DocumentKey key = DocumentKey.fromPath(singleDocPath); if (pendingDocumentUpdates.get(key) == null && !targetContainsDocument(targetId, key)) { MutableDocument result = MutableDocument.newNoDocument(key, snapshotVersion); removeDocumentFromTarget(targetId, key, result); diff --git a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java index 47672e7aec4..b6d12e56794 100644 --- a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java +++ b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java @@ -17,8 +17,6 @@ import static com.google.firebase.firestore.testutil.TestUtil.doc; import static com.google.firebase.firestore.testutil.TestUtil.docSet; import static com.google.firebase.firestore.testutil.TestUtil.key; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.google.android.gms.tasks.Task; import com.google.firebase.database.collection.ImmutableSortedSet; @@ -40,14 +38,11 @@ public class TestUtil { - public static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); - private static final DatabaseId DATABASE_ID = DatabaseId.forProject("project"); + public static final String PROJECT_ID = "projectId"; + public static final DatabaseId DATABASE_ID = DatabaseId.forProject(PROJECT_ID); public static final UserDataReader USER_DATA_READER = new UserDataReader(DATABASE_ID); - - static { - when(FIRESTORE.getDatabaseId()).thenReturn(DATABASE_ID); - when(FIRESTORE.getUserDataReader()).thenReturn(USER_DATA_READER); - } + private static final FirebaseFirestore FIRESTORE = + new FirebaseFirestoreIntegrationTestFactory(DATABASE_ID).firestore; public static FirebaseFirestore firestore() { return FIRESTORE; @@ -119,7 +114,8 @@ public static QuerySnapshot querySnapshot( } ViewSnapshot viewSnapshot = new ViewSnapshot( - com.google.firebase.firestore.testutil.TestUtil.query(path), + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper( + com.google.firebase.firestore.testutil.TestUtil.query(path)), newDocuments, oldDocuments, documentChanges, diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/DocumentChangeTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/DocumentChangeTest.java index 0285e452460..d8d5eff691f 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/DocumentChangeTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/DocumentChangeTest.java @@ -73,7 +73,10 @@ private static void validatePositions( updates = updates.insert(doc.getKey(), doc); } - View view = new View(query, DocumentKey.emptyKeySet()); + View view = + new View( + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper(query), + DocumentKey.emptyKeySet()); View.DocumentChanges initialChanges = view.computeDocChanges(initialDocs); TargetChange initialTargetChange = ackTarget(initialDocsList.toArray(new MutableDocument[] {})); ViewSnapshot initialSnapshot = diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java index 3ee810cf51a..5313852527a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.when; import com.google.firebase.Timestamp; import com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior; @@ -87,10 +86,6 @@ public void testEquals() { @Test public void testToObjects() { - // Prevent NPE on trying to access non-existent settings on the mock. - when(TestUtil.firestore().getFirestoreSettings()) - .thenReturn(new FirebaseFirestoreSettings.Builder().build()); - ObjectValue objectData = ObjectValue.fromMap(map("timestamp", ServerTimestamps.valueOf(Timestamp.now(), null))); QuerySnapshot foo = @@ -125,7 +120,7 @@ public void testIncludeMetadataChanges() { com.google.firebase.firestore.core.Query fooQuery = query("foo"); ViewSnapshot viewSnapshot = new ViewSnapshot( - fooQuery, + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper(fooQuery), newDocuments, oldDocuments, documentChanges, diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/TestUtilKtx.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/TestUtilKtx.java index 15aa070f2d5..2c6b9a7d10e 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/TestUtilKtx.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/TestUtilKtx.java @@ -98,7 +98,8 @@ public static QuerySnapshot querySnapshot( } ViewSnapshot viewSnapshot = new ViewSnapshot( - com.google.firebase.firestore.testutil.TestUtil.query(path), + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper( + com.google.firebase.firestore.testutil.TestUtil.query(path)), newDocuments, oldDocuments, documentChanges, diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java index 5531033a272..5714fba78ad 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java @@ -42,12 +42,14 @@ @Config(manifest = Config.NONE) public class EventManagerTest { private static QueryListener queryListener(Query query) { - return new QueryListener(query, new ListenOptions(), (value, error) -> {}); + return new QueryListener( + new QueryOrPipeline.QueryWrapper(query), new ListenOptions(), (value, error) -> {}); } @Test public void testMultipleListensPerQuery() { Query query = Query.atPath(path("foo/bar")); + QueryOrPipeline qop = new QueryOrPipeline.QueryWrapper(query); QueryListener listener1 = queryListener(query); QueryListener listener2 = queryListener(query); @@ -62,12 +64,12 @@ public void testMultipleListensPerQuery() { manager.removeQueryListener(listener2); verify(syncSpy, times(1)) .listen( - query, + qop, /** shouldListenToRemote= */ true); verify(syncSpy, times(1)) .stopListening( - query, + qop, /** shouldUnlistenToRemote= */ true); } @@ -75,40 +77,43 @@ public void testMultipleListensPerQuery() { @Test public void testUnlistensOnUnknownListeners() { Query query = Query.atPath(path("foo/bar")); + QueryOrPipeline qop = new QueryOrPipeline.QueryWrapper(query); SyncEngine syncSpy = mock(SyncEngine.class); EventManager manager = new EventManager(syncSpy); manager.removeQueryListener(queryListener(query)); - verify(syncSpy, never()).stopListening(eq(query), anyBoolean()); + verify(syncSpy, never()).stopListening(eq(qop), anyBoolean()); } @Test public void testListenCalledInOrder() { Query query1 = Query.atPath(path("foo/bar")); + QueryOrPipeline qop1 = new QueryOrPipeline.QueryWrapper(query1); Query query2 = Query.atPath(path("bar/baz")); + QueryOrPipeline qop2 = new QueryOrPipeline.QueryWrapper(query2); SyncEngine syncSpy = mock(SyncEngine.class); EventManager eventManager = new EventManager(syncSpy); QueryListener spy1 = mock(QueryListener.class); - when(spy1.getQuery()).thenReturn(query1); + when(spy1.getQuery()).thenReturn(qop1); QueryListener spy2 = mock(QueryListener.class); - when(spy2.getQuery()).thenReturn(query2); + when(spy2.getQuery()).thenReturn(qop2); QueryListener spy3 = mock(QueryListener.class); - when(spy3.getQuery()).thenReturn(query1); + when(spy3.getQuery()).thenReturn(qop1); eventManager.addQueryListener(spy1); eventManager.addQueryListener(spy2); eventManager.addQueryListener(spy3); - verify(syncSpy, times(1)).listen(eq(query1), anyBoolean()); - verify(syncSpy, times(1)).listen(eq(query2), anyBoolean()); + verify(syncSpy, times(1)).listen(eq(qop1), anyBoolean()); + verify(syncSpy, times(1)).listen(eq(qop2), anyBoolean()); ViewSnapshot snap1 = mock(ViewSnapshot.class); - when(snap1.getQuery()).thenReturn(query1); + when(snap1.getQuery()).thenReturn(qop1); ViewSnapshot snap2 = mock(ViewSnapshot.class); - when(snap2.getQuery()).thenReturn(query2); + when(snap2.getQuery()).thenReturn(qop2); eventManager.onViewSnapshots(Arrays.asList(snap1, snap2)); InOrder inOrder = inOrder(spy1, spy3, spy2); @@ -120,6 +125,7 @@ public void testListenCalledInOrder() { @Test public void testWillForwardOnOnlineStateChangedCalls() { Query query1 = Query.atPath(path("foo/bar")); + QueryOrPipeline qop1 = new QueryOrPipeline.QueryWrapper(query1); SyncEngine syncSpy = mock(SyncEngine.class); EventManager eventManager = new EventManager(syncSpy); @@ -127,7 +133,7 @@ public void testWillForwardOnOnlineStateChangedCalls() { List events = new ArrayList<>(); QueryListener spy = mock(QueryListener.class); - when(spy.getQuery()).thenReturn(query1); + when(spy.getQuery()).thenReturn(qop1); doAnswer( invocation -> { events.add(invocation.getArguments()[0]); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java index 31c1e9b0712..4cbf86dd020 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java @@ -54,7 +54,7 @@ private static ViewSnapshot applyChanges(View view, MutableDocument... docs) { private static QueryListener queryListener( Query query, ListenOptions options, List accumulator) { return new QueryListener( - query, + new QueryOrPipeline.QueryWrapper(query), options, (value, error) -> { assertNull(error); @@ -82,7 +82,7 @@ public void testRaisesCollectionEvents() { QueryListener listener = queryListener(query, accum); QueryListener otherListener = queryListener(query, otherAccum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1, doc2); ViewSnapshot snap2 = applyChanges(view, doc2prime); @@ -103,7 +103,7 @@ public void testRaisesCollectionEvents() { new ViewSnapshot( snap2.getQuery(), snap2.getDocuments(), - DocumentSet.emptySet(snap2.getQuery().comparator()), + DocumentSet.emptySet(query.comparator()), asList(change1, change4), snap2.isFromCache(), snap2.getMutatedKeys(), @@ -120,7 +120,7 @@ public void testRaisesErrorEvent() { AtomicBoolean hadEvent = new AtomicBoolean(false); QueryListener listener = new QueryListener( - query, + new QueryOrPipeline.QueryWrapper(query), new ListenOptions(), (value, error) -> { assertNull(value); @@ -140,7 +140,7 @@ public void testRaisesEventForEmptyCollectionsAfterSync() { QueryListener listener = queryListener(query, accum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view); TargetChange ackTarget = ackTarget(); ViewSnapshot snap2 = @@ -167,7 +167,7 @@ public void testDoesNotRaiseEventsForMetadataChangesUnlessSpecified() { QueryListener filteredListener = queryListener(query, options1, filteredAccum); QueryListener fullListener = queryListener(query, options2, fullAccum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1); TargetChange ackTarget = ackTarget(doc1); @@ -207,7 +207,7 @@ public void testRaisesDocumentMetadataEventsOnlyWhenSpecified() { QueryListener filteredListener = queryListener(query, options1, filteredAccum); QueryListener fullListener = queryListener(query, options2, fullAccum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1, doc2); ViewSnapshot snap2 = applyChanges(view, doc1Prime); ViewSnapshot snap3 = applyChanges(view, doc3); @@ -243,7 +243,7 @@ public void testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChang options.includeQueryMetadataChanges = true; QueryListener fullListener = queryListener(query, options, fullAccum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1, doc2); ViewSnapshot snap2 = applyChanges(view, doc1Prime); ViewSnapshot snap3 = applyChanges(view, doc3); @@ -287,7 +287,7 @@ public void testMetadataOnlyDocumentChangesAreFilteredOut() { options.includeDocumentMetadataChanges = false; QueryListener filteredListener = queryListener(query, options, filteredAccum); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1, doc2); ViewSnapshot snap2 = applyChanges(view, doc1Prime, doc3); @@ -324,7 +324,7 @@ public void testWillWaitForSyncIfOnline() { options.waitForSyncWhenOnline = true; QueryListener listener = queryListener(query, options, events); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1); ViewSnapshot snap2 = applyChanges(view, doc2); DocumentChanges changes = view.computeDocChanges(docUpdates()); @@ -343,7 +343,7 @@ public void testWillWaitForSyncIfOnline() { new ViewSnapshot( snap3.getQuery(), snap3.getDocuments(), - DocumentSet.emptySet(snap3.getQuery().comparator()), + DocumentSet.emptySet(query.comparator()), asList(change1, change2), /* isFromCache= */ false, snap3.getMutatedKeys(), @@ -365,7 +365,7 @@ public void testWillRaiseInitialEventWhenGoingOffline() { options.waitForSyncWhenOnline = true; QueryListener listener = queryListener(query, options, events); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view, doc1); ViewSnapshot snap2 = applyChanges(view, doc2); @@ -382,7 +382,7 @@ public void testWillRaiseInitialEventWhenGoingOffline() { new ViewSnapshot( snap1.getQuery(), snap1.getDocuments(), - DocumentSet.emptySet(snap1.getQuery().comparator()), + DocumentSet.emptySet(query.comparator()), asList(change1), /* isFromCache= */ true, snap1.getMutatedKeys(), @@ -411,7 +411,7 @@ public void testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs() { QueryListener listener = queryListener(query, new ListenOptions(), events); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view); listener.onOnlineStateChanged(OnlineState.ONLINE); // no event @@ -422,7 +422,7 @@ public void testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs() { new ViewSnapshot( snap1.getQuery(), snap1.getDocuments(), - DocumentSet.emptySet(snap1.getQuery().comparator()), + DocumentSet.emptySet(query.comparator()), asList(), /* isFromCache= */ true, snap1.getMutatedKeys(), @@ -439,7 +439,7 @@ public void testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs() { QueryListener listener = queryListener(query, new ListenOptions(), events); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); ViewSnapshot snap1 = applyChanges(view); listener.onOnlineStateChanged(OnlineState.OFFLINE); @@ -449,7 +449,7 @@ public void testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs() { new ViewSnapshot( snap1.getQuery(), snap1.getDocuments(), - DocumentSet.emptySet(snap1.getQuery().comparator()), + DocumentSet.emptySet(query.comparator()), asList(), /* isFromCache= */ true, snap1.getMutatedKeys(), diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java index ee3920d3658..90f1ff820f9 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java @@ -53,7 +53,7 @@ public void testConstructor() { ViewSnapshot snapshot = new ViewSnapshot( - query, + new QueryOrPipeline.QueryWrapper(query), docs, oldDocs, changes, @@ -63,7 +63,7 @@ public void testConstructor() { excludesMetadataChanges, hasCachedResults); - assertEquals(query, snapshot.getQuery()); + assertEquals(query, snapshot.getQuery().query()); assertEquals(docs, snapshot.getDocuments()); assertEquals(oldDocs, snapshot.getOldDocuments()); assertEquals(changes, snapshot.getChanges()); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewTest.java index 6358966f499..a3d2bd51c11 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewTest.java @@ -61,7 +61,7 @@ private static ViewChange applyChanges(View view, MutableDocument... docs) { @Test public void testAddsDocumentsBasedOnQuery() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("text", "msg1")); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("text", "msg2")); @@ -71,7 +71,7 @@ public void testAddsDocumentsBasedOnQuery() { View.DocumentChanges docViewChanges = view.computeDocChanges(updates); TargetChange targetChange = ackTarget(doc1, doc2, doc3); ViewSnapshot snapshot = view.applyChanges(docViewChanges, targetChange).getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc2), snapshot.getDocuments().toList()); assertEquals( asList( @@ -86,7 +86,7 @@ public void testAddsDocumentsBasedOnQuery() { @Test public void testRemovesDocument() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("text", "msg1")); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("text", "msg2")); @@ -102,7 +102,7 @@ public void testRemovesDocument() { ackTarget(doc1, doc3)) .getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc3), snapshot.getDocuments().toList()); assertEquals( asList( @@ -116,7 +116,7 @@ public void testRemovesDocument() { @Test public void testReturnsNilIfNoChange() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("text", "msg1")); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("text", "msg2")); @@ -131,7 +131,7 @@ public void testReturnsNilIfNoChange() { @Test public void testReturnsNotNilForFirstChanges() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // initial state assertNotNull(applyChanges(view).getSnapshot()); @@ -140,7 +140,7 @@ public void testReturnsNotNilForFirstChanges() { @Test public void testFiltersDocumentsBasedOnQueryWithFilters() { Query query = messageQuery().filter(filter("sort", "<=", 2)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("sort", 1)); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("sort", 2)); @@ -150,7 +150,7 @@ public void testFiltersDocumentsBasedOnQueryWithFilters() { ViewSnapshot snapshot = applyChanges(view, doc1, doc2, doc3, doc4, doc5).getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc5, doc2), snapshot.getDocuments().toList()); assertEquals( asList( @@ -165,7 +165,7 @@ public void testFiltersDocumentsBasedOnQueryWithFilters() { @Test public void testUpdatesDocumentsBasedOnQueryWithFilters() { Query query = messageQuery().filter(filter("sort", "<=", 2)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("sort", 1)); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("sort", 3)); @@ -174,7 +174,7 @@ public void testUpdatesDocumentsBasedOnQueryWithFilters() { ViewSnapshot snapshot = applyChanges(view, doc1, doc2, doc3, doc4).getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc3), snapshot.getDocuments().toList()); MutableDocument newDoc2 = doc("rooms/eros/messages/2", 1, map("sort", 2)); @@ -183,7 +183,7 @@ public void testUpdatesDocumentsBasedOnQueryWithFilters() { snapshot = applyChanges(view, newDoc2, newDoc3, newDoc4).getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(newDoc4, doc1, newDoc2), snapshot.getDocuments().toList()); assertEquals( @@ -199,7 +199,7 @@ public void testUpdatesDocumentsBasedOnQueryWithFilters() { @Test public void testRemovesDocumentsForQueryWithLimit() { Query query = messageQuery().limitToFirst(2); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("text", "msg1")); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("text", "msg2")); @@ -211,7 +211,7 @@ public void testRemovesDocumentsForQueryWithLimit() { ViewSnapshot snapshot = view.applyChanges(view.computeDocChanges(docUpdates(doc2)), ackTarget(doc1, doc2, doc3)) .getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc2), snapshot.getDocuments().toList()); assertEquals( @@ -226,7 +226,7 @@ public void testRemovesDocumentsForQueryWithLimit() { @Test public void testDoesNotReportChangesForDocumentBeyondLimit() { Query query = messageQuery().orderBy(orderBy("num")).limitToFirst(2); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map("num", 1)); MutableDocument doc2 = doc("rooms/eros/messages/2", 0, map("num", 2)); @@ -247,7 +247,7 @@ public void testDoesNotReportChangesForDocumentBeyondLimit() { ViewSnapshot snapshot = view.applyChanges(viewDocChanges, ackTarget(doc1, doc2, doc3, doc4)).getSnapshot(); - assertEquals(query, snapshot.getQuery()); + assertEquals(new QueryOrPipeline.QueryWrapper(query), snapshot.getQuery()); assertEquals(asList(doc1, doc3), snapshot.getDocuments().toList()); assertEquals( @@ -262,7 +262,7 @@ public void testDoesNotReportChangesForDocumentBeyondLimit() { @Test public void testKeepsTrackOfLimboDocuments() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); MutableDocument doc3 = doc("rooms/eros/messages/2", 0, map()); @@ -304,7 +304,7 @@ public void testKeepsTrackOfLimboDocuments() { @Test public void testViewsWithLimboDocumentsAreMarkedFromCache() { Query query = messageQuery(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); @@ -335,7 +335,8 @@ public void testResumingQueryCreatesNoLimbos() { // Unlike other cases, here the view is initialized with a set of previously synced documents // which happens when listening to a previously listened-to query. - View view = new View(query, keySet(doc1.getKey(), doc2.getKey())); + View view = + new View(new QueryOrPipeline.QueryWrapper(query), keySet(doc1.getKey(), doc2.getKey())); TargetChange markCurrent = ackTarget(); View.DocumentChanges changes = view.computeDocChanges(docUpdates()); @@ -348,7 +349,7 @@ public void testReturnsNeedsRefillOnDeleteInLimitQuery() { Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -376,7 +377,7 @@ public void testReturnsNeedsRefillOnReorderInLimitQuery() { MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map("order", 1)); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map("order", 2)); MutableDocument doc3 = doc("rooms/eros/messages/2", 0, map("order", 3)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2, doc3)); @@ -407,7 +408,7 @@ public void testDoesNotNeedRefillOnReorderWithinLimit() { MutableDocument doc3 = doc("rooms/eros/messages/2", 0, map("order", 3)); MutableDocument doc4 = doc("rooms/eros/messages/3", 0, map("order", 4)); MutableDocument doc5 = doc("rooms/eros/messages/4", 0, map("order", 5)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2, doc3, doc4, doc5)); @@ -433,7 +434,7 @@ public void testDoesNotNeedRefillOnReorderAfterLimitQuery() { MutableDocument doc3 = doc("rooms/eros/messages/2", 0, map("order", 3)); MutableDocument doc4 = doc("rooms/eros/messages/3", 0, map("order", 4)); MutableDocument doc5 = doc("rooms/eros/messages/4", 0, map("order", 5)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2, doc3, doc4, doc5)); @@ -456,7 +457,7 @@ public void testDoesNotNeedRefillForAdditionAfterTheLimit() { Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -478,7 +479,7 @@ public void testDoesNotNeedRefillForDeletionsWhenNotNearTheLimit() { Query query = messageQuery().limitToFirst(20); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); assertEquals(2, changes.documentSet.size()); @@ -499,7 +500,7 @@ public void testHandlesApplyingIrrelevantDocs() { Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -521,7 +522,7 @@ public void testComputesMutatedDocumentKeys() { Query query = messageQuery(); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -539,7 +540,7 @@ public void testRemovesKeysFromMutatedDocumentKeysWhenNewDocDoesNotHaveChanges() Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()).setHasLocalMutations(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -558,7 +559,7 @@ public void testRemembersLocalMutationsFromPreviousSnapshot() { Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()).setHasLocalMutations(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -575,7 +576,7 @@ public void testRemembersLocalMutationsFromPreviousCallToComputeChanges() { Query query = messageQuery().limitToFirst(2); MutableDocument doc1 = doc("rooms/eros/messages/0", 0, map()); MutableDocument doc2 = doc("rooms/eros/messages/1", 0, map()).setHasLocalMutations(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); // Start with a full view. View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); @@ -590,7 +591,7 @@ public void testRemembersLocalMutationsFromPreviousCallToComputeChanges() { public void testRaisesHasPendingWritesForPendingMutationsInInitialSnapshot() { Query query = messageQuery(); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map()).setHasLocalMutations(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1)); ViewChange viewChange = view.applyChanges(changes); @@ -602,7 +603,7 @@ public void testRaisesHasPendingWritesForPendingMutationsInInitialSnapshot() { public void testDoesntRaiseHasPendingWritesForCommittedMutationsInInitialSnapshot() { Query query = messageQuery(); MutableDocument doc1 = doc("rooms/eros/messages/1", 0, map()).setHasCommittedMutations(); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1)); ViewChange viewChange = view.applyChanges(changes); @@ -626,7 +627,7 @@ public void testSuppressesWriteAcknowledgementIfWatchHasNotCaughtUp() { doc("rooms/eros/messages/2", 2, map("time", 3)).setHasLocalMutations(); MutableDocument doc2Acknowledged = doc("rooms/eros/messages/2", 2, map("time", 3)); - View view = new View(query, DocumentKey.emptyKeySet()); + View view = new View(new QueryOrPipeline.QueryWrapper(query), DocumentKey.emptyKeySet()); View.DocumentChanges changes = view.computeDocChanges(docUpdates(doc1, doc2)); ViewChange snap = view.applyChanges(changes); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java index d93231ad215..f688295c974 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java @@ -18,7 +18,7 @@ import androidx.annotation.Nullable; import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; -import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; @@ -81,7 +81,7 @@ public void initialize(LocalDocumentsView localDocuments, IndexManager indexMana @Override public ImmutableSortedMap getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, SnapshotVersion lastLimboFreeSnapshotVersion, ImmutableSortedSet remoteKeys) { return queryEngine.getDocumentsMatchingQuery(query, lastLimboFreeSnapshotVersion, remoteKeys); @@ -185,13 +185,13 @@ public Map getAll( @Override public Map getDocumentsMatchingQuery( - Query query, IndexOffset offset, @NonNull Set mutatedKeys) { + QueryOrPipeline query, IndexOffset offset, @NonNull Set mutatedKeys) { return getDocumentsMatchingQuery(query, offset, mutatedKeys, /*context*/ null); } @Override public Map getDocumentsMatchingQuery( - Query query, + QueryOrPipeline query, IndexOffset offset, @NonNull Set mutatedKeys, @Nullable QueryContext context) { diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalSerializerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalSerializerTest.java index dd5b97a4728..93803338331 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalSerializerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalSerializerTest.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.local; +import static com.google.firebase.firestore.TestUtil.DATABASE_ID; import static com.google.firebase.firestore.testutil.Assert.assertThrows; import static com.google.firebase.firestore.testutil.TestUtil.deleteMutation; import static com.google.firebase.firestore.testutil.TestUtil.deletedDoc; @@ -31,10 +32,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.firebase.Timestamp; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.RealtimePipeline; import com.google.firebase.firestore.bundle.BundledQuery; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.core.Target; -import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.MutableDocument; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.model.mutation.FieldMask; @@ -42,6 +45,7 @@ import com.google.firebase.firestore.model.mutation.MutationBatch; import com.google.firebase.firestore.model.mutation.PatchMutation; import com.google.firebase.firestore.model.mutation.SetMutation; +import com.google.firebase.firestore.pipeline.Expr; import com.google.firebase.firestore.proto.WriteBatch; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.testutil.TestUtil; @@ -86,7 +90,7 @@ static class TestWriteBuilder { TestWriteBuilder addSet() { builder.setUpdate( com.google.firestore.v1.Document.newBuilder() - .setName("projects/p/databases/d/documents/foo/bar") + .setName("projects/projectId/databases/(default)/documents/foo/bar") .putFields("a", Value.newBuilder().setStringValue("b").build()) .putFields("num", Value.newBuilder().setIntegerValue(1).build())); return this; @@ -97,7 +101,7 @@ TestWriteBuilder addPatch() { builder .setUpdate( com.google.firestore.v1.Document.newBuilder() - .setName("projects/p/databases/d/documents/bar/baz") + .setName("projects/projectId/databases/(default)/documents/bar/baz") .putFields("a", Value.newBuilder().setStringValue("b").build()) .putFields("num", Value.newBuilder().setIntegerValue(1).build())) .setUpdateMask(DocumentMask.newBuilder().addFieldPaths("a")) @@ -107,7 +111,7 @@ TestWriteBuilder addPatch() { @CanIgnoreReturnValue TestWriteBuilder addDelete() { - builder.setDelete("projects/p/databases/d/documents/baz/quux"); + builder.setDelete("projects/projectId/databases/(default)/documents/baz/quux"); return this; } @@ -130,7 +134,7 @@ TestWriteBuilder addLegacyTransform() { builder .setTransform( DocumentTransform.newBuilder() - .setDocument("projects/p/databases/d/documents/docs/1") + .setDocument("projects/projectId/databases/(default)/documents/docs/1") .addFieldTransforms( FieldTransform.newBuilder() .setFieldPath("integer") @@ -155,8 +159,7 @@ Write build() { @Before public void setUp() { - DatabaseId databaseId = DatabaseId.forDatabase("p", "d"); - remoteSerializer = new RemoteSerializer(databaseId); + remoteSerializer = new RemoteSerializer(DATABASE_ID); serializer = new LocalSerializer(remoteSerializer); } @@ -283,7 +286,7 @@ public void testEncodesMutationBatch() { Write.newBuilder() .setUpdate( com.google.firestore.v1.Document.newBuilder() - .setName("projects/p/databases/d/documents/foo/bar") + .setName("projects/projectId/databases/(default)/documents/foo/bar") .putFields("a", Value.newBuilder().setStringValue("b").build())) .setUpdateMask(DocumentMask.newBuilder().addFieldPaths("a")) .build(); @@ -313,7 +316,7 @@ public void testEncodesFoundDocument() { com.google.firebase.firestore.proto.MaybeDocument.newBuilder() .setDocument( com.google.firestore.v1.Document.newBuilder() - .setName("projects/p/databases/d/documents/some/path") + .setName("projects/projectId/databases/(default)/documents/some/path") .putFields("foo", Value.newBuilder().setStringValue("bar").build()) .setUpdateTime( com.google.protobuf.Timestamp.newBuilder().setSeconds(0).setNanos(42000))) @@ -332,7 +335,7 @@ public void testEncodesDeletedDocument() { com.google.firebase.firestore.proto.MaybeDocument.newBuilder() .setNoDocument( com.google.firebase.firestore.proto.NoDocument.newBuilder() - .setName("projects/p/databases/d/documents/some/path") + .setName("projects/projectId/databases/(default)/documents/some/path") .setReadTime( com.google.protobuf.Timestamp.newBuilder().setSeconds(0).setNanos(42000))) .build(); @@ -350,7 +353,7 @@ public void testEncodesUnknownDocument() { com.google.firebase.firestore.proto.MaybeDocument.newBuilder() .setUnknownDocument( com.google.firebase.firestore.proto.UnknownDocument.newBuilder() - .setName("projects/p/databases/d/documents/some/path") + .setName("projects/projectId/databases/(default)/documents/some/path") .setVersion( com.google.protobuf.Timestamp.newBuilder().setSeconds(0).setNanos(42000))) .setHasCommittedMutations(true) @@ -372,7 +375,7 @@ public void testEncodesTargetData() { TargetData targetData = new TargetData( - query.toTarget(), + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), targetId, sequenceNumber, QueryPurpose.LISTEN, @@ -415,7 +418,7 @@ public void localSerializerShouldDropExpectedCountInTargetData() { TargetData targetData = new TargetData( - query.toTarget(), + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), targetId, sequenceNumber, QueryPurpose.LISTEN, @@ -486,4 +489,32 @@ public void testEncodesLimitToLastQuery() { assertEquals(bundledQuery, decodedBundledQuery); } + + @Test + public void encodesTargetDataWithPipeline() { + FirebaseFirestore db = com.google.firebase.firestore.TestUtil.firestore(); + RealtimePipeline pipeline = + db.realtimePipeline() + .collection("rooms") + .where(Expr.field("name").eq("test room")) + .sort(Expr.field("age").descending()) + .limit(10); + + TargetOrPipeline targetOrPipeline = new TargetOrPipeline.PipelineWrapper(pipeline); + + TargetData targetData = + new TargetData( + targetOrPipeline, + 1, + 2, + QueryPurpose.LISTEN, + TestUtil.version(100), + TestUtil.version(100), + ByteString.EMPTY, + null); + + com.google.firebase.firestore.proto.Target encoded = serializer.encodeTargetData(targetData); + TargetData decoded = serializer.decodeTargetData(encoded); + assertEquals(targetData, decoded); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java index 21823b1af42..c92694db86a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java @@ -15,6 +15,8 @@ package com.google.firebase.firestore.local; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.firestore.TestUtil.DATABASE_ID; +import static com.google.firebase.firestore.TestUtil.firestore; import static com.google.firebase.firestore.testutil.TestUtil.addedRemoteEvent; import static com.google.firebase.firestore.testutil.TestUtil.assertSetEquals; import static com.google.firebase.firestore.testutil.TestUtil.deleteMutation; @@ -55,12 +57,16 @@ import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.FieldValue; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.RealtimePipeline; +import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.bundle.BundleMetadata; import com.google.firebase.firestore.bundle.BundledQuery; import com.google.firebase.firestore.bundle.NamedQuery; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.core.Target; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex; @@ -114,6 +120,10 @@ public abstract class LocalStoreTestCase { private @Nullable QueryResult lastQueryResult; private int lastTargetId; + // TODO(b/352982463): This flag should be `final` but we cannot do so due to the test structure. + protected boolean shouldUsePipeline; + private FirebaseFirestore db; + abstract Persistence getPersistence(); abstract boolean garbageCollectorIsEager(); @@ -124,6 +134,8 @@ public void setUp() { lastChanges = null; lastQueryResult = null; lastTargetId = 0; + shouldUsePipeline = false; + db = firestore(); localStorePersistence = getPersistence(); queryEngine = new CountingQueryEngine(new QueryEngine()); @@ -206,15 +218,43 @@ protected void configureFieldIndexes(List fieldIndexes) { localStore.configureFieldIndexes(fieldIndexes); } + // Helper to convert a Query to a RealtimePipeline. + // This is identical to the one in QueryEngineTestBase. + private RealtimePipeline convertQueryToPipeline(Query query) { + return query.toRealtimePipeline(db, new UserDataReader(DATABASE_ID)); + } + protected int allocateQuery(Query query) { - TargetData targetData = localStore.allocateTarget(query.toTarget()); + com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper targetWrapper = + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()); + TargetData targetData; + if (shouldUsePipeline) { + targetData = + localStore.allocateTarget( + new com.google.firebase.firestore.core.TargetOrPipeline.PipelineWrapper( + convertQueryToPipeline(query))); + } else { + targetData = localStore.allocateTarget(targetWrapper); + } + lastTargetId = targetData.getTargetId(); return targetData.getTargetId(); } protected void executeQuery(Query query) { resetPersistenceStats(); - lastQueryResult = localStore.executeQuery(query, /* usePreviousResults= */ true); + if (shouldUsePipeline) { + lastQueryResult = + localStore.executeQuery( + new com.google.firebase.firestore.core.QueryOrPipeline.PipelineWrapper( + convertQueryToPipeline(query)), + /* usePreviousResults= */ true); + } else { + lastQueryResult = + localStore.executeQuery( + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper(query), + /* usePreviousResults= */ true); + } } protected void setIndexAutoCreationEnabled(boolean isEnabled) { @@ -953,8 +993,8 @@ public void testCanExecuteDocumentQueries() { setMutation("foo/baz", map("foo", "baz")), setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")))); Query query = Query.atPath(ResourcePath.fromSegments(asList("foo", "bar"))); - QueryResult result = localStore.executeQuery(query, /* usePreviousResults= */ true); - assertThat(values(result.getDocuments())) + executeQuery(query); + assertThat(values(lastQueryResult.getDocuments())) .containsExactly(doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations()); } @@ -968,8 +1008,8 @@ public void testCanExecuteCollectionQueries() { setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")), setMutation("fooo/blah", map("fooo", "blah")))); Query query = query("foo"); - QueryResult result = localStore.executeQuery(query, /* usePreviousResults= */ true); - assertThat(values(result.getDocuments())) + executeQuery(query); + assertThat(values(lastQueryResult.getDocuments())) .containsExactly( doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations(), doc("foo/baz", 0, map("foo", "baz")).setHasLocalMutations()); @@ -985,8 +1025,8 @@ public void testCanExecuteMixedCollectionQueries() { applyRemoteEvent(updateRemoteEvent(doc("foo/bar", 20, map("a", "b")), asList(2), emptyList())); writeMutation(setMutation("foo/bonk", map("a", "b"))); - QueryResult result = localStore.executeQuery(query, /* usePreviousResults= */ true); - assertThat(values(result.getDocuments())) + executeQuery(query); + assertThat(values(lastQueryResult.getDocuments())) .containsExactly( doc("foo/bar", 20, map("a", "b")), doc("foo/baz", 10, map("a", "b")), @@ -1004,7 +1044,7 @@ public void testReadsAllDocumentsForInitialCollectionQueries() { resetPersistenceStats(); - localStore.executeQuery(query, /* usePreviousResults= */ true); + executeQuery(query); assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1); assertOverlayTypes(keyMap("foo/bonk", CountingQueryEngine.OverlayType.Set)); @@ -1016,6 +1056,10 @@ public void testPersistsResumeTokens() { Query query = query("foo/bar"); int targetId = allocateQuery(query); + TargetOrPipeline targetOrPipeline = + shouldUsePipeline + ? new TargetOrPipeline.PipelineWrapper(convertQueryToPipeline(query)) + : new TargetOrPipeline.TargetWrapper(query.toTarget()); applyRemoteEvent(noChangeEvent(targetId, 1000)); @@ -1023,7 +1067,7 @@ public void testPersistsResumeTokens() { localStore.releaseTarget(targetId); // Should come back with the same resume token - TargetData targetData2 = localStore.allocateTarget(query.toTarget()); + TargetData targetData2 = localStore.allocateTarget(targetOrPipeline); assertEquals(resumeToken(1000), targetData2.getResumeToken()); } @@ -1033,6 +1077,10 @@ public void testDoesNotReplaceResumeTokenWithEmptyByteString() { Query query = query("foo/bar"); int targetId = allocateQuery(query); + TargetOrPipeline targetOrPipeline = + shouldUsePipeline + ? new TargetOrPipeline.PipelineWrapper(convertQueryToPipeline(query)) + : new TargetOrPipeline.TargetWrapper(query.toTarget()); applyRemoteEvent(noChangeEvent(targetId, 1000)); @@ -1043,7 +1091,7 @@ public void testDoesNotReplaceResumeTokenWithEmptyByteString() { localStore.releaseTarget(targetId); // Should come back with the same resume token - TargetData targetData2 = localStore.allocateTarget(query.toTarget()); + TargetData targetData2 = localStore.allocateTarget(targetOrPipeline); assertEquals(resumeToken(1000), targetData2.getResumeToken()); } @@ -1154,6 +1202,10 @@ public void testIgnoresTargetMappingAfterExistenceFilterMismatch() { Query query = query("foo").filter(filter("matches", "==", true)); int targetId = allocateQuery(query); + TargetOrPipeline targetOrPipeline = + shouldUsePipeline + ? new TargetOrPipeline.PipelineWrapper(convertQueryToPipeline(query)) + : new TargetOrPipeline.TargetWrapper(query.toTarget()); executeQuery(query); @@ -1164,13 +1216,13 @@ public void testIgnoresTargetMappingAfterExistenceFilterMismatch() { applyRemoteEvent(noChangeEvent(targetId, 10)); updateViews(targetId, /* fromCache= */ false); - TargetData cachedTargetData = localStore.getTargetData(query.toTarget()); + TargetData cachedTargetData = localStore.getTargetData(targetOrPipeline); Assert.assertEquals(version(10), cachedTargetData.getLastLimboFreeSnapshotVersion()); // Create an existence filter mismatch and verify that the last limbo free snapshot version // is deleted applyRemoteEvent(existenceFilterEvent(targetId, keySet(key("foo/a")), 2, 20)); - cachedTargetData = localStore.getTargetData(query.toTarget()); + cachedTargetData = localStore.getTargetData(targetOrPipeline); Assert.assertEquals(version(0), cachedTargetData.getLastLimboFreeSnapshotVersion()); Assert.assertEquals(ByteString.EMPTY, cachedTargetData.getResumeToken()); @@ -1188,24 +1240,28 @@ public void testLastLimboFreeSnapshotIsAdvancedDuringViewProcessing() { Query query = query("foo"); Target target = query.toTarget(); int targetId = allocateQuery(query); + TargetOrPipeline targetOrPipeline = + shouldUsePipeline + ? new TargetOrPipeline.PipelineWrapper(convertQueryToPipeline(query)) + : new TargetOrPipeline.TargetWrapper(target); // Advance the target snapshot. applyRemoteEvent(noChangeEvent(targetId, 10)); // At this point, we have not yet confirmed that the target is limbo free. - TargetData cachedTargetData = localStore.getTargetData(target); + TargetData cachedTargetData = localStore.getTargetData(targetOrPipeline); Assert.assertEquals(SnapshotVersion.NONE, cachedTargetData.getLastLimboFreeSnapshotVersion()); // Mark the view synced, which updates the last limbo free snapshot version. updateViews(targetId, /* fromCache= */ false); - cachedTargetData = localStore.getTargetData(target); + cachedTargetData = localStore.getTargetData(targetOrPipeline); Assert.assertEquals(version(10), cachedTargetData.getLastLimboFreeSnapshotVersion()); // The last limbo free snapshot version is persisted even if we release the target. releaseTarget(targetId); if (!garbageCollectorIsEager()) { - cachedTargetData = localStore.getTargetData(target); + cachedTargetData = localStore.getTargetData(targetOrPipeline); Assert.assertEquals(version(10), cachedTargetData.getLastLimboFreeSnapshotVersion()); } } @@ -1734,7 +1790,7 @@ public void testUpdateOnRemoteDocLeadsToUpdateOverlay() { resetPersistenceStats(); - localStore.executeQuery(query, /* usePreviousResults= */ true); + executeQuery(query); assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1); assertOverlayTypes(keyMap("foo/baz", CountingQueryEngine.OverlayType.Patch)); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LruGarbageCollectorTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LruGarbageCollectorTestCase.java index cf9ced51c3d..69d211d9446 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LruGarbageCollectorTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LruGarbageCollectorTestCase.java @@ -104,7 +104,11 @@ private TargetData nextTargetData() { int targetId = ++previousTargetId; long sequenceNumber = persistence.getReferenceDelegate().getCurrentSequenceNumber(); Query query = query("path" + targetId); - return new TargetData(query.toTarget(), targetId, sequenceNumber, QueryPurpose.LISTEN); + return new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), + targetId, + sequenceNumber, + QueryPurpose.LISTEN); } private void updateTargetInTransaction(TargetData targetData) { diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryLocalStorePipelineTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryLocalStorePipelineTest.java new file mode 100644 index 00000000000..88b60f707bf --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryLocalStorePipelineTest.java @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.local; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MemoryLocalStorePipelineTest extends MemoryLocalStoreTest { + @Before + @Override + public void setUp() { + super.setUp(); + shouldUsePipeline = true; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryQueryEnginePipelineTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryQueryEnginePipelineTest.java new file mode 100644 index 00000000000..25a1f553b4e --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/MemoryQueryEnginePipelineTest.java @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.local; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MemoryQueryEnginePipelineTest extends MemoryQueryEngineTest { + @Before + @Override + public void setUp() { + super.setUp(); + shouldUsePipeline = true; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/PersistenceTestHelpers.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/PersistenceTestHelpers.java index 24444921719..86cee48ef29 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/PersistenceTestHelpers.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/PersistenceTestHelpers.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.local; +import static com.google.firebase.firestore.TestUtil.PROJECT_ID; import static com.google.firebase.firestore.local.SQLitePersistence.databaseName; import android.content.Context; @@ -72,7 +73,7 @@ public static MemoryPersistence createEagerGCMemoryPersistence() { } public static MemoryPersistence createLRUMemoryPersistence(LruGarbageCollector.Params params) { - DatabaseId databaseId = DatabaseId.forProject("projectId"); + DatabaseId databaseId = DatabaseId.forProject(PROJECT_ID); LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); MemoryPersistence persistence = MemoryPersistence.createLruGcMemoryPersistence(params, serializer); @@ -82,7 +83,7 @@ public static MemoryPersistence createLRUMemoryPersistence(LruGarbageCollector.P private static SQLitePersistence openSQLitePersistence( String name, LruGarbageCollector.Params params) { - DatabaseId databaseId = DatabaseId.forProject("projectId"); + DatabaseId databaseId = DatabaseId.forProject(PROJECT_ID); LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); Context context = ApplicationProvider.getApplicationContext(); SQLitePersistence persistence = diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java index aedd1401c74..fe7250191ad 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java @@ -14,7 +14,8 @@ package com.google.firebase.firestore.local; -import static com.google.firebase.firestore.model.DocumentCollections.emptyMutableDocumentMap; +import static com.google.firebase.firestore.TestUtil.DATABASE_ID; +import static com.google.firebase.firestore.TestUtil.firestore; import static com.google.firebase.firestore.testutil.TestUtil.andFilters; import static com.google.firebase.firestore.testutil.TestUtil.doc; import static com.google.firebase.firestore.testutil.TestUtil.docSet; @@ -27,12 +28,15 @@ import static com.google.firebase.firestore.testutil.TestUtil.version; import static org.junit.Assert.assertEquals; -import com.google.android.gms.common.internal.Preconditions; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.RealtimePipeline; +import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.View; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; @@ -94,9 +98,14 @@ public abstract class QueryEngineTestCase { private @Nullable Boolean expectFullCollectionScan; + protected boolean shouldUsePipeline; + private FirebaseFirestore db; + @Before public void setUp() { expectFullCollectionScan = null; + shouldUsePipeline = false; + db = firestore(); persistence = getPersistence(); @@ -117,12 +126,12 @@ public void setUp() { remoteDocumentCache, mutationQueue, documentOverlayCache, indexManager) { @Override public ImmutableSortedMap getDocumentsMatchingQuery( - Query query, IndexOffset offset) { + QueryOrPipeline query, IndexOffset offset, @Nullable QueryContext context) { assertEquals( "Observed query execution mode did not match expectation", expectFullCollectionScan, IndexOffset.NONE.equals(offset)); - return super.getDocumentsMatchingQuery(query, offset); + return super.getDocumentsMatchingQuery(query, offset, context); } }; queryEngine.initialize(localDocuments, indexManager); @@ -200,17 +209,33 @@ private T expectFullCollectionScan(Callable c) throws Exception { } } + // Helper to convert a Query to a RealtimePipeline. + // This is identical to the one in LocalStoreTestCase. + private RealtimePipeline convertQueryToPipeline(Query query) { + return query.toRealtimePipeline(db, new UserDataReader(DATABASE_ID)); + } + protected DocumentSet runQuery(Query query, SnapshotVersion lastLimboFreeSnapshotVersion) { - Preconditions.checkNotNull( + com.google.android.gms.common.internal.Preconditions.checkNotNull( expectFullCollectionScan, "Encountered runQuery() call not wrapped in expectOptimizedCollectionQuery()/expectFullCollectionQuery()"); + + QueryOrPipeline queryOrPipeline; + if (shouldUsePipeline) { + queryOrPipeline = new QueryOrPipeline.PipelineWrapper(convertQueryToPipeline(query)); + } else { + queryOrPipeline = new QueryOrPipeline.QueryWrapper(query); + } + ImmutableSortedMap docs = queryEngine.getDocumentsMatchingQuery( - query, + queryOrPipeline, lastLimboFreeSnapshotVersion, targetCache.getMatchingKeysForTargetId(TEST_TARGET_ID)); View view = - new View(query, new ImmutableSortedSet<>(Collections.emptyList(), DocumentKey::compareTo)); + new View( + queryOrPipeline, + new ImmutableSortedSet<>(Collections.emptyList(), DocumentKey::compareTo)); View.DocumentChanges viewDocChanges = view.computeDocChanges(docs); return view.applyChanges(viewDocChanges).getSnapshot().getDocuments(); } @@ -402,11 +427,18 @@ public void limitQueriesUseInitialResultsIfLastDocumentInLimitIsUnchanged() thro addDocumentWithEventVersion(version(1), doc("coll/a", 1, map("order", 2))); addMutation(DOC_A_EMPTY_PATCH); - // Since the last document in the limit didn't change (and hence we know that all documents - // written prior to query execution still sort after "coll/b"), we should use an Index-Free - // query. DocumentSet docs = - expectOptimizedCollectionScan(() -> runQuery(query, LAST_LIMBO_FREE_SNAPSHOT)); + shouldUsePipeline + ? + // Pipeline always use full collection scan if there is a limit stage + expectFullCollectionScan(() -> runQuery(query, LAST_LIMBO_FREE_SNAPSHOT)) + : + // Since the last document in the limit didn't change (and hence we know that all + // documents + // written prior to query execution still sort after "coll/b"), we should use an + // Index-Free + // query. + expectOptimizedCollectionScan(() -> runQuery(query, LAST_LIMBO_FREE_SNAPSHOT)); assertEquals( docSet( query.comparator(), @@ -425,14 +457,8 @@ public void doesNotIncludeDocumentsDeletedByMutation() throws Exception { // Add an unacknowledged mutation addMutation(new DeleteMutation(key("coll/b"), Precondition.NONE)); - ImmutableSortedMap docs = - expectFullCollectionScan( - () -> - queryEngine.getDocumentsMatchingQuery( - query, - LAST_LIMBO_FREE_SNAPSHOT, - targetCache.getMatchingKeysForTargetId(TEST_TARGET_ID))); - assertEquals(emptyMutableDocumentMap().insert(MATCHING_DOC_A.getKey(), MATCHING_DOC_A), docs); + DocumentSet docs = expectFullCollectionScan(() -> runQuery(query, LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query.comparator(), MATCHING_DOC_A), docs); } @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/RemoteDocumentCacheTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/RemoteDocumentCacheTestCase.java index dae151e7e90..9018b96e5fe 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/RemoteDocumentCacheTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/RemoteDocumentCacheTestCase.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertNotEquals; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; import com.google.firebase.firestore.model.MutableDocument; @@ -183,7 +184,9 @@ public void testGetAllFromCollection() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("b"), IndexOffset.NONE, new HashSet()); + new QueryOrPipeline.QueryWrapper(query("b")), + IndexOffset.NONE, + new HashSet()); assertThat(results.values()) .containsExactly(doc("b/1", 42, DOC_DATA), doc("b/2", 42, DOC_DATA)); } @@ -196,7 +199,9 @@ public void testGetAllFromExcludesSubcollections() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("a"), IndexOffset.NONE, new HashSet()); + new QueryOrPipeline.QueryWrapper(query("a")), + IndexOffset.NONE, + new HashSet()); assertThat(results.values()) .containsExactly(doc("a/1", 42, DOC_DATA), doc("a/2", 42, DOC_DATA)); } @@ -209,7 +214,9 @@ public void testGetAllFromSinceReadTimeAndSeconds() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("b"), IndexOffset.createSuccessor(version(12), -1), new HashSet()); + new QueryOrPipeline.QueryWrapper(query("b")), + IndexOffset.createSuccessor(version(12), -1), + new HashSet()); assertThat(results.values()).containsExactly(doc("b/new", 3, DOC_DATA)); } @@ -221,7 +228,9 @@ public void testGetAllFromSinceReadTimeAndNanoseconds() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("b"), IndexOffset.createSuccessor(version(1, 2), -1), new HashSet()); + new QueryOrPipeline.QueryWrapper(query("b")), + IndexOffset.createSuccessor(version(1, 2), -1), + new HashSet()); assertThat(results.values()).containsExactly(doc("b/new", 1, DOC_DATA)); } @@ -234,7 +243,7 @@ public void testGetAllFromSinceReadTimeAndDocumentKey() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("b"), + new QueryOrPipeline.QueryWrapper(query("b")), IndexOffset.create(version(11), key("b/b"), -1), new HashSet()); assertThat(results.values()).containsExactly(doc("b/c", 3, DOC_DATA), doc("b/d", 4, DOC_DATA)); @@ -247,7 +256,9 @@ public void testGetAllFromUsesReadTimeNotUpdateTime() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("b"), IndexOffset.createSuccessor(version(1), -1), new HashSet()); + new QueryOrPipeline.QueryWrapper(query("b")), + IndexOffset.createSuccessor(version(1), -1), + new HashSet()); assertThat(results.values()).containsExactly(doc("b/old", 1, DOC_DATA)); } @@ -259,7 +270,8 @@ public void testGetMatchingDocsAppliesQueryCheck() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("a").filter(filter("matches", "==", true)), + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper( + query("a").filter(filter("matches", "==", true))), IndexOffset.createSuccessor(version(1), -1), new HashSet()); assertThat(results.values()).containsExactly(doc("a/2", 1, map("matches", true))); @@ -272,7 +284,7 @@ public void testGetMatchingDocsRespectsMutatedDocs() { Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("a").filter(filter("matches", "==", true)), + new QueryOrPipeline.QueryWrapper(query("a").filter(filter("matches", "==", true))), IndexOffset.createSuccessor(version(1), -1), new HashSet(Collections.singletonList(key("a/2")))); assertThat(results.values()).containsExactly(doc("a/2", 1, map("matches", false))); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStorePipelineTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStorePipelineTest.java new file mode 100644 index 00000000000..c5d7fc331d5 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStorePipelineTest.java @@ -0,0 +1,43 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.local; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +// Note: it does not extend SQLiteLocalStoreTest because pipelines do not support indexes +// and SQLiteLocalStoreTest is only verifying behaviors with indexes. +public class SQLiteLocalStorePipelineTest extends LocalStoreTestCase { + @Override + Persistence getPersistence() { + return PersistenceTestHelpers.createSQLitePersistence(); + } + + @Override + boolean garbageCollectorIsEager() { + return false; + } + + @Before + @Override + public void setUp() { + super.setUp(); + shouldUsePipeline = true; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEnginePipelineTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEnginePipelineTest.java new file mode 100644 index 00000000000..32ee8087bf4 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEnginePipelineTest.java @@ -0,0 +1,37 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.local; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SQLiteQueryEnginePipelineTest extends QueryEngineTestCase { + + @Override + Persistence getPersistence() { + return PersistenceTestHelpers.createSQLitePersistence(); + } + + @Before + @Override + public void setUp() { + super.setUp(); + shouldUsePipeline = true; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java index 6e250332143..86a560fdc37 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java @@ -35,6 +35,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex.IndexOffset; @@ -446,14 +447,18 @@ public void existingDocumentsRemainReadableAfterIndexFreeMigration() { // read time has been set. Map results = remoteDocumentCache.getDocumentsMatchingQuery( - query("coll"), IndexOffset.NONE, new HashSet()); + new QueryOrPipeline.QueryWrapper(query("coll")), + IndexOffset.NONE, + new HashSet()); assertResultsContain(results, "coll/existing", "coll/old", "coll/current", "coll/new"); // Queries that filter by read time only return documents that were written after the index-free // migration. results = remoteDocumentCache.getDocumentsMatchingQuery( - query("coll"), IndexOffset.createSuccessor(version(2), -1), new HashSet()); + new com.google.firebase.firestore.core.QueryOrPipeline.QueryWrapper(query("coll")), + IndexOffset.createSuccessor(version(2), -1), + new HashSet()); assertResultsContain(results, "coll/new"); } @@ -550,7 +555,8 @@ public void rewritesCanonicalIds() { Query filteredQuery = query("colletion").filter(filter("foo", "==", "bar")); TargetData initialTargetData = new TargetData( - filteredQuery.toTarget(), + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + filteredQuery.toTarget()), /* targetId= */ 2, /* sequenceNumber= */ 1, QueryPurpose.LISTEN); @@ -571,7 +577,10 @@ public void rewritesCanonicalIds() { try { Target targetProto = Target.parseFrom(targetProtoBytes); TargetData targetData = serializer.decodeTargetData(targetProto); - String expectedCanonicalId = targetData.getTarget().getCanonicalId(); + String expectedCanonicalId = + targetData.getTarget().isTarget() + ? targetData.getTarget().target().getCanonicalId() + : targetData.getTarget().pipeline().canonicalId(); assertEquals(expectedCanonicalId, actualCanonicalId); } catch (InvalidProtocolBufferException e) { fail("Failed to decode Target data"); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteTargetCacheTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteTargetCacheTest.java index 1ee586c1947..ec761c94b80 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteTargetCacheTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteTargetCacheTest.java @@ -49,7 +49,11 @@ public void testMetadataPersistedAcrossRestarts() { Query query = query("rooms"); TargetData targetData = - new TargetData(query.toTarget(), targetId, originalSequenceNumber, QueryPurpose.LISTEN); + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), + targetId, + originalSequenceNumber, + QueryPurpose.LISTEN); db1.runTransaction( "add query data", () -> { diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/TargetCacheTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/TargetCacheTestCase.java index 58a3a57ba69..3be8953d911 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/TargetCacheTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/TargetCacheTestCase.java @@ -73,7 +73,10 @@ public void tearDown() { @Test public void testReadQueryNotInCache() { - assertNull(targetCache.getTargetData(query("rooms").toTarget())); + assertNull( + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget()))); } @Test @@ -81,7 +84,10 @@ public void testSetAndReadAQuery() { TargetData targetData = newTargetData(query("rooms"), 1, 1); addTargetData(targetData); - TargetData result = targetCache.getTargetData(query("rooms").toTarget()); + TargetData result = + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget())); assertNotNull(result); assertEquals(targetData.getTarget(), result.getTarget()); assertEquals(targetData.getTargetId(), result.getTargetId()); @@ -100,24 +106,44 @@ public void testCanonicalIdCollision() { addTargetData(data1); // Using the other query should not return the query cache entry despite equal canonicalIDs. - assertNull(targetCache.getTargetData(q2.toTarget())); - assertEquals(data1, targetCache.getTargetData(q1.toTarget())); + assertNull( + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q2.toTarget()))); + assertEquals( + data1, + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q1.toTarget()))); TargetData data2 = newTargetData(q2, 2, 1); addTargetData(data2); assertEquals(2, targetCache.getTargetCount()); - assertEquals(data1, targetCache.getTargetData(q1.toTarget())); - assertEquals(data2, targetCache.getTargetData(q2.toTarget())); + assertEquals( + data1, + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q1.toTarget()))); + assertEquals( + data2, + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q2.toTarget()))); removeTargetData(data1); - assertNull(targetCache.getTargetData(q1.toTarget())); - assertEquals(data2, targetCache.getTargetData(q2.toTarget())); + assertNull( + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q1.toTarget()))); + assertEquals( + data2, + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q2.toTarget()))); assertEquals(1, targetCache.getTargetCount()); removeTargetData(data2); - assertNull(targetCache.getTargetData(q1.toTarget())); - assertNull(targetCache.getTargetData(q2.toTarget())); + assertNull( + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q1.toTarget()))); + assertNull( + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q2.toTarget()))); assertEquals(0, targetCache.getTargetCount()); } @@ -129,7 +155,10 @@ public void testSetQueryToNewValue() { TargetData targetData2 = newTargetData(query("rooms"), 1, 2); addTargetData(targetData2); - TargetData result = targetCache.getTargetData(query("rooms").toTarget()); + TargetData result = + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget())); // There's no assertArrayNotEquals assertThat(targetData2.getResumeToken(), not(equalTo(targetData1.getResumeToken()))); @@ -146,14 +175,21 @@ public void testRemoveQuery() { removeTargetData(targetData1); - TargetData result = targetCache.getTargetData(query("rooms").toTarget()); + TargetData result = + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget())); assertNull(result); } @Test public void testRemoveNonExistentQuery() { // no-op, but make sure it doesn't throw. - assertDoesNotThrow(() -> targetCache.getTargetData(query("rooms").toTarget())); + assertDoesNotThrow( + () -> + targetCache.getTargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget()))); } @Test @@ -241,9 +277,19 @@ public void testHighestSequenceNumber() { Query halls = query("halls"); Query garages = query("garages"); - TargetData query1 = new TargetData(rooms.toTarget(), 1, 10, QueryPurpose.LISTEN); + TargetData query1 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(rooms.toTarget()), + 1, + 10, + QueryPurpose.LISTEN); addTargetData(query1); - TargetData query2 = new TargetData(halls.toTarget(), 2, 20, QueryPurpose.LISTEN); + TargetData query2 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(halls.toTarget()), + 2, + 20, + QueryPurpose.LISTEN); addTargetData(query2); assertEquals(20, targetCache.getHighestListenSequenceNumber()); @@ -251,7 +297,13 @@ public void testHighestSequenceNumber() { removeTargetData(query2); assertEquals(20, targetCache.getHighestListenSequenceNumber()); - TargetData query3 = new TargetData(garages.toTarget(), 42, 100, QueryPurpose.LISTEN); + TargetData query3 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + garages.toTarget()), + 42, + 100, + QueryPurpose.LISTEN); addTargetData(query3); assertEquals(100, targetCache.getHighestListenSequenceNumber()); @@ -265,7 +317,13 @@ public void testHighestSequenceNumber() { public void testHighestTargetId() { assertEquals(0, targetCache.getHighestTargetId()); - TargetData query1 = new TargetData(query("rooms").toTarget(), 1, 10, QueryPurpose.LISTEN); + TargetData query1 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("rooms").toTarget()), + 1, + 10, + QueryPurpose.LISTEN); addTargetData(query1); DocumentKey key1 = key("rooms/bar"); @@ -273,7 +331,13 @@ public void testHighestTargetId() { addMatchingKey(key1, 1); addMatchingKey(key2, 1); - TargetData query2 = new TargetData(query("halls").toTarget(), 2, 20, QueryPurpose.LISTEN); + TargetData query2 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("halls").toTarget()), + 2, + 20, + QueryPurpose.LISTEN); addTargetData(query2); DocumentKey key3 = key("halls/foo"); addMatchingKey(key3, 2); @@ -284,7 +348,13 @@ public void testHighestTargetId() { assertEquals(2, targetCache.getHighestTargetId()); // A query with an empty result set still counts. - TargetData query3 = new TargetData(query("garages").toTarget(), 42, 100, QueryPurpose.LISTEN); + TargetData query3 = + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper( + query("garages").toTarget()), + 42, + 100, + QueryPurpose.LISTEN); addTargetData(query3); assertEquals(42, targetCache.getHighestTargetId()); @@ -325,7 +395,7 @@ public void testSnapshotVersion() { private TargetData newTargetData(Query query, int targetId, long version) { long sequenceNumber = ++previousSequenceNumber; return new TargetData( - query.toTarget(), + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), targetId, sequenceNumber, QueryPurpose.LISTEN, diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt index 2b1ef9062ba..84d5fd809f8 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline/testUtil.kt @@ -16,17 +16,23 @@ package com.google.firebase.firestore.pipeline import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.firestore.RealtimePipeline -import com.google.firebase.firestore.TestUtil.FIRESTORE -import com.google.firebase.firestore.TestUtil.USER_DATA_READER +import com.google.firebase.firestore.UserDataReader +import com.google.firebase.firestore.model.DatabaseId import com.google.firebase.firestore.model.MutableDocument import com.google.firebase.firestore.model.Values.NULL_VALUE import com.google.firebase.firestore.model.Values.encodeValue +import com.google.firebase.firestore.remote.RemoteSerializer import com.google.firebase.firestore.testutil.TestUtilKtx.doc import com.google.firestore.v1.Value +private val FAKE_DATABASE_ID = DatabaseId.forProject("project") +private val FAKE_USER_DATA_READER = UserDataReader(FAKE_DATABASE_ID) + val EMPTY_DOC: MutableDocument = doc("foo/1", 0, mapOf()) internal val EVALUATION_CONTEXT: EvaluationContext = - EvaluationContext(RealtimePipeline(FIRESTORE, USER_DATA_READER, emptyList())) + EvaluationContext( + RealtimePipeline(RemoteSerializer(FAKE_DATABASE_ID), FAKE_USER_DATA_READER, emptyList()) + ) internal fun evaluate(expr: Expr): EvaluateResult = evaluate(expr, EMPTY_DOC) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java index b208da20c52..e3282ba10b2 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java @@ -48,6 +48,7 @@ import com.google.firebase.firestore.core.KeyFieldFilter; import com.google.firebase.firestore.core.NotInFilter; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.local.QueryPurpose; import com.google.firebase.firestore.local.TargetData; import com.google.firebase.firestore.model.DatabaseId; @@ -516,21 +517,37 @@ private Order defaultKeyOrder() { @Test public void testEncodesListenRequestLabels() { Query query = query("collection/key"); - TargetData targetData = new TargetData(query.toTarget(), 2, 3, QueryPurpose.LISTEN); + TargetData targetData = + new TargetData( + new TargetOrPipeline.TargetWrapper(query.toTarget()), 2, 3, QueryPurpose.LISTEN); Map result = serializer.encodeListenRequestLabels(targetData); assertNull(result); - targetData = new TargetData(query.toTarget(), 2, 3, QueryPurpose.LIMBO_RESOLUTION); + targetData = + new TargetData( + new TargetOrPipeline.TargetWrapper(query.toTarget()), + 2, + 3, + QueryPurpose.LIMBO_RESOLUTION); result = serializer.encodeListenRequestLabels(targetData); assertEquals(map("goog-listen-tags", "limbo-document"), result); - targetData = new TargetData(query.toTarget(), 2, 3, QueryPurpose.EXISTENCE_FILTER_MISMATCH); + targetData = + new TargetData( + new TargetOrPipeline.TargetWrapper(query.toTarget()), + 2, + 3, + QueryPurpose.EXISTENCE_FILTER_MISMATCH); result = serializer.encodeListenRequestLabels(targetData); assertEquals(map("goog-listen-tags", "existence-filter-mismatch"), result); targetData = - new TargetData(query.toTarget(), 2, 3, QueryPurpose.EXISTENCE_FILTER_MISMATCH_BLOOM); + new TargetData( + new TargetOrPipeline.TargetWrapper(query.toTarget()), + 2, + 3, + QueryPurpose.EXISTENCE_FILTER_MISMATCH_BLOOM); result = serializer.encodeListenRequestLabels(targetData); assertEquals(map("goog-listen-tags", "existence-filter-mismatch-bloom"), result); } @@ -539,7 +556,9 @@ public void testEncodesListenRequestLabels() { public void testEncodesFirstLevelKeyQueries() { Query q = Query.atPath(ResourcePath.fromString("docs/1")); Target actual = - serializer.encodeTarget(new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN)); + serializer.encodeTarget( + new TargetData( + new TargetOrPipeline.TargetWrapper(q.toTarget()), 1, 2, QueryPurpose.LISTEN)); DocumentsTarget.Builder docs = DocumentsTarget.newBuilder().addDocuments("projects/p/databases/d/documents/docs/1"); @@ -1123,7 +1142,11 @@ public void testEncodesBounds() { public void testEncodesResumeTokens() { Query q = Query.atPath(ResourcePath.fromString("docs")); TargetData targetData = - new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN) + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q.toTarget()), + 1, + 2, + QueryPurpose.LISTEN) .withResumeToken(TestUtil.resumeToken(1000), SnapshotVersion.NONE); Target actual = serializer.encodeTarget(targetData); @@ -1152,7 +1175,11 @@ public void testEncodesResumeTokens() { public void testEncodesReadTime() { Query q = Query.atPath(ResourcePath.fromString("docs")); TargetData targetData = - new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN) + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q.toTarget()), + 1, + 2, + QueryPurpose.LISTEN) .withResumeToken(ByteString.EMPTY, version(4000000)); Target actual = serializer.encodeTarget(targetData); @@ -1181,7 +1208,11 @@ public void testEncodesReadTime() { public void encodesExpectedCountWhenResumeTokenIsPresent() { Query q = Query.atPath(ResourcePath.fromString("docs")); TargetData targetData = - new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN) + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q.toTarget()), + 1, + 2, + QueryPurpose.LISTEN) .withResumeToken(TestUtil.resumeToken(1000), SnapshotVersion.NONE) .withExpectedCount(42); Target actual = serializer.encodeTarget(targetData); @@ -1212,7 +1243,11 @@ public void encodesExpectedCountWhenResumeTokenIsPresent() { public void encodesExpectedCountWhenReadTimeIsPresent() { Query q = Query.atPath(ResourcePath.fromString("docs")); TargetData targetData = - new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN) + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q.toTarget()), + 1, + 2, + QueryPurpose.LISTEN) .withResumeToken(ByteString.EMPTY, version(4000000)) .withExpectedCount(42); Target actual = serializer.encodeTarget(targetData); @@ -1243,7 +1278,12 @@ public void encodesExpectedCountWhenReadTimeIsPresent() { public void shouldIgnoreExpectedCountWithoutResumeTokenOrReadTime() { Query q = Query.atPath(ResourcePath.fromString("docs")); TargetData targetData = - new TargetData(q.toTarget(), 1, 2, QueryPurpose.LISTEN).withExpectedCount(42); + new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(q.toTarget()), + 1, + 2, + QueryPurpose.LISTEN) + .withExpectedCount(42); Target actual = serializer.encodeTarget(targetData); StructuredQuery.Builder structuredQueryBuilder = @@ -1272,7 +1312,11 @@ public void shouldIgnoreExpectedCountWithoutResumeTokenOrReadTime() { * TargetData, but for the most part we're just testing variations on Query. */ private static TargetData wrapTargetData(Query query) { - return new TargetData(query.toTarget(), 1, 2, QueryPurpose.LISTEN); + return new TargetData( + new com.google.firebase.firestore.core.TargetOrPipeline.TargetWrapper(query.toTarget()), + 1, + 2, + QueryPurpose.LISTEN); } @Test diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 8814b42baef..f2757c267ea 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -55,7 +55,9 @@ import com.google.firebase.firestore.core.OnlineState; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.core.QueryListener; +import com.google.firebase.firestore.core.QueryOrPipeline; import com.google.firebase.firestore.core.SyncEngine; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.local.LocalStore; import com.google.firebase.firestore.local.LruDelegate; import com.google.firebase.firestore.local.LruGarbageCollector; @@ -577,7 +579,7 @@ private void doListen(JSONObject listenSpec) throws Exception { QueryListener listener = new QueryListener( - query, + new QueryOrPipeline.QueryWrapper(query), options, (value, error) -> { QueryEvent event = new QueryEvent(); @@ -1118,7 +1120,11 @@ private void validateExpectedState(@Nullable JSONObject expectedState) throws JS } TargetData targetData = - new TargetData(query.toTarget(), targetId, ARBITRARY_SEQUENCE_NUMBER, purpose); + new TargetData( + new TargetOrPipeline.TargetWrapper(query.toTarget()), + targetId, + ARBITRARY_SEQUENCE_NUMBER, + purpose); if (queryDataJson.has("resumeToken")) { targetData = targetData.withResumeToken( diff --git a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java index b0c39742336..0d232e71f47 100644 --- a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java +++ b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -48,6 +48,7 @@ import com.google.firebase.firestore.core.OrderBy; import com.google.firebase.firestore.core.OrderBy.Direction; import com.google.firebase.firestore.core.Query; +import com.google.firebase.firestore.core.TargetOrPipeline; import com.google.firebase.firestore.core.UserData.ParsedSetData; import com.google.firebase.firestore.core.UserData.ParsedUpdateData; import com.google.firebase.firestore.local.LocalViewChanges; @@ -352,7 +353,10 @@ public static void testEquality(List> equalityGroups) { public static TargetData targetData(int targetId, QueryPurpose queryPurpose, String path) { return new TargetData( - query(path).toTarget(), targetId, ARBITRARY_SEQUENCE_NUMBER, queryPurpose); + new TargetOrPipeline.TargetWrapper(query(path).toTarget()), + targetId, + ARBITRARY_SEQUENCE_NUMBER, + queryPurpose); } public static ImmutableSortedMap docUpdates(MutableDocument... docs) { @@ -405,7 +409,10 @@ public static Map activeQueries(Iterable targets) for (Integer targetId : targets) { TargetData targetData = new TargetData( - query.toTarget(), targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LISTEN); + new TargetOrPipeline.TargetWrapper(query.toTarget()), + targetId, + ARBITRARY_SEQUENCE_NUMBER, + QueryPurpose.LISTEN); listenMap.put(targetId, targetData); } return listenMap; @@ -422,7 +429,10 @@ public static Map activeLimboQueries( for (Integer targetId : targets) { TargetData targetData = new TargetData( - query.toTarget(), targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LIMBO_RESOLUTION); + new TargetOrPipeline.TargetWrapper(query.toTarget()), + targetId, + ARBITRARY_SEQUENCE_NUMBER, + QueryPurpose.LIMBO_RESOLUTION); listenMap.put(targetId, targetData); } return listenMap;