diff --git a/packages/firestore/src/lite-api/query.ts b/packages/firestore/src/lite-api/query.ts index 0b3de2268d0..e6c2385efe0 100644 --- a/packages/firestore/src/lite-api/query.ts +++ b/packages/firestore/src/lite-api/query.ts @@ -1076,20 +1076,14 @@ function conflictingOps(op: Operator): Operator[] { case Operator.NOT_EQUAL: return [Operator.NOT_EQUAL, Operator.NOT_IN]; case Operator.ARRAY_CONTAINS: - return [ - Operator.ARRAY_CONTAINS, - Operator.ARRAY_CONTAINS_ANY, - Operator.NOT_IN - ]; - case Operator.IN: - return [Operator.ARRAY_CONTAINS_ANY, Operator.IN, Operator.NOT_IN]; case Operator.ARRAY_CONTAINS_ANY: return [ Operator.ARRAY_CONTAINS, Operator.ARRAY_CONTAINS_ANY, - Operator.IN, Operator.NOT_IN ]; + case Operator.IN: + return [Operator.NOT_IN]; case Operator.NOT_IN: return [ Operator.ARRAY_CONTAINS, @@ -1218,6 +1212,7 @@ export function validateQueryFilterConstraint( ) { throw new FirestoreError( Code.INVALID_ARGUMENT, + `Function ${functionName}() requires AppliableConstraints created with a call to 'where(...)', 'or(...)', or 'and(...)'.` ); } diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index f76baf19b9f..c7337285899 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -1574,6 +1574,246 @@ apiDescribe('Queries', (persistence: boolean) => { ); }); }); + + // TODO(orquery): Enable this test when prod supports OR queries. + // eslint-disable-next-line no-restricted-properties + it('supports multiple in ops', () => { + const testDocs = { + doc1: { a: 1, b: 0 }, + doc2: { b: 1 }, + doc3: { a: 3, b: 2 }, + doc4: { a: 1, b: 3 }, + doc5: { a: 1 }, + doc6: { a: 2 } + }; + + return withTestCollection(persistence, testDocs, async coll => { + // Two IN operations on different fields with disjunction. + await checkOnlineAndOfflineResultsMatch( + query( + coll, + or(where('a', 'in', [2, 3]), where('b', 'in', [0, 2])), + orderBy('a') + ), + 'doc1', + 'doc6', + 'doc3' + ); + + // Two IN operations on different fields with conjunction. + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and(where('a', 'in', [2, 3]), where('b', 'in', [0, 2])), + orderBy('a') + ), + 'doc3' + ); + + // Two IN operations on the same field. + // a IN [1,2,3] && a IN [0,1,4] should result in "a==1". + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and(where('a', 'in', [1, 2, 3]), where('a', 'in', [0, 1, 4])) + ), + 'doc1', + 'doc4', + 'doc5' + ); + + // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an + // empty set. + await checkOnlineAndOfflineResultsMatch( + query(coll, and(where('a', 'in', [2, 3]), where('a', 'in', [0, 1, 4]))) + ); + + // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). + await checkOnlineAndOfflineResultsMatch( + query(coll, or(where('a', 'in', [0, 3]), where('a', 'in', [0, 2]))), + 'doc3', + 'doc6' + ); + + // Nested composite filter on the same field. + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and( + where('a', 'in', [1, 3]), + or( + where('a', 'in', [0, 2]), + and(where('b', '>=', 1), where('a', 'in', [1, 3])) + ) + ) + ), + 'doc3', + 'doc4' + ); + + // Nested composite filter on the different fields. + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and( + where('b', 'in', [0, 3]), + or( + where('b', 'in', [1]), + and(where('b', 'in', [2, 3]), where('a', 'in', [1, 3])) + ) + ) + ), + 'doc4' + ); + }); + }); + + // TODO(orquery): Enable this test when prod supports OR queries. + // eslint-disable-next-line no-restricted-properties + it.skip('supports using in with array contains any', () => { + const testDocs = { + doc1: { a: 1, b: [0] }, + doc2: { b: [1] }, + doc3: { a: 3, b: [2, 7], c: 10 }, + doc4: { a: 1, b: [3, 7] }, + doc5: { a: 1 }, + doc6: { a: 2, c: 20 } + }; + + return withTestCollection(persistence, testDocs, async coll => { + await checkOnlineAndOfflineResultsMatch( + query( + coll, + or(where('a', 'in', [2, 3]), where('b', 'array-contains-any', [0, 7])) + ), + 'doc1', + 'doc3', + 'doc4', + 'doc6' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and( + where('a', 'in', [2, 3]), + where('b', 'array-contains-any', [0, 7]) + ) + ), + 'doc3' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + or( + and(where('a', 'in', [2, 3]), where('c', '==', 10)), + where('b', 'array-contains-any', [0, 7]) + ) + ), + 'doc1', + 'doc3', + 'doc4' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and( + where('a', 'in', [2, 3]), + or(where('b', 'array-contains-any', [0, 7]), where('c', '==', 20)) + ) + ), + 'doc3', + 'doc6' + ); + }); + }); + + // TODO(orquery): Enable this test when prod supports OR queries. + // eslint-disable-next-line no-restricted-properties + it.skip('supports using in with array contains', () => { + const testDocs = { + doc1: { a: 1, b: [0] }, + doc2: { b: [1] }, + doc3: { a: 3, b: [2, 7] }, + doc4: { a: 1, b: [3, 7] }, + doc5: { a: 1 }, + doc6: { a: 2 } + }; + + return withTestCollection(persistence, testDocs, async coll => { + await checkOnlineAndOfflineResultsMatch( + query( + coll, + or(where('a', 'in', [2, 3]), where('b', 'array-contains', 3)) + ), + 'doc3', + 'doc4', + 'doc6' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and(where('a', 'in', [2, 3]), where('b', 'array-contains', 7)) + ), + 'doc3' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + or( + where('a', 'in', [2, 3]), + and(where('b', 'array-contains', 3), where('a', '==', 1)) + ) + ), + 'doc3', + 'doc4', + 'doc6' + ); + + await checkOnlineAndOfflineResultsMatch( + query( + coll, + and( + where('a', 'in', [2, 3]), + or(where('b', 'array-contains', 7), where('a', '==', 1)) + ) + ), + 'doc3' + ); + }); + }); + + // TODO(orquery): Enable this test when prod supports OR queries. + // eslint-disable-next-line no-restricted-properties + it.skip('supports order by equality', () => { + const testDocs = { + doc1: {a: 1, b: [0]}, + doc2: {b: [1]}, + doc3: {a: 3, b: [2, 7], c: 10}, + doc4: {a: 1, b: [3, 7]}, + doc5: {a: 1}, + doc6: {a: 2, c: 20} + }; + + return withTestCollection(persistence, testDocs, async coll => { + await checkOnlineAndOfflineResultsMatch( + query(coll, where('a', '==', 1), orderBy('a')), + 'doc1', + 'doc4', + 'doc5' + ); + + await checkOnlineAndOfflineResultsMatch( + query(coll, where('a', 'in', [2, 3]), orderBy('a')), + 'doc6', + 'doc3' + ); + }); + }); }); // Reproduces https://github.com/firebase/firebase-js-sdk/issues/5873 @@ -1612,6 +1852,7 @@ apiDescribe('Queries', (persistence: boolean) => { expect(snapshot2.metadata.fromCache).to.be.true; expect(toDataArray(snapshot2)).to.deep.equal([]); }); +>>>>>>> master }); }); }); diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index c118e485310..50c78bd1d1e 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -961,149 +961,123 @@ apiDescribe('Validation:', (persistence: boolean) => { ); }); - validationIt(persistence, 'with multiple disjunctive filters fail', db => { - expect(() => - query( - collection(db, 'test'), - where('foo', 'in', [1, 2]), - where('foo', 'in', [2, 3]) - ) - ).to.throw("Invalid query. You cannot use more than one 'in' filter."); - - expect(() => - query( - collection(db, 'test'), - where('foo', 'not-in', [1, 2]), - where('foo', 'not-in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use more than one 'not-in' filter." - ); - - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains-any', [1, 2]), - where('foo', 'array-contains-any', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use more than one 'array-contains-any'" + - ' filter.' - ); - - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains-any', [2, 3]), - where('foo', 'in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'in' filters with " + - "'array-contains-any' filters." - ); + validationIt( + persistence, + 'with multiple disjunctive filters fail', + db => { + expect(() => + query( + collection(db, 'test'), + where('foo', 'not-in', [1, 2]), + where('foo', 'not-in', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use more than one 'not-in' filter." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'in', [2, 3]), - where('foo', 'array-contains-any', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'array-contains-any' filters with " + - "'in' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'array-contains-any', [1, 2]), + where('foo', 'array-contains-any', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use more than one 'array-contains-any'" + + ' filter.' + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'not-in', [2, 3]), - where('foo', 'array-contains-any', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'array-contains-any' filters with " + - "'not-in' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'not-in', [2, 3]), + where('foo', 'array-contains-any', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use 'array-contains-any' filters with " + + "'not-in' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains-any', [2, 3]), - where('foo', 'not-in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'not-in' filters with " + - "'array-contains-any' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'array-contains-any', [2, 3]), + where('foo', 'not-in', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use 'not-in' filters with " + + "'array-contains-any' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'not-in', [2, 3]), - where('foo', 'in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'in' filters with 'not-in' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'not-in', [2, 3]), + where('foo', 'in', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use 'in' filters with 'not-in' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'in', [2, 3]), - where('foo', 'not-in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'not-in' filters with 'in' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'in', [2, 3]), + where('foo', 'not-in', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use 'not-in' filters with 'in' filters." + ); - // This is redundant with the above tests, but makes sure our validation - // doesn't get confused. - expect(() => - query( - collection(db, 'test'), - where('foo', 'in', [2, 3]), - where('foo', 'array-contains', 1), - where('foo', 'array-contains-any', [2]) - ) - ).to.throw( - "Invalid query. You cannot use 'array-contains-any' filters with 'in' filters." - ); + // This is redundant with the above tests, but makes sure our validation + // doesn't get confused. + expect(() => + query( + collection(db, 'test'), + where('foo', 'in', [2, 3]), + where('foo', 'array-contains', 1), + where('foo', 'array-contains-any', [2]) + ) + ).to.throw( + "Invalid query. You cannot use 'array-contains-any' filters with 'array-contains' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains', 1), - where('foo', 'in', [2, 3]), - where('foo', 'array-contains-any', [2]) - ) - ).to.throw( - "Invalid query. You cannot use 'array-contains-any' filters with " + - "'array-contains' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'array-contains', 1), + where('foo', 'in', [2, 3]), + where('foo', 'array-contains-any', [2]) + ) + ).to.throw( + "Invalid query. You cannot use 'array-contains-any' filters with " + + "'array-contains' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'not-in', [2, 3]), - where('foo', 'array-contains', 2), - where('foo', 'array-contains-any', [2]) - ) - ).to.throw( - "Invalid query. You cannot use 'array-contains' filters with " + - "'not-in' filters." - ); + expect(() => + query( + collection(db, 'test'), + where('foo', 'not-in', [2, 3]), + where('foo', 'array-contains', 2), + where('foo', 'array-contains-any', [2]) + ) + ).to.throw( + "Invalid query. You cannot use 'array-contains' filters with " + + "'not-in' filters." + ); - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains', 2), - where('foo', 'in', [2]), - where('foo', 'not-in', [2, 3]) - ) - ).to.throw( - "Invalid query. You cannot use 'not-in' filters with " + - "'array-contains' filters." - ); - }); + expect(() => + query( + collection(db, 'test'), + where('foo', 'array-contains', 2), + where('foo', 'in', [2]), + where('foo', 'not-in', [2, 3]) + ) + ).to.throw( + "Invalid query. You cannot use 'not-in' filters with " + + "'array-contains' filters." + ); + } + ); validationIt( persistence, @@ -1135,15 +1109,6 @@ apiDescribe('Validation:', (persistence: boolean) => { ).to.throw( "Invalid query. You cannot use more than one 'array-contains' filter." ); - - expect(() => - query( - collection(db, 'test'), - where('foo', 'array-contains', 1), - where('foo', 'in', [2, 3]), - where('foo', 'in', [2, 3]) - ) - ).to.throw("Invalid query. You cannot use more than one 'in' filter."); } ); diff --git a/packages/firestore/test/unit/local/query_engine.test.ts b/packages/firestore/test/unit/local/query_engine.test.ts index 0762bad2a42..a321cb85cdd 100644 --- a/packages/firestore/test/unit/local/query_engine.test.ts +++ b/packages/firestore/test/unit/local/query_engine.test.ts @@ -1409,6 +1409,675 @@ function genericQueryEngineTest( verifyResult(result2, [doc1, doc4, doc6]); }); } + + // Tests in this section require client side indexing + if (configureCsi) { + it('combines indexed with non-indexed results', async () => { + debugAssert(configureCsi, 'Test requires durable persistence'); + + const doc1 = doc('coll/a', 1, { 'foo': true }); + const doc2 = doc('coll/b', 2, { 'foo': true }); + const doc3 = doc('coll/c', 3, { 'foo': true }); + const doc4 = doc('coll/d', 3, { 'foo': true }).setHasLocalMutations(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['foo', IndexKind.ASCENDING]] }) + ); + + await addDocument(doc1); + await addDocument(doc2); + await indexManager.updateIndexEntries(documentMap(doc1, doc2)); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc2) + ); + + await addDocument(doc3); + await addMutation(setMutation('coll/d', { 'foo': true })); + + const queryWithFilter = queryWithAddedFilter( + query('coll'), + filter('foo', '==', true) + ); + const results = await expectOptimizedCollectionQuery(() => + runQuery(queryWithFilter, SnapshotVersion.min()) + ); + + verifyResult(results, [doc1, doc2, doc3, doc4]); + }); + + it('uses partial index for limit queries', async () => { + debugAssert(configureCsi, 'Test requires durable persistence'); + + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': 0 }); + const doc2 = doc('coll/2', 1, { 'a': 1, 'b': 1 }); + const doc3 = doc('coll/3', 1, { 'a': 1, 'b': 2 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': 3 }); + const doc5 = doc('coll/5', 1, { 'a': 2, 'b': 3 }); + await addDocument(doc1, doc2, doc3, doc4, doc5); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc5) + ); + + const q = queryWithLimit( + queryWithAddedFilter( + queryWithAddedFilter(query('coll'), filter('a', '==', 1)), + filter('b', '==', 1) + ), + 3, + LimitType.First + ); + const results = await expectOptimizedCollectionQuery(() => + runQuery(q, SnapshotVersion.min()) + ); + + verifyResult(results, [doc2]); + }); + + it('re-fills indexed limit queries', async () => { + debugAssert(configureCsi, 'Test requires durable persistence'); + + const doc1 = doc('coll/1', 1, { 'a': 1 }); + const doc2 = doc('coll/2', 1, { 'a': 2 }); + const doc3 = doc('coll/3', 1, { 'a': 3 }); + const doc4 = doc('coll/4', 1, { 'a': 4 }); + await addDocument(doc1, doc2, doc3, doc4); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc4) + ); + + await addMutation(patchMutation('coll/3', { 'a': 5 })); + + const q = queryWithLimit( + queryWithAddedOrderBy(query('coll'), orderBy('a')), + 3, + LimitType.First + ); + const results = await expectOptimizedCollectionQuery(() => + runQuery(q, SnapshotVersion.min()) + ); + + verifyResult(results, [doc1, doc2, doc4]); + }); + + it('or query with in and not-in using index', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': 0 }); + const doc2 = doc('coll/2', 1, { 'b': 1 }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': 2 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': 3 }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.DESCENDING]] }) + ); + + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + + const query1 = query( + 'coll', + orFilter(filter('a', '==', 2), filter('b', 'in', [2, 3])) + ); + const result1 = await expectOptimizedCollectionQuery(() => + runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT) + ); + + verifyResult(result1, [doc3, doc4, doc6]); + + // a==2 || (b != 2 && b != 3) + // Has implicit "orderBy b" + const query2 = query( + 'coll', + orFilter(filter('a', '==', 2), filter('b', 'not-in', [2, 3])) + ); + const result2 = await expectOptimizedCollectionQuery(() => + runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT) + ); + verifyResult(result2, [doc1, doc2]); + }); + + it('query with array membership using index', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': [0] }); + const doc2 = doc('coll/2', 1, { 'b': [1] }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': [2, 7] }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': [3, 7] }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.CONTAINS]] }) + ); + + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + + const query1 = query( + 'coll', + orFilter(filter('a', '==', 2), filter('b', 'array-contains', 7)) + ); + const result1 = await expectOptimizedCollectionQuery(() => + runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT) + ); + verifyResult(result1, [doc3, doc4, doc6]); + + const query2 = query( + 'coll', + orFilter( + filter('a', '==', 2), + filter('b', 'array-contains-any', [0, 3]) + ) + ); + const result2 = await expectOptimizedCollectionQuery(() => + runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT) + ); + verifyResult(result2, [doc1, doc4, doc6]); + }); + } + + // Tests below this line execute with and without client side indexing + it('query with multiple ins on the same field', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': 0 }); + const doc2 = doc('coll/2', 1, { 'b': 1 }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': 2 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': 3 }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.DESCENDING]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + // a IN [1,2,3] && a IN [0,1,4] should result in "a==1". + const query1 = query( + 'coll', + andFilter(filter('a', 'in', [1, 2, 3]), filter('a', 'in', [0, 1, 4])) + ); + const result1 = await expectFunction(() => + runQuery(query1, lastLimboFreeSnapshot) + ); + verifyResult(result1, [doc1, doc4, doc5]); + + // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an empty set. + const query2 = query( + 'coll', + andFilter(filter('a', 'in', [2, 3]), filter('a', 'in', [0, 1, 4])) + ); + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, []); + + // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). + const query3 = query( + 'coll', + orFilter(filter('a', 'in', [0, 3]), filter('a', 'in', [0, 2])) + ); + const result3 = await expectFunction(() => + runQuery(query3, lastLimboFreeSnapshot) + ); + verifyResult(result3, [doc3, doc6]); + + // Nested composite filter: (a IN [0,1,2,3] && (a IN [0,2] || (b>1 && a IN [1,3])) + const query4 = query( + 'coll', + andFilter( + filter('a', 'in', [1, 2, 3]), + orFilter( + filter('a', 'in', [0, 2]), + andFilter(filter('b', '>=', 1), filter('a', 'in', [1, 3])) + ) + ) + ); + const result4 = await expectFunction(() => + runQuery(query4, lastLimboFreeSnapshot) + ); + verifyResult(result4, [doc3, doc4]); + }); + + it('query with ins and not-ins on the same field', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': 0 }); + const doc2 = doc('coll/2', 1, { 'b': 1 }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': 2 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': 3 }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.DESCENDING]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + // a IN [1,2,3] && a IN [0,1,3,4] && a NOT-IN [1] should result in + // "a==1 && a!=1 || a==3 && a!=1" or just "a == 3" + const query1 = query( + 'coll', + andFilter( + filter('a', 'in', [1, 2, 3]), + filter('a', 'in', [0, 1, 3, 4]), + filter('a', 'not-in', [1]) + ) + ); + const result1 = await expectFunction(() => + runQuery(query1, lastLimboFreeSnapshot) + ); + verifyResult(result1, [doc3]); + + // a IN [2,3] && a IN [0,1,2,4] && a NOT-IN [1,2] is never true and so the + // result should be an empty set. + const query2 = query( + 'coll', + andFilter( + filter('a', 'in', [2, 3]), + filter('a', 'in', [0, 1, 2, 4]), + filter('a', 'not-in', [1, 2]) + ) + ); + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, []); + + // a IN [] || a NOT-IN [0,1,2] should union them (similar to: a NOT-IN [0,1,2]). + const query3 = query( + 'coll', + orFilter(filter('a', 'in', []), filter('a', 'not-in', [0, 1, 2])) + ); + const result3 = await expectFunction(() => + runQuery(query3, lastLimboFreeSnapshot) + ); + verifyResult(result3, [doc3]); + + const query4 = query( + 'coll', + andFilter( + filter('a', '<=', 1), + filter('a', 'in', [1, 2, 3, 4]), + filter('a', 'not-in', [0, 2]) + ) + ); + const result4 = await expectFunction(() => + runQuery(query4, lastLimboFreeSnapshot) + ); + verifyResult(result4, [doc1, doc4, doc5]); + }); + + it('query with multiple ins on different fields', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': 0 }); + const doc2 = doc('coll/2', 1, { 'b': 1 }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': 2 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': 3 }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.DESCENDING]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + const query1 = query( + 'coll', + orFilter(filter('a', 'in', [2, 3]), filter('b', 'in', [0, 2])) + ); + const result1 = await expectFunction(() => + runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT) + ); + verifyResult(result1, [doc1, doc3, doc6]); + + const query2 = query( + 'coll', + andFilter(filter('a', 'in', [2, 3]), filter('b', 'in', [0, 2])) + ); + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, [doc3]); + + // Nested composite filter: (a IN [0,1,2,3] && (a IN [0,2] || (b>1 && a IN [1,3])) + const query3 = query( + 'coll', + andFilter( + filter('b', 'in', [0, 3]), + orFilter( + filter('b', 'in', [1]), + andFilter(filter('b', 'in', [2, 3]), filter('a', 'in', [1, 3])) + ) + ) + ); + const result3 = await expectFunction(() => + runQuery(query3, lastLimboFreeSnapshot) + ); + verifyResult(result3, [doc4]); + }); + + it('query in with array-contains-any', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': [0] }); + const doc2 = doc('coll/2', 1, { 'b': [1] }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': [2, 7], 'c': 10 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': [3, 7] }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2, 'c': 20 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.CONTAINS]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + const query1 = query( + 'coll', + orFilter( + filter('a', 'in', [2, 3]), + filter('b', 'array-contains-any', [0, 7]) + ) + ); + const result1 = await expectFunction(() => + runQuery(query1, lastLimboFreeSnapshot) + ); + verifyResult(result1, [doc1, doc3, doc4, doc6]); + + const query2 = query( + 'coll', + andFilter( + filter('a', 'in', [2, 3]), + filter('b', 'array-contains-any', [0, 7]) + ) + ); + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, [doc3]); + + const query3 = query( + 'coll', + orFilter( + andFilter(filter('a', 'in', [2, 3]), filter('c', '==', 10)), + filter('b', 'array-contains-any', [0, 7]) + ) + ); + const result3 = await expectFunction(() => + runQuery(query3, lastLimboFreeSnapshot) + ); + verifyResult(result3, [doc1, doc3, doc4]); + + const query4 = query( + 'coll', + andFilter( + filter('a', 'in', [2, 3]), + orFilter( + filter('b', 'array-contains-any', [0, 7]), + filter('c', '==', 20) + ) + ) + ); + const result4 = await expectFunction(() => + runQuery(query4, lastLimboFreeSnapshot) + ); + verifyResult(result4, [doc3, doc6]); + }); + + it('query in with array-contains', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': [0] }); + const doc2 = doc('coll/2', 1, { 'b': [1] }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': [2, 7], 'c': 10 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': [3, 7] }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2, 'c': 20 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.CONTAINS]] }) + ); + + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + const query1 = query( + 'coll', + orFilter(filter('a', 'in', [2, 3]), filter('b', 'array-contains', 3)) + ); + const result1 = await expectFunction(() => + runQuery(query1, lastLimboFreeSnapshot) + ); + verifyResult(result1, [doc3, doc4, doc6]); + + const query2 = query( + 'coll', + andFilter(filter('a', 'in', [2, 3]), filter('b', 'array-contains', 7)) + ); + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, [doc3]); + + const query3 = query( + 'coll', + orFilter( + filter('a', 'in', [2, 3]), + andFilter(filter('b', 'array-contains', 3), filter('a', '==', 1)) + ) + ); + const result3 = await expectFunction(() => + runQuery(query3, lastLimboFreeSnapshot) + ); + verifyResult(result3, [doc3, doc4, doc6]); + + const query4 = query( + 'coll', + andFilter( + filter('a', 'in', [2, 3]), + orFilter(filter('b', 'array-contains', 7), filter('a', '==', 1)) + ) + ); + const result4 = await expectFunction(() => + runQuery(query4, lastLimboFreeSnapshot) + ); + verifyResult(result4, [doc3]); + }); + + it('order by equality', async () => { + const doc1 = doc('coll/1', 1, { 'a': 1, 'b': [0] }); + const doc2 = doc('coll/2', 1, { 'b': [1] }); + const doc3 = doc('coll/3', 1, { 'a': 3, 'b': [2, 7], 'c': 10 }); + const doc4 = doc('coll/4', 1, { 'a': 1, 'b': [3, 7] }); + const doc5 = doc('coll/5', 1, { 'a': 1 }); + const doc6 = doc('coll/6', 1, { 'a': 2, 'c': 20 }); + await addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + let expectFunction = expectFullCollectionQuery; + let lastLimboFreeSnapshot = MISSING_LAST_LIMBO_FREE_SNAPSHOT; + + if (configureCsi) { + expectFunction = expectOptimizedCollectionQuery; + lastLimboFreeSnapshot = SnapshotVersion.min(); + + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['a', IndexKind.DESCENDING]] }) + ); + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['b', IndexKind.CONTAINS]] }) + ); + await indexManager.updateIndexEntries( + documentMap(doc1, doc2, doc3, doc4, doc5, doc6) + ); + await indexManager.updateCollectionGroup( + 'coll', + newIndexOffsetFromDocument(doc6) + ); + } + + const query1 = query('coll', filter('a', '==', 1), orderBy('a')); + const result1 = await expectFunction(() => + runQuery(query1, lastLimboFreeSnapshot) + ); + verifyResult(result1, [doc1, doc4, doc5]); + + const query2 = query('coll', filter('a', 'in', [2, 3]), orderBy('a')); + + const result2 = await expectFunction(() => + runQuery(query2, lastLimboFreeSnapshot) + ); + verifyResult(result2, [doc6, doc3]); + }); } function verifyResult(actualDocs: DocumentSet, expectedDocs: Document[]): void {