diff --git a/.changeset/odd-gorillas-cough.md b/.changeset/odd-gorillas-cough.md new file mode 100644 index 00000000000..b89c5de1a38 --- /dev/null +++ b/.changeset/odd-gorillas-cough.md @@ -0,0 +1,6 @@ +--- +'@firebase/firestore': minor +'firebase': minor +--- + +Enable queries with range & inequality filters on multiple fields. diff --git a/packages/firestore/firestore_composite_index_config.tf b/packages/firestore/firestore_composite_index_config.tf index 217dbb70294..495c6ef1467 100644 --- a/packages/firestore/firestore_composite_index_config.tf +++ b/packages/firestore/firestore_composite_index_config.tf @@ -115,6 +115,36 @@ locals { }, ] index9 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "a" + order = "ASCENDING" + }, + + { + field_path = "b" + order = "ASCENDING" + }, + ] + index10 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "b" + order = "DESCENDING" + }, + + { + field_path = "a" + order = "DESCENDING" + }, + ] + index11 = [ { field_path = "testId" order = "ASCENDING" @@ -128,7 +158,7 @@ locals { order = "ASCENDING" }, ] - index10 = [ + index12 = [ { field_path = "testId" order = "ASCENDING" @@ -146,7 +176,7 @@ locals { order = "ASCENDING" }, ] - index11 = [ + index13 = [ { field_path = "rating" array_config = "CONTAINS" @@ -164,5 +194,218 @@ locals { order = "ASCENDING" }, ] + index14 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "sort" + order = "ASCENDING" + } + ] + index15 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "sort" + order = "ASCENDING" + }, + { + field_path = "v" + order = "ASCENDING" + } + ] + index16 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "v" + order = "DESCENDING" + }, + { + field_path = "key" + order = "DESCENDING" + }, + { + field_path = "sort" + order = "DESCENDING" + }, + ] + index17 = [ + { + field_path = "v" + array_config = "CONTAINS" + }, + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "sort" + order = "ASCENDING" + }, + ] + index18 = [ + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "testId" + order = "ASCENDING" + }, + + { + field_path = "sort" + order = "DESCENDING" + }, + { + field_path = "v" + order = "ASCENDING" + }, + ] + index19 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + + { + field_path = "sort" + order = "DESCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "v" + order = "ASCENDING" + }, + ] + index20 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "v" + order = "ASCENDING" + }, + + { + field_path = "sort" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + + ] + index21 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "sort" + order = "DESCENDING" + }, + { + field_path = "key" + order = "DESCENDING" + }, + + ] + index22 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "v" + order = "DESCENDING" + }, + { + field_path = "sort" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + ] + index23 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "name" + order = "ASCENDING" + }, + { + field_path = "metadata.createdAt" + order = "ASCENDING" + }, + ] + index24 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "name" + order = "DESCENDING" + }, + { + field_path = "field" + order = "DESCENDING" + }, + { + field_path = "`field.dot`" + order = "DESCENDING" + }, + { + field_path = "`field\\\\slash`" + order = "DESCENDING" + }, + ], + index25 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "v" + order = "ASCENDING" + }, + { + field_path = "key" + order = "ASCENDING" + }, + { + field_path = "sort" + order = "ASCENDING" + }, + ] } } diff --git a/packages/firestore/test/integration/api/composite_index_query.test.ts b/packages/firestore/test/integration/api/composite_index_query.test.ts index da70ac1ece7..04cdafe7169 100644 --- a/packages/firestore/test/integration/api/composite_index_query.test.ts +++ b/packages/firestore/test/integration/api/composite_index_query.test.ts @@ -30,7 +30,12 @@ import { count, doc, writeBatch, - collectionGroup + collectionGroup, + FieldPath, + and, + getCountFromServer, + documentId, + disableNetwork } from '../util/firebase_export'; import { apiDescribe } from '../util/helpers'; @@ -302,4 +307,649 @@ apiDescribe('Composite Index Queries', persistence => { }); }); }); + + describe('Multiple Inequality', () => { + it('can use multiple inequality filters', async () => { + const testDocs = { + doc1: { key: 'a', sort: 0, v: 0 }, + doc2: { key: 'b', sort: 3, v: 1 }, + doc3: { key: 'c', sort: 1, v: 3 }, + doc4: { key: 'd', sort: 2, v: 2 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + // Multiple inequality fields + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '<=', 2), + where('v', '>', 2) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc3']); + + // Duplicate inequality fields + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '<=', 2), + where('sort', '>', 1) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4']); + + // With multiple IN + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>=', 'a'), + where('sort', '<=', 2), + where('v', 'in', [2, 3, 4]), + where('sort', 'in', [2, 3]) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4']); + + // With NOT-IN + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>=', 'a'), + where('sort', '<=', 2), + where('v', 'not-in', [2, 4, 5]) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc1', 'doc3']); + + // With orderby + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>=', 'a'), + where('sort', '<=', 2), + orderBy('v', 'desc') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc3', + 'doc4', + 'doc1' + ]); + + // With limit + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>=', 'a'), + where('sort', '<=', 2), + orderBy('v', 'desc'), + limit(2) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc3', 'doc4']); + + // With limitToLast + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>=', 'a'), + where('sort', '<=', 2), + orderBy('v', 'desc'), + limitToLast(2) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4', 'doc1']); + }); + }); + + it('can use on special values', async () => { + const testDocs = { + doc1: { key: 'a', sort: 0, v: 0 }, + doc2: { key: 'b', sort: NaN, v: 1 }, + doc3: { key: 'c', sort: null, v: 3 }, + doc4: { key: 'd', v: 0 }, + doc5: { key: 'e', sort: 1 }, + doc6: { key: 'f', sort: 1, v: 1 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '<=', 2) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc5', 'doc6']); + + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '<=', 2), + where('v', '<=', 1) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc6']); + }); + }); + + it('can use with array membership', async () => { + const testDocs = { + doc1: { key: 'a', sort: 0, v: [0] }, + doc2: { key: 'b', sort: 1, v: [0, 1, 3] }, + doc3: { key: 'c', sort: 1, v: [] }, + doc4: { key: 'd', sort: 2, v: [1] }, + doc5: { key: 'e', sort: 3, v: [2, 4] }, + doc6: { key: 'f', sort: 4, v: [NaN] }, + doc7: { key: 'g', sort: 4, v: [null] } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '>=', 1), + where('v', 'array-contains', 0) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2']); + + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '>=', 1), + where('v', 'array-contains-any', [0, 1]) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2', 'doc4']); + }); + }); + + it('can use with nested field', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testData = (n?: number): any => { + n = n || 1; + return { + name: 'room ' + n, + metadata: { + createdAt: n + }, + field: 'field ' + n, + 'field.dot': n, + 'field\\slash': n + }; + }; + + const testDocs = { + 'doc1': testData(400), + 'doc2': testData(200), + 'doc3': testData(100), + 'doc4': testData(300) + }; + + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('metadata.createdAt', '<=', 500), + where('metadata.createdAt', '>', 100), + where('name', '!=', 'room 200'), + orderBy('name') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4', 'doc1']); + + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('field', '>=', 'field 100'), + where(new FieldPath('field.dot'), '!=', 300), + where('field\\slash', '<', 400), + orderBy('name', 'desc') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2', 'doc3']); + }); + }); + + it('can use with nested composite filters', async () => { + const testDocs = { + doc1: { key: 'a', sort: 0, v: 5 }, + doc2: { key: 'aa', sort: 4, v: 4 }, + doc3: { key: 'c', sort: 3, v: 3 }, + doc4: { key: 'b', sort: 2, v: 2 }, + doc5: { key: 'b', sort: 2, v: 1 }, + doc6: { key: 'b', sort: 0, v: 0 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + let snapshot = await testHelper.getDocs( + testHelper.compositeQuery( + coll, + or( + and(where('key', '==', 'b'), where('sort', '<=', 2)), + and(where('key', '!=', 'b'), where('v', '>', 4)) + ) + ) + ); + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc1', + 'doc6', + 'doc5', + 'doc4' + ]); + + snapshot = await testHelper.getDocs( + testHelper.compositeQuery( + coll, + or( + and(where('key', '==', 'b'), where('sort', '<=', 2)), + and(where('key', '!=', 'b'), where('v', '>', 4)) + ), + orderBy('sort', 'desc'), + orderBy('key') + ) + ); + // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc5', + 'doc4', + 'doc1', + 'doc6' + ]); + + snapshot = await testHelper.getDocs( + testHelper.compositeQuery( + coll, + and( + or( + and(where('key', '==', 'b'), where('sort', '<=', 4)), + and(where('key', '!=', 'b'), where('v', '>=', 4)) + ), + or( + and(where('key', '>', 'b'), where('sort', '>=', 1)), + and(where('key', '<', 'b'), where('v', '>', 0)) + ) + ) + ) + ); + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc1', 'doc2']); + }); + }); + + it('inequality fields will be implicitly ordered lexicographically', async () => { + const testDocs = { + doc1: { key: 'a', sort: 0, v: 5 }, + doc2: { key: 'aa', sort: 4, v: 4 }, + doc3: { key: 'b', sort: 3, v: 3 }, + doc4: { key: 'b', sort: 2, v: 2 }, + doc5: { key: 'b', sort: 2, v: 1 }, + doc6: { key: 'b', sort: 0, v: 0 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '>', 1), + where('v', 'in', [1, 2, 3, 4]) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc4', + 'doc5', + 'doc3' + ]); + + // Implicitly ordered by: 'key' asc, 'sort' asc,__name__ asc + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('sort', '>', 1), + where('key', '!=', 'a'), + where('v', 'in', [1, 2, 3, 4]) + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc4', + 'doc5', + 'doc3' + ]); + }); + }); + + it('can use multiple explicit order by field', async () => { + const testDocs = { + doc1: { key: 'a', sort: 5, v: 0 }, + doc2: { key: 'aa', sort: 4, v: 0 }, + doc3: { key: 'b', sort: 3, v: 1 }, + doc4: { key: 'b', sort: 2, v: 1 }, + doc5: { key: 'bb', sort: 1, v: 1 }, + doc6: { key: 'c', sort: 0, v: 2 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + orderBy('v') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc4', + 'doc3', + 'doc5' + ]); + + // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + orderBy('v'), + orderBy('sort') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc5', + 'doc4', + 'doc3' + ]); + + // Implicit order by matches the direction of last explicit order by. + // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + orderBy('v', 'desc') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc5', + 'doc3', + 'doc4', + 'doc2' + ]); + + // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + orderBy('v', 'desc'), + orderBy('sort') + ) + ); + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc5', + 'doc4', + 'doc3', + 'doc2' + ]); + }); + }); + + it('can use in aggregate query', async () => { + const testDocs = { + doc1: { key: 'a', sort: 5, v: 0 }, + doc2: { key: 'aa', sort: 4, v: 0 }, + doc3: { key: 'b', sort: 3, v: 1 }, + doc4: { key: 'b', sort: 2, v: 1 }, + doc5: { key: 'bb', sort: 1, v: 1 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot1 = await getCountFromServer( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + orderBy('v') + ) + ); + + expect(snapshot1.data().count).to.equal(4); + + const snapshot2 = await getAggregateFromServer( + testHelper.query( + coll, + where('key', '>', 'a'), + where('sort', '>=', 1), + where('v', '!=', 0) + ), + { + count: count(), + sum: sum('sort'), + avg: average('v') + } + ); + expect(snapshot2.data().count).to.equal(3); + expect(snapshot2.data().sum).to.equal(6); + expect(snapshot2.data().avg).to.equal(1); + }); + }); + + it('can use document ID im multiple inequality query', () => { + const testDocs = { + doc1: { key: 'a', sort: 5 }, + doc2: { key: 'aa', sort: 4 }, + doc3: { key: 'b', sort: 3 }, + doc4: { key: 'b', sort: 2 }, + doc5: { key: 'bb', sort: 1 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + let snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where('sort', '>=', 1), + where('key', '!=', 'a'), + where(documentId(), '<', 'doc5') + ) + ); + // Document Key in inequality field will implicitly ordered to the last. + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc4', + 'doc3' + ]); + + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where(documentId(), '<', 'doc5'), + where('sort', '>=', 1), + where('key', '!=', 'a') + ) + ); + // Changing filters order will not effect implicit order. + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc4', + 'doc3' + ]); + + snapshot = await testHelper.getDocs( + testHelper.query( + coll, + where(documentId(), '<', 'doc5'), + where('sort', '>=', 1), + where('key', '!=', 'a'), + orderBy('sort', 'desc') + ) + ); + // Ordered by: 'sort' desc,'key' desc, __name__ desc + testHelper.assertSnapshotResultIdsMatch(snapshot, [ + 'doc2', + 'doc3', + 'doc4' + ]); + }); + }); + + it('can get documents while offline', () => { + const testDocs = { + doc1: { key: 'a', sort: 1 }, + doc2: { key: 'aa', sort: 4 }, + doc3: { key: 'b', sort: 3 }, + doc4: { key: 'b', sort: 2 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs( + persistence.toLruGc(), + testDocs, + async (coll, db) => { + const query_ = testHelper.query( + coll, + where('key', '!=', 'a'), + where('sort', '<=', 3) + ); + //populate the cache. + const snapshot1 = await testHelper.getDocs(query_); + expect(snapshot1.size).to.equal(2); + + await disableNetwork(db); + + const snapshot2 = await testHelper.getDocs(query_); + expect(snapshot2.metadata.fromCache).to.be.true; + expect(snapshot2.metadata.hasPendingWrites).to.be.false; + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + testHelper.assertSnapshotResultIdsMatch(snapshot2, ['doc4', 'doc3']); + } + ); + }); + + // eslint-disable-next-line no-restricted-properties + (persistence.gc === 'lru' ? it : it.skip)( + 'can get same result from server and cache', + () => { + const testDocs = { + doc1: { a: 1, b: 0 }, + doc2: { a: 2, b: 1 }, + doc3: { a: 3, b: 2 }, + doc4: { a: 1, b: 3 }, + doc5: { a: 1, b: 1 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + // implicit AND: a != 1 && b < 2 + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.query(coll, where('a', '!=', 1), where('b', '<', 2)), + 'doc2' + ); + + // explicit AND: a != 1 && b < 2 + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.compositeQuery( + coll, + and(where('a', '!=', 1), where('b', '<', 2)) + ), + 'doc2' + ); + + // explicit AND: a < 3 && b not-in [2, 3] + // Implicitly ordered by: a asc, b asc, __name__ asc + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.compositeQuery( + coll, + and(where('a', '<', 3), where('b', 'not-in', [2, 3])) + ), + 'doc1', + 'doc5', + 'doc2' + ); + + // a <3 && b != 0, implicitly ordered by: a asc, b asc, __name__ asc + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.query( + coll, + where('b', '!=', 0), + where('a', '<', 3), + limit(2) + ), + 'doc5', + 'doc4' + ); + + // a <3 && b != 0, ordered by: b desc, a desc, __name__ desc + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.query( + coll, + where('a', '<', 3), + where('b', '!=', 0), + orderBy('b', 'desc'), + limit(2) + ), + 'doc4', + 'doc2' + ); + + // explicit OR: multiple inequality: a>2 || b<1. + await testHelper.assertOnlineAndOfflineResultsMatch( + testHelper.compositeQuery( + coll, + or(where('a', '>', 2), where('b', '<', 1)) + ), + 'doc1', + 'doc3' + ); + }); + } + ); + + it('inequality query will reject if document key is not the last orderBy field', () => { + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestCollection(persistence, async coll => { + // Implicitly ordered by: __name__ asc, 'key' asc, + const queryForRejection = testHelper.query( + coll, + where('key', '!=', 42), + orderBy(documentId()) + ); + + await expect( + testHelper.getDocs(queryForRejection) + ).to.be.eventually.rejectedWith( + /order by clause cannot contain more fields after the key/i + ); + }); + }); + + it('inequality query will reject if document key appears only in equality filter', () => { + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestCollection(persistence, async coll => { + const query_ = testHelper.query( + coll, + where('key', '!=', 42), + where(documentId(), '==', 'doc1') + ); + await expect(testHelper.getDocs(query_)).to.be.eventually.rejectedWith( + 'Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.' + ); + }); + }); + }); }); diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 7468b39bfeb..7bb8d70c193 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -23,11 +23,9 @@ import { EventsAccumulator } from '../util/events_accumulator'; import { addDoc, and, - average, Bytes, collection, collectionGroup, - count, deleteDoc, disableNetwork, doc, @@ -38,10 +36,7 @@ import { enableNetwork, endAt, endBefore, - FieldPath, GeoPoint, - getAggregateFromServer, - getCountFromServer, getDocs, limit, limitToLast, @@ -53,7 +48,6 @@ import { setDoc, startAfter, startAt, - sum, Timestamp, updateDoc, where, @@ -67,7 +61,6 @@ import { RetryError, toChangesArray, toDataArray, - toIds, PERSISTENCE_MODE_UNSPECIFIED, withEmptyTestCollection, withRetry, @@ -1347,604 +1340,6 @@ apiDescribe('Queries', persistence => { }); }); - // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? describe : describe.skip)('Multiple Inequality', () => { - it('can use multiple inequality filters', async () => { - const testDocs = { - doc1: { key: 'a', sort: 0, v: 0 }, - doc2: { key: 'b', sort: 3, v: 1 }, - doc3: { key: 'c', sort: 1, v: 3 }, - doc4: { key: 'd', sort: 2, v: 2 } - }; - return withTestCollection(persistence, testDocs, async coll => { - // Multiple inequality fields - const snapshot1 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '<=', 2), - where('v', '>', 2) - ) - ); - expect(toIds(snapshot1)).to.deep.equal(['doc3']); - - // Duplicate inequality fields - const snapshot2 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '<=', 2), - where('sort', '>', 1) - ) - ); - expect(toIds(snapshot2)).to.deep.equal(['doc4']); - - // With multiple IN - const snapshot3 = await getDocs( - query( - coll, - where('key', '>=', 'a'), - where('sort', '<=', 2), - where('v', 'in', [2, 3, 4]), - where('sort', 'in', [2, 3]) - ) - ); - expect(toIds(snapshot3)).to.deep.equal(['doc4']); - - // With NOT-IN - const snapshot4 = await getDocs( - query( - coll, - where('key', '>=', 'a'), - where('sort', '<=', 2), - where('v', 'not-in', [2, 4, 5]) - ) - ); - expect(toIds(snapshot4)).to.deep.equal(['doc1', 'doc3']); - - // With orderby - const snapshot5 = await getDocs( - query( - coll, - where('key', '>=', 'a'), - where('sort', '<=', 2), - orderBy('v', 'desc') - ) - ); - expect(toIds(snapshot5)).to.deep.equal(['doc3', 'doc4', 'doc1']); - - // With limit - const snapshot6 = await getDocs( - query( - coll, - where('key', '>=', 'a'), - where('sort', '<=', 2), - orderBy('v', 'desc'), - limit(2) - ) - ); - expect(toIds(snapshot6)).to.deep.equal(['doc3', 'doc4']); - - // With limitToLast - const snapshot7 = await getDocs( - query( - coll, - where('key', '>=', 'a'), - where('sort', '<=', 2), - orderBy('v', 'desc'), - limitToLast(2) - ) - ); - expect(toIds(snapshot7)).to.deep.equal(['doc4', 'doc1']); - }); - }); - - it('can use on special values', async () => { - const testDocs = { - doc1: { key: 'a', sort: 0, v: 0 }, - doc2: { key: 'b', sort: NaN, v: 1 }, - doc3: { key: 'c', sort: null, v: 3 }, - doc4: { key: 'd', v: 0 }, - doc5: { key: 'e', sort: 1 }, - doc6: { key: 'f', sort: 1, v: 1 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getDocs( - query(coll, where('key', '!=', 'a'), where('sort', '<=', 2)) - ); - expect(toIds(snapshot1)).to.deep.equal(['doc5', 'doc6']); - - const snapshot2 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '<=', 2), - where('v', '<=', 1) - ) - ); - expect(toIds(snapshot2)).to.deep.equal(['doc6']); - }); - }); - it('can use with array membership', async () => { - const testDocs = { - doc1: { key: 'a', sort: 0, v: [0] }, - doc2: { key: 'b', sort: 1, v: [0, 1, 3] }, - doc3: { key: 'c', sort: 1, v: [] }, - doc4: { key: 'd', sort: 2, v: [1] }, - doc5: { key: 'e', sort: 3, v: [2, 4] }, - doc6: { key: 'f', sort: 4, v: [NaN] }, - doc7: { key: 'g', sort: 4, v: [null] } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '>=', 1), - where('v', 'array-contains', 0) - ) - ); - expect(toIds(snapshot1)).to.deep.equal(['doc2']); - - const snapshot2 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '>=', 1), - where('v', 'array-contains-any', [0, 1]) - ) - ); - expect(toIds(snapshot2)).to.deep.equal(['doc2', 'doc4']); - }); - }); - - it('can use with nested field', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const testData = (n?: number): any => { - n = n || 1; - return { - name: 'room ' + n, - metadata: { - createdAt: n - }, - field: 'field ' + n, - 'field.dot': n, - 'field\\slash': n - }; - }; - - const testDocs = { - 'doc1': testData(400), - 'doc2': testData(200), - 'doc3': testData(100), - 'doc4': testData(300) - }; - - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getDocs( - query( - coll, - where('metadata.createdAt', '<=', 500), - where('metadata.createdAt', '>', 100), - where('name', '!=', 'room 200'), - orderBy('name') - ) - ); - expect(toIds(snapshot1)).to.deep.equal(['doc4', 'doc1']); - - const snapshot2 = await getDocs( - query( - coll, - where('field', '>=', 'field 100'), - where(new FieldPath('field.dot'), '!=', 300), - where('field\\slash', '<', 400), - orderBy('name', 'desc') - ) - ); - expect(toIds(snapshot2)).to.deep.equal(['doc2', 'doc3']); - }); - }); - - it('can use with nested composite filters', async () => { - const testDocs = { - doc1: { key: 'a', sort: 0, v: 5 }, - doc2: { key: 'aa', sort: 4, v: 4 }, - doc3: { key: 'c', sort: 3, v: 3 }, - doc4: { key: 'b', sort: 2, v: 2 }, - doc5: { key: 'b', sort: 2, v: 1 }, - doc6: { key: 'b', sort: 0, v: 0 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getDocs( - query( - coll, - or( - and(where('key', '==', 'b'), where('sort', '<=', 2)), - and(where('key', '!=', 'b'), where('v', '>', 4)) - ) - ) - ); - // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expect(toIds(snapshot1)).to.deep.equal([ - 'doc1', - 'doc6', - 'doc5', - 'doc4' - ]); - - const snapshot2 = await getDocs( - query( - coll, - or( - and(where('key', '==', 'b'), where('sort', '<=', 2)), - and(where('key', '!=', 'b'), where('v', '>', 4)) - ), - orderBy('sort', 'desc'), - orderBy('key') - ) - ); - // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc - expect(toIds(snapshot2)).to.deep.equal([ - 'doc5', - 'doc4', - 'doc1', - 'doc6' - ]); - - const snapshot3 = await getDocs( - query( - coll, - and( - or( - and(where('key', '==', 'b'), where('sort', '<=', 4)), - and(where('key', '!=', 'b'), where('v', '>=', 4)) - ), - or( - and(where('key', '>', 'b'), where('sort', '>=', 1)), - and(where('key', '<', 'b'), where('v', '>', 0)) - ) - ) - ) - ); - // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expect(toIds(snapshot3)).to.deep.equal(['doc1', 'doc2']); - }); - }); - - it('inequality fields will be implicitly ordered lexicographically', async () => { - const testDocs = { - doc1: { key: 'a', sort: 0, v: 5 }, - doc2: { key: 'aa', sort: 4, v: 4 }, - doc3: { key: 'b', sort: 3, v: 3 }, - doc4: { key: 'b', sort: 2, v: 2 }, - doc5: { key: 'b', sort: 2, v: 1 }, - doc6: { key: 'b', sort: 0, v: 0 } - }; - return withTestCollection(persistence, testDocs, async coll => { - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - const snapshot1 = await getDocs( - query( - coll, - where('key', '!=', 'a'), - where('sort', '>', 1), - where('v', 'in', [1, 2, 3, 4]) - ) - ); - expect(toIds(snapshot1)).to.deep.equal([ - 'doc2', - 'doc4', - 'doc5', - 'doc3' - ]); - - // Implicitly ordered by: 'key' asc, 'sort' asc,__name__ asc - const snapshot2 = await getDocs( - query( - coll, - where('sort', '>', 1), - where('key', '!=', 'a'), - where('v', 'in', [1, 2, 3, 4]) - ) - ); - expect(toIds(snapshot2)).to.deep.equal([ - 'doc2', - 'doc4', - 'doc5', - 'doc3' - ]); - }); - }); - - it('can use multiple explicit order by field', async () => { - const testDocs = { - doc1: { key: 'a', sort: 5, v: 0 }, - doc2: { key: 'aa', sort: 4, v: 0 }, - doc3: { key: 'b', sort: 3, v: 1 }, - doc4: { key: 'b', sort: 2, v: 1 }, - doc5: { key: 'bb', sort: 1, v: 1 }, - doc6: { key: 'c', sort: 0, v: 2 } - }; - - return withTestCollection(persistence, testDocs, async coll => { - // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc - const snapshot1 = await getDocs( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - orderBy('v') - ) - ); - expect(toIds(snapshot1)).to.deep.equal([ - 'doc2', - 'doc4', - 'doc3', - 'doc5' - ]); - - // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc - const snapshot2 = await getDocs( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - orderBy('v'), - orderBy('sort') - ) - ); - expect(toIds(snapshot2)).to.deep.equal([ - 'doc2', - 'doc5', - 'doc4', - 'doc3' - ]); - - // Implicit order by matches the direction of last explicit order by. - // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc - const snapshot3 = await getDocs( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - orderBy('v', 'desc') - ) - ); - expect(toIds(snapshot3)).to.deep.equal([ - 'doc5', - 'doc3', - 'doc4', - 'doc2' - ]); - - // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc - const snapshot4 = await getDocs( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - orderBy('v', 'desc'), - orderBy('sort') - ) - ); - expect(toIds(snapshot4)).to.deep.equal([ - 'doc5', - 'doc4', - 'doc3', - 'doc2' - ]); - }); - }); - - it('can use in aggregate query', async () => { - const testDocs = { - doc1: { key: 'a', sort: 5, v: 0 }, - doc2: { key: 'aa', sort: 4, v: 0 }, - doc3: { key: 'b', sort: 3, v: 1 }, - doc4: { key: 'b', sort: 2, v: 1 }, - doc5: { key: 'bb', sort: 1, v: 1 } - }; - - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getCountFromServer( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - orderBy('v') - ) - ); - expect(snapshot1.data().count).to.equal(4); - - const snapshot2 = await getAggregateFromServer( - query( - coll, - where('key', '>', 'a'), - where('sort', '>=', 1), - where('v', '!=', 0) - ), - { - count: count(), - sum: sum('sort'), - avg: average('v') - } - ); - expect(snapshot2.data().count).to.equal(3); - expect(snapshot2.data().sum).to.equal(6); - expect(snapshot2.data().avg).to.equal(1); - }); - }); - - it('can use document ID im multiple inequality query', () => { - const testDocs = { - doc1: { key: 'a', sort: 5 }, - doc2: { key: 'aa', sort: 4 }, - doc3: { key: 'b', sort: 3 }, - doc4: { key: 'b', sort: 2 }, - doc5: { key: 'bb', sort: 1 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot1 = await getDocs( - query( - coll, - where('sort', '>=', 1), - where('key', '!=', 'a'), - where(documentId(), '<', 'doc5') - ) - ); - // Document Key in inequality field will implicitly ordered to the last. - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expect(toIds(snapshot1)).to.deep.equal(['doc2', 'doc4', 'doc3']); - - const snapshot2 = await getDocs( - query( - coll, - where(documentId(), '<', 'doc5'), - where('sort', '>=', 1), - where('key', '!=', 'a') - ) - ); - // Changing filters order will not effect implicit order. - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expect(toIds(snapshot2)).to.deep.equal(['doc2', 'doc4', 'doc3']); - - const snapshot3 = await getDocs( - query( - coll, - where(documentId(), '<', 'doc5'), - where('sort', '>=', 1), - where('key', '!=', 'a'), - orderBy('sort', 'desc') - ) - ); - // Ordered by: 'sort' desc,'key' desc, __name__ desc - expect(toIds(snapshot3)).to.deep.equal(['doc2', 'doc3', 'doc4']); - }); - }); - - it('can get documents while offline', () => { - const testDocs = { - doc1: { key: 'a', sort: 1 }, - doc2: { key: 'aa', sort: 4 }, - doc3: { key: 'b', sort: 3 }, - doc4: { key: 'b', sort: 2 } - }; - return withTestCollection( - persistence.toLruGc(), - testDocs, - async (coll, db) => { - const query_ = query( - coll, - where('key', '!=', 'a'), - where('sort', '<=', 3) - ); - //populate the cache. - const snapshot1 = await getDocs(query_); - expect(snapshot1.size).to.equal(2); - - await disableNetwork(db); - - const snapshot2 = await getDocs(query_); - expect(snapshot2.metadata.fromCache).to.be.true; - expect(snapshot2.metadata.hasPendingWrites).to.be.false; - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expect(toIds(snapshot2)).to.deep.equal(['doc4', 'doc3']); - } - ); - }); - - // eslint-disable-next-line no-restricted-properties - (persistence.gc === 'lru' ? it : it.skip)( - 'can get same result from server and cache', - () => { - const testDocs = { - doc1: { a: 1, b: 0 }, - doc2: { a: 2, b: 1 }, - doc3: { a: 3, b: 2 }, - doc4: { a: 1, b: 3 }, - doc5: { a: 1, b: 1 } - }; - - return withTestCollection(persistence, testDocs, async coll => { - // implicit AND: a != 1 && b < 2 - await checkOnlineAndOfflineResultsMatch( - query(coll, where('a', '!=', 1), where('b', '<', 2)), - 'doc2' - ); - - // explicit AND: a != 1 && b < 2 - await checkOnlineAndOfflineResultsMatch( - query(coll, and(where('a', '!=', 1), where('b', '<', 2))), - 'doc2' - ); - - // explicit AND: a < 3 && b not-in [2, 3] - // Implicitly ordered by: a asc, b asc, __name__ asc - await checkOnlineAndOfflineResultsMatch( - query(coll, and(where('a', '<', 3), where('b', 'not-in', [2, 3]))), - 'doc1', - 'doc5', - 'doc2' - ); - - // a <3 && b != 0, implicitly ordered by: a asc, b asc, __name__ asc - await checkOnlineAndOfflineResultsMatch( - query(coll, where('b', '!=', 0), where('a', '<', 3), limit(2)), - 'doc5', - 'doc4' - ); - - // a <3 && b != 0, ordered by: b desc, a desc, __name__ desc - await checkOnlineAndOfflineResultsMatch( - query( - coll, - where('a', '<', 3), - where('b', '!=', 0), - orderBy('b', 'desc'), - limit(2) - ), - 'doc4', - 'doc2' - ); - - // explicit OR: multiple inequality: a>2 || b<1. - await checkOnlineAndOfflineResultsMatch( - query(coll, or(where('a', '>', 2), where('b', '<', 1))), - 'doc1', - 'doc3' - ); - }); - } - ); - - it('inequality query will reject if document key is not the last orderBy field', () => { - return withEmptyTestCollection(persistence, async coll => { - // Implicitly ordered by: __name__ asc, 'key' asc, - const queryForRejection = query( - coll, - where('key', '!=', 42), - orderBy(documentId()) - ); - - await expect(getDocs(queryForRejection)).to.be.eventually.rejectedWith( - 'order by clause cannot contain more fields after the key' - ); - }); - }); - - it('inequality query will reject if document key appears only in equality filter', () => { - return withEmptyTestCollection(persistence, async coll => { - const query_ = query( - coll, - where('key', '!=', 42), - where(documentId(), '==', 'doc1') - ); - await expect(getDocs(query_)).to.be.eventually.rejectedWith( - 'Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.' - ); - }); - }); - }); - // OR Query tests only run when the SDK's local cache is configured to use // LRU garbage collection (rather than eager garbage collection) because // they validate that the result from server and cache match. diff --git a/packages/firestore/test/integration/util/composite_index_test_helper.ts b/packages/firestore/test/integration/util/composite_index_test_helper.ts index b04bb483558..5199539768b 100644 --- a/packages/firestore/test/integration/util/composite_index_test_helper.ts +++ b/packages/firestore/test/integration/util/composite_index_test_helper.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { expect } from 'chai'; import { query as internalQuery, @@ -49,7 +50,8 @@ import { batchCommitDocsToCollection, checkOnlineAndOfflineResultsMatch, PERSISTENCE_MODE_UNSPECIFIED, - PersistenceMode + PersistenceMode, + toIds } from './helpers'; import { COMPOSITE_INDEX_TEST_COLLECTION, @@ -169,6 +171,15 @@ export class CompositeIndexTestHelper { ); } + // Asserts that the IDs in the query snapshot matches the expected Ids. The expected document + // IDs are hashed to match the actual document IDs created by the test helper. + assertSnapshotResultIdsMatch( + snapshot: QuerySnapshot, + expectedIds: string[] + ): void { + expect(toIds(snapshot)).to.deep.equal(this.toHashedIds(expectedIds)); + } + // Adds a filter on test id for a query. query(query_: Query, ...queryConstraints: QueryConstraint[]): Query { return internalQuery(