From fddeb6fb56089e1613c27b76730b0c5aee9f086a Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:26:16 -0400 Subject: [PATCH 01/16] add integration tests --- dev/system-test/firestore.ts | 352 ++++++++++++++++++++++++++++++++++- 1 file changed, 351 insertions(+), 1 deletion(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 12a43e478..614d5dc82 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -1645,7 +1645,7 @@ describe('Query class', () => { }); it('has limit() method', async () => { - await addDocs({foo: 'a'}, {foo: 'b'}); + await addDocs({ foo: 'a' }, { foo: 'b' }); const res = await randomCol.orderBy('foo').limit(1).get(); expectDocs(res, {foo: 'a'}); }); @@ -2544,6 +2544,356 @@ describe('Query class', () => { unsubscribe(); }); }); + + (process.env.FIRESTORE_EMULATOR_HOST === undefined + ? describe.skip + : describe)('multiple inequality', () => { + it('supports multiple inequality queries', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + // Multiple inequality fields + let res = await collection + .where('key', '!=', 'a') + .where('sort', '<=', 2) + .where('v', '>', 2) + .get(); + expectDocs(res, 'doc3'); + + // Duplicate inequality fields + res = await collection + .where('key', '!=', 'a') + .where('sort', '<=', 2) + .where('sort', '>', 1) + .get(); + expectDocs(res, 'doc4'); + + // With multiple IN + res = await collection + .where('key', '>=', 'a') + .where('sort', '<=', 2) + .where('v', 'in', [2, 3, 4]) + .where('sort', 'in', [2, 3]) + .get(); + expectDocs(res, 'doc4'); + + // With NOT-IN + res = await collection + .where('key', '>=', 'a') + .where('sort', '<=', 2) + .where('v', 'not-in', [2, 4, 5]) + .get(); + expectDocs(res, 'doc1', 'doc3'); + + // With orderby + res = await collection + .where('key', '>=', 'a') + .where('sort', '<=', 2) + .orderBy('v', 'desc') + .get(); + expectDocs(res, 'doc3', 'doc4', 'doc1'); + + // With limit + res = await collection + .where('key', '>=', 'a') + .where('sort', '<=', 2) + .orderBy('v', 'desc') + .limit(2) + .get(); + expectDocs(res, 'doc3', 'doc4'); + + // With limitToLast + res = await collection + .where('key', '>=', 'a') + .where('sort', '<=', 2) + .orderBy('v', 'desc') + .limitToLast(2) + .get(); + expectDocs(res, 'doc4', 'doc1'); + }); + + it('can use on special values', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let res = await collection + .where('key', '!=', 'a') + .where('sort', '<=', 2) + .get(); + expectDocs(res, 'doc5', 'doc6'); + + res = await collection + .where('key', '!=', 'a') + .where('sort', '<=', 2) + .where('v', '<=', 1) + .get(); + expectDocs(res, 'doc6'); + }); + + it('can use with array membership', async () => { + const collection = await testCollectionWithDocs({ + 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]}, + }); + + let res = await collection + .where('key', '!=', 'a') + .where('sort', '>=', 1) + .where('v', 'array-contains', 0) + .get(); + expectDocs(res, 'doc2'); + + res = await collection + .where('key', '!=', 'a') + .where('sort', '>=', 1) + .where('v', 'array-contains-any', [0, 1]) + .get(); + expectDocs(res, 'doc2', 'doc4'); + }); + + it('can use with nested field', async () => { + 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 collection = await testCollectionWithDocs({ + doc1: testData(400), + doc2: testData(200), + doc3: testData(100), + doc4: testData(300), + }); + + let res = await collection + .where('metadata.createdAt', '<=', 500) + .where('metadata.createdAt', '>', 100) + .where('name', '!=', 'room 200') + .orderBy('name') + .get(); + expectDocs(res, 'doc4', 'doc1'); + + res = await collection + .where('field', '>=', 'field 100') + .where(new FieldPath('field.dot'), '!=', 300) + .where('field\\slash', '<', 400) + .orderBy('name', 'desc') + .get(); + expectDocs(res, 'doc2', 'doc3'); + }); + + it('can use with nested composite filters', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let res = await collection + .where( + Filter.or( + Filter.and( + Filter.where('key', '==', 'b'), + Filter.where('sort', '<=', 2) + ), + Filter.and( + Filter.where('key', '!=', 'b'), + Filter.where('v', '>', 4) + ) + ) + ) + .get(); + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + expectDocs(res, 'doc1', 'doc6', 'doc5', 'doc4'); + + res = await collection + .where( + Filter.or( + Filter.and( + Filter.where('key', '==', 'b'), + Filter.where('sort', '<=', 2) + ), + Filter.and( + Filter.where('key', '!=', 'b'), + Filter.where('v', '>', 4) + ) + ) + ) + .orderBy('sort', 'desc') + .orderBy('key') + .get(); + // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc + expectDocs(res, 'doc5', 'doc4', 'doc1', 'doc6'); + + res = await collection + .where( + Filter.and( + Filter.or( + Filter.and( + Filter.where('key', '==', 'b'), + Filter.where('sort', '<=', 4) + ), + Filter.and( + Filter.where('key', '!=', 'b'), + Filter.where('v', '>=', 4) + ) + ), + Filter.or( + Filter.and( + Filter.where('key', '>', 'b'), + Filter.where('sort', '>=', 1) + ), + Filter.and( + Filter.where('key', '<', 'b'), + Filter.where('v', '>', 0) + ) + ) + ) + ) + .get(); + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + expectDocs(res, 'doc1', 'doc2'); + }); + + it('inequality fields will be implicitly ordered lexicographically by the server', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let res = await collection + .where('key', '!=', 'a') + .where('sort', '>', 1) + .where('v', 'in', [1, 2, 3, 4]) + .get(); + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc2', 'doc4', 'doc5', 'doc3'); + + res = await collection + .where('sort', '>', 1) + .where('key', '!=', 'a') + .where('v', 'in', [1, 2, 3, 4]) + .get(); + // Changing filters order will not effect implicit order. + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc2', 'doc4', 'doc5', 'doc3'); + }); + + it('can use multiple explicit order by field', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let res = await collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v') + .get(); + // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc2', 'doc4', 'doc3', 'doc5'); + + res = await collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v') + .orderBy('sort') + .get(); + // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc + expectDocs(res, 'doc2', 'doc5', 'doc4', 'doc3'); + + res = await collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v', 'desc') + .get(); + // Implicit order by matches the direction of last explicit order by. + // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc + expectDocs(res, 'doc5', 'doc3', 'doc4', 'doc2'); + + res = await collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v', 'desc') + .orderBy('sort') + .get(); + // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc + expectDocs(res, 'doc5', 'doc4', 'doc3', 'doc2'); + }); + + // TODO(Mila): do we need this? + it('can use in aggregate query', async () => {}); + + it('can use document ID im multiple inequality query', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let res = await collection + .where('sort', '>=', 1) + .where('key', '!=', 'a') + .where(FieldPath.documentId(), '<', 'doc5') + .get(); + // Document Key in inequality field will implicitly ordered to the last. + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc2', 'doc4', 'doc3'); + + res = await collection + .where(FieldPath.documentId(), '<', 'doc5') + .where('sort', '>=', 1) + .where('key', '!=', 'a') + .get(); + // Changing filters order will not effect implicit order. + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc2', 'doc4', 'doc3'); + + res = await collection + .where(FieldPath.documentId(), '<', 'doc5') + .where('sort', '>=', 1) + .where('key', '!=', 'a') + .orderBy('sort', 'desc') + .get(); + // Ordered by: 'sort' desc,'key' desc, __name__ desc + expectDocs(res, 'doc2', 'doc3', 'doc4'); + }); + }); }); describe('Aggregates', () => { From 77b2eb44b9a772e2917cb16a056e55ad54ab1059 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:40:59 -0400 Subject: [PATCH 02/16] add implicit order-bys if there is a cursor --- dev/src/reference.ts | 60 ++++++++++++++++++++++++------------ dev/system-test/firestore.ts | 24 ++++++++++++--- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index f268df88e..be0a01294 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -843,6 +843,7 @@ class FieldFilterInternal extends FilterInternal { case 'GREATER_THAN_OR_EQUAL': case 'LESS_THAN': case 'LESS_THAN_OR_EQUAL': + case 'NOT_EQUAL': return true; default: return false; @@ -1821,6 +1822,22 @@ export class Query implements firestore.Query { ); } + private getInequalityFilterFields(): FieldPath[] { + let FieldPathSet = new Set(); + + for (const filter of this._queryOptions.filters) { + for (const subFilter of filter.getFlattenedFilters()) { + if (subFilter.isInequalityFilter()) { + FieldPathSet = FieldPathSet.add(subFilter.field); + } + } + } + + // Sort the inequality fields lexicographically. + const sortedFieldPath = [...FieldPathSet].sort((a, b) => a.compareTo(b)); + return sortedFieldPath; + } + /** * Computes the backend ordering semantics for DocumentSnapshot cursors. * @@ -1846,29 +1863,32 @@ export class Query implements firestore.Query { } const fieldOrders = this._queryOptions.fieldOrders.slice(); - - // If no explicit ordering is specified, use the first inequality to - // define an implicit order. - if (fieldOrders.length === 0) { - for (const filter of this._queryOptions.filters) { - const fieldReference = filter.getFirstInequalityField(); - if (fieldReference !== null) { - fieldOrders.push(new FieldOrder(fieldReference)); - break; - } + const fieldsNormalized = new Set([...fieldOrders.map(item => item.field)]); + + /** The order of the implicit ordering always matches the last explicit order by. */ + const lastDirection = + fieldOrders.length === 0 + ? directionOperators.ASC + : fieldOrders[fieldOrders.length - 1].direction; + + /** + * Any inequality fields not explicitly ordered should be implicitly ordered in a + * lexicographical order. When there are multiple inequality filters on the same field, the + * field should be added only once. + * Note: getInequalityFilterFields function sorts the key field before + * other fields. However, we want the key field to be sorted last. + */ + const inequalityFields = this.getInequalityFilterFields(); + for (const field of inequalityFields) { + if ( + !fieldsNormalized.has(field) && + !field.isEqual(FieldPath.documentId()) + ) { + fieldOrders.push(new FieldOrder(field, lastDirection)); } } - const hasDocumentId = !!fieldOrders.find(fieldOrder => - FieldPath.documentId().isEqual(fieldOrder.field) - ); - if (!hasDocumentId) { - // Add implicit sorting by name, using the last specified direction. - const lastDirection = - fieldOrders.length === 0 - ? directionOperators.ASC - : fieldOrders[fieldOrders.length - 1].direction; - + if (!fieldsNormalized.has(FieldPath.documentId())) { fieldOrders.push(new FieldOrder(FieldPath.documentId(), lastDirection)); } diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 614d5dc82..159679853 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -1645,7 +1645,7 @@ describe('Query class', () => { }); it('has limit() method', async () => { - await addDocs({ foo: 'a' }, { foo: 'b' }); + await addDocs({foo: 'a'}, {foo: 'b'}); const res = await randomCol.orderBy('foo').limit(1).get(); expectDocs(res, {foo: 'a'}); }); @@ -2854,9 +2854,6 @@ describe('Query class', () => { expectDocs(res, 'doc5', 'doc4', 'doc3', 'doc2'); }); - // TODO(Mila): do we need this? - it('can use in aggregate query', async () => {}); - it('can use document ID im multiple inequality query', async () => { const collection = await testCollectionWithDocs({ doc1: {key: 'a', sort: 5}, @@ -2893,6 +2890,25 @@ describe('Query class', () => { // Ordered by: 'sort' desc,'key' desc, __name__ desc expectDocs(res, 'doc2', 'doc3', 'doc4'); }); + + it('Cursors with DocumentSnapshot implicitly adds inequality fields', async () => { + const collection = await testCollectionWithDocs({ + 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 docSnap = await collection.doc('doc4').get(); + let res = await collection + .where('key', '!=', 'a') + .where('sort', '>', 1) + .startAt(docSnap) + .get(); + expectDocs(res, 'doc4', 'doc5', 'doc3'); + }); }); }); From 04d8ec85217b958302a6a089659e2ba983a6de41 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:49:17 -0400 Subject: [PATCH 03/16] add comment --- dev/system-test/firestore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 159679853..9da59158f 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2897,8 +2897,7 @@ describe('Query class', () => { 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}, + doc5: {key: 'b', sort: 0, v: 1}, }); const docSnap = await collection.doc('doc4').get(); @@ -2907,7 +2906,8 @@ describe('Query class', () => { .where('sort', '>', 1) .startAt(docSnap) .get(); - expectDocs(res, 'doc4', 'doc5', 'doc3'); + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + expectDocs(res, 'doc4', 'doc3'); }); }); }); From 5bab6784f9672c3cf6beccbd27a11e0fd032c9c1 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:24:55 -0400 Subject: [PATCH 04/16] format --- dev/src/reference.ts | 11 ++++++----- dev/system-test/firestore.ts | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index be0a01294..4e42f79ef 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1834,8 +1834,7 @@ export class Query implements firestore.Query { } // Sort the inequality fields lexicographically. - const sortedFieldPath = [...FieldPathSet].sort((a, b) => a.compareTo(b)); - return sortedFieldPath; + return [...FieldPathSet].sort((a, b) => a.compareTo(b)); } /** @@ -1863,7 +1862,9 @@ export class Query implements firestore.Query { } const fieldOrders = this._queryOptions.fieldOrders.slice(); - const fieldsNormalized = new Set([...fieldOrders.map(item => item.field)]); + const fieldsNormalized = new Set([ + ...fieldOrders.map(item => item.field.toString()), + ]); /** The order of the implicit ordering always matches the last explicit order by. */ const lastDirection = @@ -1881,14 +1882,14 @@ export class Query implements firestore.Query { const inequalityFields = this.getInequalityFilterFields(); for (const field of inequalityFields) { if ( - !fieldsNormalized.has(field) && + !fieldsNormalized.has(field.toString()) && !field.isEqual(FieldPath.documentId()) ) { fieldOrders.push(new FieldOrder(field, lastDirection)); } } - if (!fieldsNormalized.has(FieldPath.documentId())) { + if (!fieldsNormalized.has(FieldPath.documentId().toString())) { fieldOrders.push(new FieldOrder(FieldPath.documentId(), lastDirection)); } diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 9da59158f..300cc835c 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2667,6 +2667,7 @@ describe('Query class', () => { }); it('can use with nested field', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const testData = (n?: number): any => { n = n || 1; return { @@ -2901,7 +2902,7 @@ describe('Query class', () => { }); const docSnap = await collection.doc('doc4').get(); - let res = await collection + const res = await collection .where('key', '!=', 'a') .where('sort', '>', 1) .startAt(docSnap) From daa5dcab82fcc2df8dba7730b80d65d2c80b9591 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 8 Aug 2023 18:00:26 -0400 Subject: [PATCH 05/16] remove getFirstInequalityField --- dev/src/reference.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 4e42f79ef..19b3cb6b4 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -710,9 +710,6 @@ abstract class FilterInternal { /** Returns a list of all filters that are contained within this filter */ abstract getFilters(): FilterInternal[]; - /** Returns the field of the first filter that's an inequality, or null if none. */ - abstract getFirstInequalityField(): FieldPath | null; - /** Returns the proto representation of this filter */ abstract toProto(): Filter; @@ -735,13 +732,6 @@ class CompositeFilterInternal extends FilterInternal { return this.filters; } - public getFirstInequalityField(): FieldPath | null { - return ( - this.getFlattenedFilters().find(filter => filter.isInequalityFilter()) - ?.field ?? null - ); - } - public isConjunction(): boolean { return this.operator === 'AND'; } @@ -807,14 +797,6 @@ class FieldFilterInternal extends FilterInternal { return [this]; } - getFirstInequalityField(): FieldPath | null { - if (this.isInequalityFilter()) { - return this.field; - } else { - return null; - } - } - /** * @param serializer The Firestore serializer * @param field The path of the property value to compare. From 655e42949132d10c7f928cbaa243393dcc591b40 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:33:47 -0400 Subject: [PATCH 06/16] Update reference.ts --- dev/src/reference.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 19b3cb6b4..bc64a44de 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1871,6 +1871,7 @@ export class Query implements firestore.Query { } } + // Add the document key field to the last if it is not explicitly ordered. if (!fieldsNormalized.has(FieldPath.documentId().toString())) { fieldOrders.push(new FieldOrder(FieldPath.documentId(), lastDirection)); } From a1318b18a412fcb2478fb143e39888893c68713c Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:48:39 -0400 Subject: [PATCH 07/16] format --- dev/src/reference.ts | 6 +- dev/system-test/firestore.ts | 123 ++++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index bc64a44de..4d8130658 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1804,6 +1804,11 @@ export class Query implements firestore.Query { ); } + /** + * Returns the sorted array of unique inequality filter fields used in this query. + * + * @return An array of inequality filter fields sorted lexicographically by FieldPath. + */ private getInequalityFilterFields(): FieldPath[] { let FieldPathSet = new Set(); @@ -1815,7 +1820,6 @@ export class Query implements firestore.Query { } } - // Sort the inequality fields lexicographically. return [...FieldPathSet].sort((a, b) => a.compareTo(b)); } diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 300cc835c..0a896c84c 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2557,63 +2557,63 @@ describe('Query class', () => { }); // Multiple inequality fields - let res = await collection + let results = await collection .where('key', '!=', 'a') .where('sort', '<=', 2) .where('v', '>', 2) .get(); - expectDocs(res, 'doc3'); + expectDocs(results, 'doc3'); // Duplicate inequality fields - res = await collection + results = await collection .where('key', '!=', 'a') .where('sort', '<=', 2) .where('sort', '>', 1) .get(); - expectDocs(res, 'doc4'); + expectDocs(results, 'doc4'); // With multiple IN - res = await collection + results = await collection .where('key', '>=', 'a') .where('sort', '<=', 2) .where('v', 'in', [2, 3, 4]) .where('sort', 'in', [2, 3]) .get(); - expectDocs(res, 'doc4'); + expectDocs(results, 'doc4'); // With NOT-IN - res = await collection + results = await collection .where('key', '>=', 'a') .where('sort', '<=', 2) .where('v', 'not-in', [2, 4, 5]) .get(); - expectDocs(res, 'doc1', 'doc3'); + expectDocs(results, 'doc1', 'doc3'); // With orderby - res = await collection + results = await collection .where('key', '>=', 'a') .where('sort', '<=', 2) .orderBy('v', 'desc') .get(); - expectDocs(res, 'doc3', 'doc4', 'doc1'); + expectDocs(results, 'doc3', 'doc4', 'doc1'); // With limit - res = await collection + results = await collection .where('key', '>=', 'a') .where('sort', '<=', 2) .orderBy('v', 'desc') .limit(2) .get(); - expectDocs(res, 'doc3', 'doc4'); + expectDocs(results, 'doc3', 'doc4'); // With limitToLast - res = await collection + results = await collection .where('key', '>=', 'a') .where('sort', '<=', 2) .orderBy('v', 'desc') .limitToLast(2) .get(); - expectDocs(res, 'doc4', 'doc1'); + expectDocs(results, 'doc4', 'doc1'); }); it('can use on special values', async () => { @@ -2626,18 +2626,18 @@ describe('Query class', () => { doc6: {key: 'f', sort: 1, v: 1}, }); - let res = await collection + let results = await collection .where('key', '!=', 'a') .where('sort', '<=', 2) .get(); - expectDocs(res, 'doc5', 'doc6'); + expectDocs(results, 'doc5', 'doc6'); - res = await collection + results = await collection .where('key', '!=', 'a') .where('sort', '<=', 2) .where('v', '<=', 1) .get(); - expectDocs(res, 'doc6'); + expectDocs(results, 'doc6'); }); it('can use with array membership', async () => { @@ -2651,19 +2651,19 @@ describe('Query class', () => { doc7: {key: 'g', sort: 4, v: [null]}, }); - let res = await collection + let results = await collection .where('key', '!=', 'a') .where('sort', '>=', 1) .where('v', 'array-contains', 0) .get(); - expectDocs(res, 'doc2'); + expectDocs(results, 'doc2'); - res = await collection + results = await collection .where('key', '!=', 'a') .where('sort', '>=', 1) .where('v', 'array-contains-any', [0, 1]) .get(); - expectDocs(res, 'doc2', 'doc4'); + expectDocs(results, 'doc2', 'doc4'); }); it('can use with nested field', async () => { @@ -2688,21 +2688,21 @@ describe('Query class', () => { doc4: testData(300), }); - let res = await collection + let results = await collection .where('metadata.createdAt', '<=', 500) .where('metadata.createdAt', '>', 100) .where('name', '!=', 'room 200') .orderBy('name') .get(); - expectDocs(res, 'doc4', 'doc1'); + expectDocs(results, 'doc4', 'doc1'); - res = await collection + results = await collection .where('field', '>=', 'field 100') .where(new FieldPath('field.dot'), '!=', 300) .where('field\\slash', '<', 400) .orderBy('name', 'desc') .get(); - expectDocs(res, 'doc2', 'doc3'); + expectDocs(results, 'doc2', 'doc3'); }); it('can use with nested composite filters', async () => { @@ -2715,7 +2715,7 @@ describe('Query class', () => { doc6: {key: 'b', sort: 0, v: 0}, }); - let res = await collection + let results = await collection .where( Filter.or( Filter.and( @@ -2730,9 +2730,9 @@ describe('Query class', () => { ) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expectDocs(res, 'doc1', 'doc6', 'doc5', 'doc4'); + expectDocs(results, 'doc1', 'doc6', 'doc5', 'doc4'); - res = await collection + results = await collection .where( Filter.or( Filter.and( @@ -2749,9 +2749,9 @@ describe('Query class', () => { .orderBy('key') .get(); // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc - expectDocs(res, 'doc5', 'doc4', 'doc1', 'doc6'); + expectDocs(results, 'doc5', 'doc4', 'doc1', 'doc6'); - res = await collection + results = await collection .where( Filter.and( Filter.or( @@ -2778,7 +2778,7 @@ describe('Query class', () => { ) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expectDocs(res, 'doc1', 'doc2'); + expectDocs(results, 'doc1', 'doc2'); }); it('inequality fields will be implicitly ordered lexicographically by the server', async () => { @@ -2791,22 +2791,22 @@ describe('Query class', () => { doc6: {key: 'b', sort: 0, v: 0}, }); - let res = await collection + let results = await collection .where('key', '!=', 'a') .where('sort', '>', 1) .where('v', 'in', [1, 2, 3, 4]) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc2', 'doc4', 'doc5', 'doc3'); + expectDocs(results, 'doc2', 'doc4', 'doc5', 'doc3'); - res = await collection + results = await collection .where('sort', '>', 1) .where('key', '!=', 'a') .where('v', 'in', [1, 2, 3, 4]) .get(); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc2', 'doc4', 'doc5', 'doc3'); + expectDocs(results, 'doc2', 'doc4', 'doc5', 'doc3'); }); it('can use multiple explicit order by field', async () => { @@ -2819,40 +2819,59 @@ describe('Query class', () => { doc6: {key: 'c', sort: 0, v: 2}, }); - let res = await collection + let results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v') .get(); // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc2', 'doc4', 'doc3', 'doc5'); + expectDocs(results, 'doc2', 'doc4', 'doc3', 'doc5'); - res = await collection + results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v') .orderBy('sort') .get(); // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc - expectDocs(res, 'doc2', 'doc5', 'doc4', 'doc3'); + expectDocs(results, 'doc2', 'doc5', 'doc4', 'doc3'); - res = await collection + results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v', 'desc') .get(); // Implicit order by matches the direction of last explicit order by. // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc - expectDocs(res, 'doc5', 'doc3', 'doc4', 'doc2'); + expectDocs(results, 'doc5', 'doc3', 'doc4', 'doc2'); - res = await collection + results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v', 'desc') .orderBy('sort') .get(); // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc - expectDocs(res, 'doc5', 'doc4', 'doc3', 'doc2'); + expectDocs(results, 'doc5', 'doc4', 'doc3', 'doc2'); + }); + + it('can use in aggregate query', async () => { + const collection = await testCollectionWithDocs({ + 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}, + }); + + let results = await collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v') + .count() + .get(); + expect(results.data().count).to.be.equal(4); + //TODO(MIEQ): Add sum and average when they are public. }); it('can use document ID im multiple inequality query', async () => { @@ -2864,32 +2883,32 @@ describe('Query class', () => { doc5: {key: 'bb', sort: 1}, }); - let res = await collection + let results = await collection .where('sort', '>=', 1) .where('key', '!=', 'a') .where(FieldPath.documentId(), '<', 'doc5') .get(); // Document Key in inequality field will implicitly ordered to the last. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc2', 'doc4', 'doc3'); + expectDocs(results, 'doc2', 'doc4', 'doc3'); - res = await collection + results = await collection .where(FieldPath.documentId(), '<', 'doc5') .where('sort', '>=', 1) .where('key', '!=', 'a') .get(); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc2', 'doc4', 'doc3'); + expectDocs(results, 'doc2', 'doc4', 'doc3'); - res = await collection + results = await collection .where(FieldPath.documentId(), '<', 'doc5') .where('sort', '>=', 1) .where('key', '!=', 'a') .orderBy('sort', 'desc') .get(); // Ordered by: 'sort' desc,'key' desc, __name__ desc - expectDocs(res, 'doc2', 'doc3', 'doc4'); + expectDocs(results, 'doc2', 'doc3', 'doc4'); }); it('Cursors with DocumentSnapshot implicitly adds inequality fields', async () => { @@ -2902,13 +2921,13 @@ describe('Query class', () => { }); const docSnap = await collection.doc('doc4').get(); - const res = await collection + const results = await collection .where('key', '!=', 'a') .where('sort', '>', 1) .startAt(docSnap) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(res, 'doc4', 'doc3'); + expectDocs(results, 'doc4', 'doc3'); }); }); }); From e56d6c097fb01b4b694320d6fc70e47219759af9 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Fri, 11 Aug 2023 16:50:47 +0000 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- dev/system-test/firestore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 0a896c84c..83d6241b5 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2864,7 +2864,7 @@ describe('Query class', () => { doc5: {key: 'bb', sort: 1, v: 1}, }); - let results = await collection + const results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v') From 2b36059dff4c35893c1bdc93071ee5a64f096e79 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:51:58 -0400 Subject: [PATCH 09/16] add cursor test for field name with dot --- dev/system-test/firestore.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 83d6241b5..5ad4f45ff 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2703,6 +2703,17 @@ describe('Query class', () => { .orderBy('name', 'desc') .get(); expectDocs(results, 'doc2', 'doc3'); + + // Cursor will add implicit order by fields in the same order as server does. + const docSnap = await collection.doc('doc2').get(); + results = await collection + .where('field', '>=', 'field 100') + .where(new FieldPath('field.dot'), '!=', 300) + .where('field\\slash', '<', 400) + .orderBy('name', 'desc') + .startAfter(docSnap) + .get(); + expectDocs(results, 'doc3'); }); it('can use with nested composite filters', async () => { From 66bd5c522c3ceef892c65f521b701860c4ff4140 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:50:21 -0400 Subject: [PATCH 10/16] use cursor to force the sdk add implicit orderbys --- dev/src/reference.ts | 9 +++--- dev/system-test/firestore.ts | 59 +++++++++++++++++------------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 4d8130658..28463491a 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1805,22 +1805,22 @@ export class Query implements firestore.Query { } /** - * Returns the sorted array of unique inequality filter fields used in this query. + * Returns the sorted array of inequality filter fields used in this query. * * @return An array of inequality filter fields sorted lexicographically by FieldPath. */ private getInequalityFilterFields(): FieldPath[] { - let FieldPathSet = new Set(); + const inequalityFields: FieldPath[] = []; for (const filter of this._queryOptions.filters) { for (const subFilter of filter.getFlattenedFilters()) { if (subFilter.isInequalityFilter()) { - FieldPathSet = FieldPathSet.add(subFilter.field); + inequalityFields.push(subFilter.field); } } } - return [...FieldPathSet].sort((a, b) => a.compareTo(b)); + return inequalityFields.sort((a, b) => a.compareTo(b)); } /** @@ -1872,6 +1872,7 @@ export class Query implements firestore.Query { !field.isEqual(FieldPath.documentId()) ) { fieldOrders.push(new FieldOrder(field, lastDirection)); + fieldsNormalized.add(field.toString()); } } diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 5ad4f45ff..dddfac7e7 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2666,6 +2666,8 @@ describe('Query class', () => { expectDocs(results, 'doc2', 'doc4'); }); + // Use cursor in following test cases to add implicit order by fields in the sdk. + it('can use with nested field', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const testData = (n?: number): any => { @@ -2688,32 +2690,27 @@ describe('Query class', () => { doc4: testData(300), }); + let docSnap = await collection.doc('doc4').get(); let results = await collection .where('metadata.createdAt', '<=', 500) .where('metadata.createdAt', '>', 100) .where('name', '!=', 'room 200') .orderBy('name') + .startAt(docSnap) .get(); + // ordered by: name asc, metadata.createdAt asc, __name__ asc expectDocs(results, 'doc4', 'doc1'); + docSnap = await collection.doc('doc2').get(); results = await collection .where('field', '>=', 'field 100') .where(new FieldPath('field.dot'), '!=', 300) .where('field\\slash', '<', 400) .orderBy('name', 'desc') + .startAt(docSnap) .get(); + // ordered by: name desc, field desc, field.dot desc, field\\slash desc, __name__ desc expectDocs(results, 'doc2', 'doc3'); - - // Cursor will add implicit order by fields in the same order as server does. - const docSnap = await collection.doc('doc2').get(); - results = await collection - .where('field', '>=', 'field 100') - .where(new FieldPath('field.dot'), '!=', 300) - .where('field\\slash', '<', 400) - .orderBy('name', 'desc') - .startAfter(docSnap) - .get(); - expectDocs(results, 'doc3'); }); it('can use with nested composite filters', async () => { @@ -2725,7 +2722,7 @@ describe('Query class', () => { doc5: {key: 'b', sort: 2, v: 1}, doc6: {key: 'b', sort: 0, v: 0}, }); - + let docSnap = await collection.doc('doc1').get(); let results = await collection .where( Filter.or( @@ -2739,10 +2736,12 @@ describe('Query class', () => { ) ) ) + .startAt(docSnap) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc expectDocs(results, 'doc1', 'doc6', 'doc5', 'doc4'); + docSnap = await collection.doc('doc5').get(); results = await collection .where( Filter.or( @@ -2758,10 +2757,12 @@ describe('Query class', () => { ) .orderBy('sort', 'desc') .orderBy('key') + .startAt(docSnap) .get(); // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc expectDocs(results, 'doc5', 'doc4', 'doc1', 'doc6'); + docSnap = await collection.doc('doc1').get(); results = await collection .where( Filter.and( @@ -2787,6 +2788,7 @@ describe('Query class', () => { ) ) ) + .startAt(docSnap) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc expectDocs(results, 'doc1', 'doc2'); @@ -2802,10 +2804,13 @@ describe('Query class', () => { doc6: {key: 'b', sort: 0, v: 0}, }); + const docSnap = await collection.doc('doc2').get(); + let results = await collection .where('key', '!=', 'a') .where('sort', '>', 1) .where('v', 'in', [1, 2, 3, 4]) + .startAt(docSnap) .get(); // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc expectDocs(results, 'doc2', 'doc4', 'doc5', 'doc3'); @@ -2814,6 +2819,7 @@ describe('Query class', () => { .where('sort', '>', 1) .where('key', '!=', 'a') .where('v', 'in', [1, 2, 3, 4]) + .startAt(docSnap) .get(); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -2830,10 +2836,12 @@ describe('Query class', () => { doc6: {key: 'c', sort: 0, v: 2}, }); + let docSnap = await collection.doc('doc2').get(); let results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v') + .startAt(docSnap) .get(); // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc expectDocs(results, 'doc2', 'doc4', 'doc3', 'doc5'); @@ -2843,14 +2851,17 @@ describe('Query class', () => { .where('sort', '>=', 1) .orderBy('v') .orderBy('sort') + .startAt(docSnap) .get(); // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc expectDocs(results, 'doc2', 'doc5', 'doc4', 'doc3'); + docSnap = await collection.doc('doc5').get(); results = await collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v', 'desc') + .startAt(docSnap) .get(); // Implicit order by matches the direction of last explicit order by. // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc @@ -2861,6 +2872,7 @@ describe('Query class', () => { .where('sort', '>=', 1) .orderBy('v', 'desc') .orderBy('sort') + .startAt(docSnap) .get(); // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc expectDocs(results, 'doc5', 'doc4', 'doc3', 'doc2'); @@ -2893,11 +2905,13 @@ describe('Query class', () => { doc4: {key: 'b', sort: 2}, doc5: {key: 'bb', sort: 1}, }); + let docSnap = await collection.doc('doc2').get(); let results = await collection .where('sort', '>=', 1) .where('key', '!=', 'a') .where(FieldPath.documentId(), '<', 'doc5') + .startAt(docSnap) .get(); // Document Key in inequality field will implicitly ordered to the last. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -2907,6 +2921,7 @@ describe('Query class', () => { .where(FieldPath.documentId(), '<', 'doc5') .where('sort', '>=', 1) .where('key', '!=', 'a') + .startAt(docSnap) .get(); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -2917,29 +2932,11 @@ describe('Query class', () => { .where('sort', '>=', 1) .where('key', '!=', 'a') .orderBy('sort', 'desc') + .startAt(docSnap) .get(); // Ordered by: 'sort' desc,'key' desc, __name__ desc expectDocs(results, 'doc2', 'doc3', 'doc4'); }); - - it('Cursors with DocumentSnapshot implicitly adds inequality fields', async () => { - const collection = await testCollectionWithDocs({ - 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: 0, v: 1}, - }); - - const docSnap = await collection.doc('doc4').get(); - const results = await collection - .where('key', '!=', 'a') - .where('sort', '>', 1) - .startAt(docSnap) - .get(); - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc4', 'doc3'); - }); }); }); From e505c1cb74633d359edf6ae348d5eb0abc969121 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Tue, 15 Aug 2023 21:06:51 +0000 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- dev/system-test/firestore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index dddfac7e7..1c48aa3bb 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2905,7 +2905,7 @@ describe('Query class', () => { doc4: {key: 'b', sort: 2}, doc5: {key: 'bb', sort: 1}, }); - let docSnap = await collection.doc('doc2').get(); + const docSnap = await collection.doc('doc2').get(); let results = await collection .where('sort', '>=', 1) From 94b0f180e3bd9b2570161d1a6cbdb656a48f1a72 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 16 Aug 2023 13:26:55 -0400 Subject: [PATCH 12/16] Update firestore.ts --- dev/system-test/firestore.ts | 282 ++++++++++++++++++++--------------- 1 file changed, 163 insertions(+), 119 deletions(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 1c48aa3bb..458330f55 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2666,8 +2666,8 @@ describe('Query class', () => { expectDocs(results, 'doc2', 'doc4'); }); - // Use cursor in following test cases to add implicit order by fields in the sdk. - + // Use cursor in following test cases to add implicit order by fields in the sdk and compare the + // result with the query fields normalized in the server. it('can use with nested field', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const testData = (n?: number): any => { @@ -2690,27 +2690,27 @@ describe('Query class', () => { doc4: testData(300), }); - let docSnap = await collection.doc('doc4').get(); - let results = await collection + // ordered by: name asc, metadata.createdAt asc, __name__ asc + let query = collection .where('metadata.createdAt', '<=', 500) .where('metadata.createdAt', '>', 100) .where('name', '!=', 'room 200') - .orderBy('name') - .startAt(docSnap) - .get(); - // ordered by: name asc, metadata.createdAt asc, __name__ asc - expectDocs(results, 'doc4', 'doc1'); + .orderBy('name'); + let docSnap = await collection.doc('doc4').get(); + let query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc4', 'doc1'); + expectDocs(await query_sdk_normalized.get(), 'doc4', 'doc1'); - docSnap = await collection.doc('doc2').get(); - results = await collection + // ordered by: name desc, field desc, field.dot desc, field\\slash desc, __name__ desc + query = collection .where('field', '>=', 'field 100') .where(new FieldPath('field.dot'), '!=', 300) .where('field\\slash', '<', 400) - .orderBy('name', 'desc') - .startAt(docSnap) - .get(); - // ordered by: name desc, field desc, field.dot desc, field\\slash desc, __name__ desc - expectDocs(results, 'doc2', 'doc3'); + .orderBy('name', 'desc'); + docSnap = await collection.doc('doc2').get(); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc3'); + expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc3'); }); it('can use with nested composite filters', async () => { @@ -2722,8 +2722,30 @@ describe('Query class', () => { doc5: {key: 'b', sort: 2, v: 1}, doc6: {key: 'b', sort: 0, v: 0}, }); + + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + let query = collection.where( + Filter.or( + Filter.and( + Filter.where('key', '==', 'b'), + Filter.where('sort', '<=', 2) + ), + Filter.and(Filter.where('key', '!=', 'b'), Filter.where('v', '>', 4)) + ) + ); let docSnap = await collection.doc('doc1').get(); - let results = await collection + let query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc1', 'doc6', 'doc5', 'doc4'); + expectDocs( + await query_sdk_normalized.get(), + 'doc1', + 'doc6', + 'doc5', + 'doc4' + ); + + // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc + query = collection .where( Filter.or( Filter.and( @@ -2736,62 +2758,45 @@ describe('Query class', () => { ) ) ) - .startAt(docSnap) - .get(); - // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expectDocs(results, 'doc1', 'doc6', 'doc5', 'doc4'); - + .orderBy('sort', 'desc') + .orderBy('key'); docSnap = await collection.doc('doc5').get(); - results = await collection - .where( + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc5', 'doc4', 'doc1', 'doc6'); + expectDocs( + await query_sdk_normalized.get(), + 'doc5', + 'doc4', + 'doc1', + 'doc6' + ); + + // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc + query = collection.where( + Filter.and( Filter.or( Filter.and( Filter.where('key', '==', 'b'), - Filter.where('sort', '<=', 2) + Filter.where('sort', '<=', 4) ), Filter.and( Filter.where('key', '!=', 'b'), - Filter.where('v', '>', 4) + Filter.where('v', '>=', 4) ) - ) - ) - .orderBy('sort', 'desc') - .orderBy('key') - .startAt(docSnap) - .get(); - // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc - expectDocs(results, 'doc5', 'doc4', 'doc1', 'doc6'); - - docSnap = await collection.doc('doc1').get(); - results = await collection - .where( - Filter.and( - Filter.or( - Filter.and( - Filter.where('key', '==', 'b'), - Filter.where('sort', '<=', 4) - ), - Filter.and( - Filter.where('key', '!=', 'b'), - Filter.where('v', '>=', 4) - ) + ), + Filter.or( + Filter.and( + Filter.where('key', '>', 'b'), + Filter.where('sort', '>=', 1) ), - Filter.or( - Filter.and( - Filter.where('key', '>', 'b'), - Filter.where('sort', '>=', 1) - ), - Filter.and( - Filter.where('key', '<', 'b'), - Filter.where('v', '>', 0) - ) - ) + Filter.and(Filter.where('key', '<', 'b'), Filter.where('v', '>', 0)) ) ) - .startAt(docSnap) - .get(); - // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc - expectDocs(results, 'doc1', 'doc2'); + ); + docSnap = await collection.doc('doc1').get(); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc1', 'doc2'); + expectDocs(await query_sdk_normalized.get(), 'doc1', 'doc2'); }); it('inequality fields will be implicitly ordered lexicographically by the server', async () => { @@ -2806,24 +2811,36 @@ describe('Query class', () => { const docSnap = await collection.doc('doc2').get(); - let results = await collection + // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc + let query = collection .where('key', '!=', 'a') .where('sort', '>', 1) - .where('v', 'in', [1, 2, 3, 4]) - .startAt(docSnap) - .get(); - // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc2', 'doc4', 'doc5', 'doc3'); + .where('v', 'in', [1, 2, 3, 4]); + let query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc4', 'doc5', 'doc3'); + expectDocs( + await query_sdk_normalized.get(), + 'doc2', + 'doc4', + 'doc5', + 'doc3' + ); - results = await collection - .where('sort', '>', 1) - .where('key', '!=', 'a') - .where('v', 'in', [1, 2, 3, 4]) - .startAt(docSnap) - .get(); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc2', 'doc4', 'doc5', 'doc3'); + query = collection + .where('sort', '>', 1) + .where('key', '!=', 'a') + .where('v', 'in', [1, 2, 3, 4]); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc4', 'doc5', 'doc3'); + expectDocs( + await query_sdk_normalized.get(), + 'doc2', + 'doc4', + 'doc5', + 'doc3' + ); }); it('can use multiple explicit order by field', async () => { @@ -2837,45 +2854,71 @@ describe('Query class', () => { }); let docSnap = await collection.doc('doc2').get(); - let results = await collection + + // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc + let query = collection .where('key', '>', 'a') .where('sort', '>=', 1) - .orderBy('v') - .startAt(docSnap) - .get(); - // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc2', 'doc4', 'doc3', 'doc5'); + .orderBy('v'); + let query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc4', 'doc3', 'doc5'); + expectDocs( + await query_sdk_normalized.get(), + 'doc2', + 'doc4', + 'doc3', + 'doc5' + ); - results = await collection + // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc + query = collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v') - .orderBy('sort') - .startAt(docSnap) - .get(); - // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc - expectDocs(results, 'doc2', 'doc5', 'doc4', 'doc3'); + .orderBy('sort'); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc5', 'doc4', 'doc3'); + expectDocs( + await query_sdk_normalized.get(), + 'doc2', + 'doc5', + 'doc4', + 'doc3' + ); docSnap = await collection.doc('doc5').get(); - results = await collection - .where('key', '>', 'a') - .where('sort', '>=', 1) - .orderBy('v', 'desc') - .startAt(docSnap) - .get(); + // Implicit order by matches the direction of last explicit order by. // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc - expectDocs(results, 'doc5', 'doc3', 'doc4', 'doc2'); + query = collection + .where('key', '>', 'a') + .where('sort', '>=', 1) + .orderBy('v', 'desc'); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc5', 'doc3', 'doc4', 'doc2'); + expectDocs( + await query_sdk_normalized.get(), + 'doc5', + 'doc3', + 'doc4', + 'doc2' + ); - results = await collection + // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc + query = collection .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v', 'desc') - .orderBy('sort') - .startAt(docSnap) - .get(); - // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc - expectDocs(results, 'doc5', 'doc4', 'doc3', 'doc2'); + .orderBy('sort'); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc5', 'doc4', 'doc3', 'doc2'); + expectDocs( + await query_sdk_normalized.get(), + 'doc5', + 'doc4', + 'doc3', + 'doc2' + ); }); it('can use in aggregate query', async () => { @@ -2905,37 +2948,38 @@ describe('Query class', () => { doc4: {key: 'b', sort: 2}, doc5: {key: 'bb', sort: 1}, }); + const docSnap = await collection.doc('doc2').get(); - let results = await collection - .where('sort', '>=', 1) - .where('key', '!=', 'a') - .where(FieldPath.documentId(), '<', 'doc5') - .startAt(docSnap) - .get(); // Document Key in inequality field will implicitly ordered to the last. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc2', 'doc4', 'doc3'); - - results = await collection - .where(FieldPath.documentId(), '<', 'doc5') + let query = collection .where('sort', '>=', 1) .where('key', '!=', 'a') - .startAt(docSnap) - .get(); + .where(FieldPath.documentId(), '<', 'doc5'); + let query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc4', 'doc3'); + expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc4', 'doc3'); + // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc - expectDocs(results, 'doc2', 'doc4', 'doc3'); + query = collection + .where(FieldPath.documentId(), '<', 'doc5') + .where('sort', '>=', 1) + .where('key', '!=', 'a'); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc4', 'doc3'); + expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc4', 'doc3'); - results = await collection + // Ordered by: 'sort' desc,'key' desc, __name__ desc + query = collection .where(FieldPath.documentId(), '<', 'doc5') .where('sort', '>=', 1) .where('key', '!=', 'a') - .orderBy('sort', 'desc') - .startAt(docSnap) - .get(); - // Ordered by: 'sort' desc,'key' desc, __name__ desc - expectDocs(results, 'doc2', 'doc3', 'doc4'); + .orderBy('sort', 'desc'); + query_sdk_normalized = query.startAt(docSnap); + expectDocs(await query.get(), 'doc2', 'doc3', 'doc4'); + expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc3', 'doc4'); }); }); }); From 990c24578bb4b58dcd667c83df12d4e106f7f72d Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:32:56 -0400 Subject: [PATCH 13/16] resolve comments --- dev/system-test/firestore.ts | 104 ++++++++++------------------------- 1 file changed, 28 insertions(+), 76 deletions(-) diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 458330f55..4a8153e7e 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2697,9 +2697,9 @@ describe('Query class', () => { .where('name', '!=', 'room 200') .orderBy('name'); let docSnap = await collection.doc('doc4').get(); - let query_sdk_normalized = query.startAt(docSnap); + let queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc4', 'doc1'); - expectDocs(await query_sdk_normalized.get(), 'doc4', 'doc1'); + expectDocs(await queryWithCursor.get(), 'doc4', 'doc1'); // ordered by: name desc, field desc, field.dot desc, field\\slash desc, __name__ desc query = collection @@ -2708,9 +2708,9 @@ describe('Query class', () => { .where('field\\slash', '<', 400) .orderBy('name', 'desc'); docSnap = await collection.doc('doc2').get(); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc3'); - expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc3'); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc3'); }); it('can use with nested composite filters', async () => { @@ -2734,15 +2734,9 @@ describe('Query class', () => { ) ); let docSnap = await collection.doc('doc1').get(); - let query_sdk_normalized = query.startAt(docSnap); + let queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc1', 'doc6', 'doc5', 'doc4'); - expectDocs( - await query_sdk_normalized.get(), - 'doc1', - 'doc6', - 'doc5', - 'doc4' - ); + expectDocs(await queryWithCursor.get(), 'doc1', 'doc6', 'doc5', 'doc4'); // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc query = collection @@ -2761,15 +2755,9 @@ describe('Query class', () => { .orderBy('sort', 'desc') .orderBy('key'); docSnap = await collection.doc('doc5').get(); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc5', 'doc4', 'doc1', 'doc6'); - expectDocs( - await query_sdk_normalized.get(), - 'doc5', - 'doc4', - 'doc1', - 'doc6' - ); + expectDocs(await queryWithCursor.get(), 'doc5', 'doc4', 'doc1', 'doc6'); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc query = collection.where( @@ -2794,9 +2782,9 @@ describe('Query class', () => { ) ); docSnap = await collection.doc('doc1').get(); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc1', 'doc2'); - expectDocs(await query_sdk_normalized.get(), 'doc1', 'doc2'); + expectDocs(await queryWithCursor.get(), 'doc1', 'doc2'); }); it('inequality fields will be implicitly ordered lexicographically by the server', async () => { @@ -2816,15 +2804,9 @@ describe('Query class', () => { .where('key', '!=', 'a') .where('sort', '>', 1) .where('v', 'in', [1, 2, 3, 4]); - let query_sdk_normalized = query.startAt(docSnap); + let queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc4', 'doc5', 'doc3'); - expectDocs( - await query_sdk_normalized.get(), - 'doc2', - 'doc4', - 'doc5', - 'doc3' - ); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc4', 'doc5', 'doc3'); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -2832,15 +2814,9 @@ describe('Query class', () => { .where('sort', '>', 1) .where('key', '!=', 'a') .where('v', 'in', [1, 2, 3, 4]); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc4', 'doc5', 'doc3'); - expectDocs( - await query_sdk_normalized.get(), - 'doc2', - 'doc4', - 'doc5', - 'doc3' - ); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc4', 'doc5', 'doc3'); }); it('can use multiple explicit order by field', async () => { @@ -2860,15 +2836,9 @@ describe('Query class', () => { .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v'); - let query_sdk_normalized = query.startAt(docSnap); + let queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc4', 'doc3', 'doc5'); - expectDocs( - await query_sdk_normalized.get(), - 'doc2', - 'doc4', - 'doc3', - 'doc5' - ); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc4', 'doc3', 'doc5'); // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc query = collection @@ -2876,15 +2846,9 @@ describe('Query class', () => { .where('sort', '>=', 1) .orderBy('v') .orderBy('sort'); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc5', 'doc4', 'doc3'); - expectDocs( - await query_sdk_normalized.get(), - 'doc2', - 'doc5', - 'doc4', - 'doc3' - ); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc5', 'doc4', 'doc3'); docSnap = await collection.doc('doc5').get(); @@ -2894,15 +2858,9 @@ describe('Query class', () => { .where('key', '>', 'a') .where('sort', '>=', 1) .orderBy('v', 'desc'); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc5', 'doc3', 'doc4', 'doc2'); - expectDocs( - await query_sdk_normalized.get(), - 'doc5', - 'doc3', - 'doc4', - 'doc2' - ); + expectDocs(await queryWithCursor.get(), 'doc5', 'doc3', 'doc4', 'doc2'); // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc query = collection @@ -2910,15 +2868,9 @@ describe('Query class', () => { .where('sort', '>=', 1) .orderBy('v', 'desc') .orderBy('sort'); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc5', 'doc4', 'doc3', 'doc2'); - expectDocs( - await query_sdk_normalized.get(), - 'doc5', - 'doc4', - 'doc3', - 'doc2' - ); + expectDocs(await queryWithCursor.get(), 'doc5', 'doc4', 'doc3', 'doc2'); }); it('can use in aggregate query', async () => { @@ -2957,9 +2909,9 @@ describe('Query class', () => { .where('sort', '>=', 1) .where('key', '!=', 'a') .where(FieldPath.documentId(), '<', 'doc5'); - let query_sdk_normalized = query.startAt(docSnap); + let queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc4', 'doc3'); - expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc4', 'doc3'); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc4', 'doc3'); // Changing filters order will not effect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -2967,9 +2919,9 @@ describe('Query class', () => { .where(FieldPath.documentId(), '<', 'doc5') .where('sort', '>=', 1) .where('key', '!=', 'a'); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc4', 'doc3'); - expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc4', 'doc3'); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc4', 'doc3'); // Ordered by: 'sort' desc,'key' desc, __name__ desc query = collection @@ -2977,9 +2929,9 @@ describe('Query class', () => { .where('sort', '>=', 1) .where('key', '!=', 'a') .orderBy('sort', 'desc'); - query_sdk_normalized = query.startAt(docSnap); + queryWithCursor = query.startAt(docSnap); expectDocs(await query.get(), 'doc2', 'doc3', 'doc4'); - expectDocs(await query_sdk_normalized.get(), 'doc2', 'doc3', 'doc4'); + expectDocs(await queryWithCursor.get(), 'doc2', 'doc3', 'doc4'); }); }); }); From db313e9d8782c774ca3ef3e3be1e8379c70b811d Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:49:25 -0400 Subject: [PATCH 14/16] port unit tests on explicit orderby normalization --- dev/test/query.ts | 564 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 564 insertions(+) diff --git a/dev/test/query.ts b/dev/test/query.ts index d65dde537..c1ca4497d 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -2440,6 +2440,570 @@ describe('startAt() interface', () => { }); }); + describe('inequality fields are implicitly ordered lexicographically', () => { + it('upper and lower case characters', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'A', + 'ASCENDING', + 'a', + 'ASCENDING', + 'aa', + 'ASCENDING', + 'b', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt(true, 'A', 'a', 'aa', 'b', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + fieldFiltersQuery( + 'a', + 'LESS_THAN', + 'value', + 'a', + 'GREATER_THAN_OR_EQUAL', + 'value', + 'aa', + 'GREATER_THAN', + 'value', + 'b', + 'GREATER_THAN', + 'value', + 'A', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + aa: 'aa', + b: 'b', + A: 'A', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'value') + .where('a', '>=', 'value') + .where('aa', '>', 'value') + .where('b', '>', 'value') + .where('A', '>', 'value') + .startAt(doc); + return query.get(); + }); + }); + }); + + it('characters and numbers', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + '`1`', + 'ASCENDING', + '`19`', + 'ASCENDING', + '`2`', + 'ASCENDING', + 'a', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt(true, '1', '19', '2', 'a', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + fieldFiltersQuery( + 'a', + 'LESS_THAN', + 'value', + '`1`', + 'GREATER_THAN', + 'value', + '`19`', + 'GREATER_THAN', + 'value', + '`2`', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + 1: '1', + 19: '19', + 2: '2', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'value') + .where('1', '>', 'value') + .where('19', '>', 'value') + .where('2', '>', 'value') + .startAt(doc); + return query.get(); + }); + }); + }); + + it('nested fields', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'a', + 'ASCENDING', + 'a.a', + 'ASCENDING', + 'aa', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt( + true, + { + mapValue: { + fields: { + a: { + stringValue: 'a.a', + }, + }, + }, + }, + 'a.a', + 'aa', + { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + } + ), + fieldFiltersQuery( + 'a', + 'LESS_THAN', + 'value', + 'a.a', + 'GREATER_THAN', + 'value', + 'aa', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', {a: {a: 'a.a'}, aa: 'aa'}).then( + doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'value') + .where('a.a', '>', 'value') + .where('aa', '>', 'value') + .startAt(doc); + return query.get(); + } + ); + }); + }); + + it('special characters', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + '_a', + 'ASCENDING', + 'a', + 'ASCENDING', + 'a.a', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt( + true, + '_a', + { + mapValue: { + fields: { + a: { + stringValue: 'a.a', + }, + }, + }, + }, + 'a.a', + { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + } + ), + fieldFiltersQuery( + 'a', + 'LESS_THAN', + 'a', + '_a', + 'GREATER_THAN', + '_a', + 'a.a', + 'GREATER_THAN', + 'a.a' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', {a: {a: 'a.a'}, _a: '_a'}).then( + doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'a') + .where('_a', '>', '_a') + .where('a.a', '>', 'a.a') + .startAt(doc); + return query.get(); + } + ); + }); + }); + + it('field name with dot', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'a', + 'ASCENDING', + 'a.z', + 'ASCENDING', + '`a.a`', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt( + true, + { + mapValue: { + fields: { + z: { + stringValue: 'a.z', + }, + }, + }, + }, + 'a.z', + 'a.a', + { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + } + ), + fieldFiltersQuery( + 'a', + 'LESS_THAN', + 'value', + '`a.a`', + 'GREATER_THAN', + 'value', + 'a.z', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', {a: {z: 'a.z'}, 'a.a': 'a.a'}).then( + doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'value') + .where(new FieldPath('a.a'), '>', 'value') // field name with dot + .where('a.z', '>', 'value') // nested field + .startAt(doc); + return query.get(); + } + ); + }); + }); + + it('composite filter', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'a', + 'ASCENDING', + 'b', + 'ASCENDING', + 'c', + 'ASCENDING', + 'd', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt(true, 'a', 'b', 'c', 'd', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + where( + compositeFilter( + 'AND', + fieldFilter('a', 'LESS_THAN', 'value'), + + compositeFilter( + 'AND', + compositeFilter( + 'OR', + fieldFilter('b', 'GREATER_THAN_OR_EQUAL', 'value'), + fieldFilter('c', 'LESS_THAN_OR_EQUAL', 'value') + ), + compositeFilter( + 'OR', + fieldFilter('d', 'GREATER_THAN', 'value'), + fieldFilter('e', 'EQUAL', 'value') + ) + ) + ) + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('a', '<', 'value') + .where( + Filter.and( + Filter.or( + Filter.where('b', '>=', 'value'), + Filter.where('c', '<=', 'value') + ), + Filter.or( + Filter.where('d', '>', 'value'), + Filter.where('e', '==', 'value') + ) + ) + ) + .startAt(doc); + return query.get(); + }); + }); + }); + + it('explicit orderby', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'z', + 'ASCENDING', + 'a', + 'ASCENDING', + 'b', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt(true, 'z', 'a', 'b', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + fieldFiltersQuery( + 'b', + 'LESS_THAN', + 'value', + 'a', + 'GREATER_THAN', + 'value', + 'z', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + b: 'b', + z: 'z', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('b', '<', 'value') + .where('a', '>', 'value') + .where('z', '>', 'value') + .orderBy('z') + .startAt(doc); + return query.get(); + }); + }); + }); + + it('explicit order by direction', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'z', + 'DESCENDING', + 'a', + 'DESCENDING', + 'b', + 'DESCENDING', + '__name__', + 'DESCENDING' + ), + startAt(true, 'z', 'a', 'b', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + fieldFiltersQuery( + 'b', + 'LESS_THAN', + 'value', + 'a', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + b: 'b', + z: 'z', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('b', '<', 'value') + .where('a', '>', 'value') + .orderBy('z', 'desc') + .startAt(doc); + return query.get(); + }); + }); + }); + + it('last explicit order by direction', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + orderBy( + 'z', + 'DESCENDING', + 'c', + 'ASCENDING', + 'a', + 'ASCENDING', + 'b', + 'ASCENDING', + '__name__', + 'ASCENDING' + ), + startAt(true, 'z', 'c', 'a', 'b', { + referenceValue: + `projects/${PROJECT_ID}/databases/(default)/` + + 'documents/collectionId/doc', + }), + fieldFiltersQuery( + 'b', + 'LESS_THAN', + 'value', + 'a', + 'GREATER_THAN', + 'value' + ) + ); + return stream(); + }, + }; + + return createInstance(overrides).then(firestoreInstance => { + firestore = firestoreInstance; + return snapshot('collectionId/doc', { + a: 'a', + b: 'b', + c: 'c', + z: 'z', + }).then(doc => { + const query = firestore + .collection('collectionId') + .where('b', '<', 'value') + .where('a', '>', 'value') + .orderBy('z', 'desc') + .orderBy('c') + .startAt(doc); + return query.get(); + }); + }); + }); + }); + it('validates field exists in document snapshot', () => { const query = firestore.collection('collectionId').orderBy('foo', 'desc'); From 1beb1d2d97a92e083e6263fcaf75a580208e3954 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:55:13 -0400 Subject: [PATCH 15/16] Update query.ts --- dev/test/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/test/query.ts b/dev/test/query.ts index c1ca4497d..1e8458f61 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -2440,7 +2440,7 @@ describe('startAt() interface', () => { }); }); - describe('inequality fields are implicitly ordered lexicographically', () => { + describe('inequality fields are implicitly ordered lexicographically for cursors', () => { it('upper and lower case characters', () => { const overrides: ApiOverride = { runQuery: request => { From b0b7f57dd7f234f25bb7cb29f333f86788525a17 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:13:21 -0400 Subject: [PATCH 16/16] Update reference.ts --- dev/src/reference.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index cbeca3a71..503be5852 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -826,6 +826,7 @@ class FieldFilterInternal extends FilterInternal { case 'LESS_THAN': case 'LESS_THAN_OR_EQUAL': case 'NOT_EQUAL': + case 'NOT_IN': return true; default: return false;