Skip to content

Commit 5e2192e

Browse files
committed
Complex Tests
1 parent 047969b commit 5e2192e

File tree

1 file changed

+340
-0
lines changed
  • firebase-firestore/src/test/java/com/google/firebase/firestore/pipeline

1 file changed

+340
-0
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.pipeline
16+
17+
import com.google.common.truth.Truth.assertThat
18+
import com.google.firebase.firestore.FieldPath as PublicFieldPath
19+
import com.google.firebase.firestore.FirebaseFirestore
20+
import com.google.firebase.firestore.RealtimePipelineSource
21+
import com.google.firebase.firestore.TestUtil
22+
import com.google.firebase.firestore.model.MutableDocument
23+
import com.google.firebase.firestore.pipeline.Expr.Companion.add
24+
import com.google.firebase.firestore.pipeline.Expr.Companion.and
25+
import com.google.firebase.firestore.pipeline.Expr.Companion.constant
26+
import com.google.firebase.firestore.pipeline.Expr.Companion.field
27+
import com.google.firebase.firestore.pipeline.Expr.Companion.or
28+
import com.google.firebase.firestore.runPipeline
29+
import com.google.firebase.firestore.testutil.TestUtilKtx.doc
30+
import kotlinx.coroutines.flow.flowOf
31+
import kotlinx.coroutines.flow.toList
32+
import kotlinx.coroutines.runBlocking
33+
import org.junit.Test
34+
import org.junit.runner.RunWith
35+
import org.robolectric.RobolectricTestRunner
36+
37+
@RunWith(RobolectricTestRunner::class)
38+
internal class ComplexTests {
39+
40+
private val db: FirebaseFirestore = TestUtil.firestore()
41+
private val collectionId = "test"
42+
private var docIdCounter = 1
43+
44+
private fun nextDocId(): String = "${collectionId}/${docIdCounter++}"
45+
46+
private fun seedDatabase(
47+
numOfDocuments: Int,
48+
numOfFields: Int,
49+
valueSupplier: (Int, Int) -> Any // docIndex, fieldIndex
50+
): List<MutableDocument> {
51+
docIdCounter = 1 // Reset for each seed
52+
return List(numOfDocuments) { docIndex ->
53+
val fields =
54+
(1..numOfFields).associate { fieldIndex ->
55+
"field_$fieldIndex" to valueSupplier(docIndex, fieldIndex)
56+
}
57+
doc(nextDocId(), 1000, fields)
58+
}
59+
}
60+
61+
@Test
62+
fun `where with max number of stages`(): Unit = runBlocking {
63+
val numOfFields = 127
64+
var valueCounter = 1L
65+
val documents = seedDatabase(10, numOfFields) { _, _ -> valueCounter++ }
66+
67+
var pipeline = RealtimePipelineSource(db).collection(collectionId)
68+
for (i in 1..numOfFields) {
69+
pipeline = pipeline.where(field("field_$i").gt(0L))
70+
}
71+
72+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
73+
assertThat(result).containsExactlyElementsIn(documents)
74+
}
75+
76+
@Test
77+
fun `eqAny with max number of elements`(): Unit = runBlocking {
78+
val numOfDocuments = 1000
79+
val maxElements = 3000
80+
var valueCounter = 1L
81+
val documentsSource = seedDatabase(numOfDocuments, 1) { _, _ -> valueCounter++ }
82+
val nonMatchingDoc = doc(nextDocId(), 1000, mapOf("field_1" to 3001L))
83+
val allDocuments = documentsSource + nonMatchingDoc
84+
85+
val values = List(maxElements) { i -> i + 1 }
86+
87+
val pipeline =
88+
RealtimePipelineSource(db).collection(collectionId).where(field("field_1").eqAny(values))
89+
90+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
91+
assertThat(result).containsExactlyElementsIn(documentsSource)
92+
}
93+
94+
@Test
95+
fun `eqAny with max number of elements on multiple fields`(): Unit = runBlocking {
96+
val numOfFields = 10
97+
val numOfDocuments = 100
98+
val maxElements = 3000
99+
var valueCounter = 1L
100+
val documentsSource = seedDatabase(numOfDocuments, numOfFields) { _, _ -> valueCounter++ }
101+
val nonMatchingDoc = doc(nextDocId(), 1000, mapOf("field_1" to 3001L))
102+
val allDocuments = documentsSource + nonMatchingDoc
103+
104+
val values = List(maxElements) { i -> i + 1 }
105+
val conditions = (1..numOfFields).map { i -> field("field_$i").eqAny(values) }
106+
107+
val pipeline =
108+
RealtimePipelineSource(db)
109+
.collection(collectionId)
110+
.where(and(conditions.first(), *conditions.drop(1).toTypedArray()))
111+
112+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
113+
assertThat(result).containsExactlyElementsIn(documentsSource)
114+
}
115+
116+
@Test
117+
fun `notEqAny with max number of elements`(): Unit = runBlocking {
118+
val numOfDocuments = 1000
119+
val maxElements = 3000
120+
var valueCounter = 1L
121+
val documentsSource = seedDatabase(numOfDocuments, 1) { _, _ -> valueCounter++ }
122+
val matchingDoc = doc(nextDocId(), 1000, mapOf("field_1" to 3001L))
123+
val allDocuments = documentsSource + matchingDoc
124+
125+
val values = List(maxElements) { i -> i + 1 }
126+
127+
val pipeline =
128+
RealtimePipelineSource(db).collection(collectionId).where(field("field_1").notEqAny(values))
129+
130+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
131+
assertThat(result).containsExactly(matchingDoc)
132+
}
133+
134+
@Test
135+
fun `notEqAny with max number of elements on multiple fields`(): Unit = runBlocking {
136+
val numOfFields = 10
137+
val numOfDocuments = 100
138+
val maxElements = 3000
139+
// Seed documents where field_x = (docIndex * numOfFields) + fieldIndex_1_based
140+
// This makes values unique and predictable.
141+
// For doc 0, field_1=1, field_2=2 ... field_10=10
142+
// For doc 1, field_1=11, field_2=12 ... field_10=20
143+
// Max value will be (99*10)+10 = 990+10 = 1000.
144+
val documentsSource =
145+
seedDatabase(numOfDocuments, numOfFields) { docIdx, fieldIdx ->
146+
(docIdx * numOfFields) + fieldIdx
147+
}
148+
149+
// This doc has field_1 = 3001L (which is NOT IN 1..3000)
150+
// Other fields are not set, so they are absent.
151+
// An absent field when checked with notEqAny(someList) should evaluate to true (as it's not in
152+
// the list).
153+
val matchingDocData = mutableMapOf<String, Any>("field_1" to (maxElements + 1))
154+
// For the OR condition to be specific to field_1, other fields in matchingDoc
155+
// must be IN the `values` list if they exist.
156+
// Let's make other fields in matchingDoc have values that are in the `values` list.
157+
for (i in 2..numOfFields) {
158+
matchingDocData["field_$i"] = i // value i is in 1..3000
159+
}
160+
val matchingDoc = doc(nextDocId(), 1000, matchingDocData)
161+
val allDocuments = documentsSource + matchingDoc
162+
163+
val values = List(maxElements) { i -> (i + 1) } // 1 to 3000
164+
165+
val conditions = (1..numOfFields).map { i -> field("field_$i").notEqAny(values) }
166+
167+
val pipeline =
168+
RealtimePipelineSource(db)
169+
.collection(collectionId)
170+
.where(or(conditions.first(), *conditions.drop(1).toTypedArray()))
171+
172+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
173+
// matchingDoc: field_1=3001 (not in values) -> true. Other fields are in values. So OR is true.
174+
// documentsSource: All fields have values from 1 to 1000. All are IN `values`. So notEqAny is
175+
// false for all fields. OR is false.
176+
assertThat(result).containsExactly(matchingDoc)
177+
}
178+
179+
@Test
180+
fun `arrayContainsAny with large number of elements`(): Unit = runBlocking {
181+
val numOfDocuments = 1000
182+
val maxElements = 3000
183+
var valueCounter = 1
184+
val documentsSource =
185+
seedDatabase(numOfDocuments, 1) { _, _ ->
186+
listOf(valueCounter++)
187+
} // field_1 contains [valueCounter]
188+
val nonMatchingDoc = doc(nextDocId(), 1000, mapOf("field_1" to listOf((maxElements + 1))))
189+
val allDocuments = documentsSource + nonMatchingDoc
190+
191+
val valuesToSearch = List(maxElements) { i -> (i + 1) }
192+
193+
val pipeline =
194+
RealtimePipelineSource(db)
195+
.collection(collectionId)
196+
.where(field("field_1").arrayContainsAny(valuesToSearch))
197+
198+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
199+
assertThat(result).containsExactlyElementsIn(documentsSource)
200+
}
201+
202+
@Test
203+
fun `arrayContainsAny with max number of elements on multiple fields`(): Unit = runBlocking {
204+
val numOfFields = 10
205+
val numOfDocuments = 100
206+
val maxElements = 3000
207+
var valueCounter = 1
208+
val documentsSource =
209+
seedDatabase(numOfDocuments, numOfFields) { _, _ -> listOf(valueCounter++) }
210+
211+
// nonMatchingDoc: field_1 = [3001L]. Other fields will be arrays like [3002L], [3003L] etc.
212+
// if we use valueCounter for them.
213+
// To make it non-matching for an OR condition, all its array fields must not contain any of
214+
// valuesToSearch.
215+
val nonMatchingDocData =
216+
(1..numOfFields).associate { i -> "field_$i" to listOf((maxElements + i)) }
217+
val nonMatchingDoc = doc(nextDocId(), 1000, nonMatchingDocData)
218+
val allDocuments = documentsSource + nonMatchingDoc
219+
220+
val valuesToSearch = List(maxElements) { i -> i + 1 } // 1 to 3000
221+
222+
val conditions =
223+
(1..numOfFields).map { i -> field("field_$i").arrayContainsAny(valuesToSearch) }
224+
225+
val pipeline =
226+
RealtimePipelineSource(db)
227+
.collection(collectionId)
228+
.where(or(conditions.first(), *conditions.drop(1).toTypedArray()))
229+
230+
val result = runPipeline(db, pipeline, flowOf(*allDocuments.toTypedArray())).toList()
231+
// documentsSource: each field_i has a list like [some_value_between_1_and_1000].
232+
// Since valuesToSearch is [1..3000], arrayContainsAny will be true for each field. So OR is
233+
// true.
234+
// nonMatchingDoc: field_i has list like [3000+i]. None of these are in valuesToSearch.
235+
// So arrayContainsAny is false for all fields. OR is false.
236+
assertThat(result).containsExactlyElementsIn(documentsSource)
237+
}
238+
239+
@Test
240+
fun `sortBy max num of fields without index`(): Unit = runBlocking {
241+
val numOfFields = 31
242+
val numOfDocuments = 100
243+
// All docs have field_i = 10L
244+
val documents = seedDatabase(numOfDocuments, numOfFields) { _, _ -> 10L }
245+
246+
val sortOrders =
247+
(1..numOfFields)
248+
.map { i -> field("field_$i").ascending() }
249+
.plus(field(PublicFieldPath.documentId()).ascending())
250+
251+
val pipeline =
252+
RealtimePipelineSource(db)
253+
.collection(collectionId)
254+
.sort(sortOrders.first(), *sortOrders.drop(1).toTypedArray())
255+
256+
// Since all field values are the same, sort order is determined by document ID.
257+
val expectedDocs = documents.sortedBy { it.key }
258+
259+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
260+
assertThat(result).containsExactlyElementsIn(expectedDocs).inOrder()
261+
}
262+
263+
@Test
264+
fun `where with nested add function max depth`(): Unit = runBlocking {
265+
val numOfFields = 1
266+
val numOfDocuments = 10
267+
val depth = 31
268+
// All docs have field_1 = 0L
269+
val documents = seedDatabase(numOfDocuments, numOfFields) { _, _ -> 0L }
270+
271+
var addExpr: Expr = field("field_1")
272+
for (i in 1..depth) {
273+
addExpr = add(addExpr, constant(1L))
274+
}
275+
// addExpr is field_1 + 1 (depth times) = field_1 + depth = 0 + 31 = 31
276+
277+
val pipeline =
278+
RealtimePipelineSource(db).collection(collectionId).where(addExpr.gt(0L)) // 31 > 0L is true
279+
280+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
281+
assertThat(result).containsExactlyElementsIn(documents)
282+
}
283+
284+
@Test
285+
fun `where with large number ors`(): Unit = runBlocking {
286+
val numOfFields = 100
287+
val numOfDocuments = 50
288+
// valueCounter removed as it was unused here. Seed values are generated based on docIdx and
289+
// fieldIdx.
290+
// field_1 = 1, field_2 = 2, ..., field_100 = 100 (for first doc)
291+
// field_1 = 101, field_2 = 102, ..., field_100 = 200 (for second doc)
292+
// ...
293+
// Max value assigned will be for the last field of the last document:
294+
// (49 * 100) + 100 = 4900 + 100 = 5000
295+
val documents =
296+
seedDatabase(numOfDocuments, numOfFields) { docIdx, fieldIdx ->
297+
(docIdx * numOfFields) + fieldIdx
298+
}
299+
val maxValueInDb = (numOfDocuments - 1) * numOfFields + numOfFields // 5000L
300+
301+
val orConditions = (1..numOfFields).map { i -> field("field_$i").lte(maxValueInDb) }
302+
303+
val pipeline =
304+
RealtimePipelineSource(db)
305+
.collection(collectionId)
306+
.where(or(orConditions.first(), *orConditions.drop(1).toTypedArray()))
307+
308+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
309+
// Every document will have at least one field_i <= maxValueInDb (actually all fields are)
310+
assertThat(result).containsExactlyElementsIn(documents)
311+
}
312+
313+
@Test
314+
fun `where with large number of conjunctions`(): Unit = runBlocking {
315+
val numOfFields = 50
316+
val numOfDocuments = 100
317+
// Values from 1 up to 100 * 50 = 5000
318+
val documents =
319+
seedDatabase(numOfDocuments, numOfFields) { docIdx, fieldIdx ->
320+
(docIdx * numOfFields) + fieldIdx
321+
}
322+
323+
val andConditions1 =
324+
(1..numOfFields).map { i -> field("field_$i").gt(0L) } // Use 0L for clarity with Long types
325+
val andConditions2 = (1..numOfFields).map { i -> field("field_$i").lt(Long.MAX_VALUE) }
326+
327+
val pipeline =
328+
RealtimePipelineSource(db)
329+
.collection(collectionId)
330+
.where(
331+
or(
332+
and(andConditions1.first(), *andConditions1.drop(1).toTypedArray()),
333+
and(andConditions2.first(), *andConditions2.drop(1).toTypedArray())
334+
)
335+
)
336+
val result = runPipeline(db, pipeline, flowOf(*documents.toTypedArray())).toList()
337+
// All seeded values are > 0 and < Long.MAX_VALUE, so all documents match.
338+
assertThat(result).containsExactlyElementsIn(documents)
339+
}
340+
}

0 commit comments

Comments
 (0)