diff --git a/packages/firestore/firestore_collection_group_index_config.tf b/packages/firestore/firestore_collection_group_index_config.tf new file mode 100644 index 00000000000..15d431e8e80 --- /dev/null +++ b/packages/firestore/firestore_collection_group_index_config.tf @@ -0,0 +1,15 @@ +locals { + collection_group_indexes = { + index1 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "a" + order = "ASCENDING" + }, + ] + + } +} diff --git a/packages/firestore/firestore_index_config.tf b/packages/firestore/firestore_composite_index_config.tf similarity index 65% rename from packages/firestore/firestore_index_config.tf rename to packages/firestore/firestore_composite_index_config.tf index 7fafeae6fbf..7258ac0157e 100644 --- a/packages/firestore/firestore_index_config.tf +++ b/packages/firestore/firestore_composite_index_config.tf @@ -100,5 +100,55 @@ locals { order = "DESCENDING" }, ] + index9 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "pages" + order = "ASCENDING" + }, + { + field_path = "year" + order = "ASCENDING" + }, + ] + index10 = [ + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "pages" + order = "ASCENDING" + }, + { + field_path = "rating" + order = "ASCENDING" + }, + { + field_path = "year" + order = "ASCENDING" + }, + ] + index11 = [ + { + field_path = "rating" + array_config = "CONTAINS" + }, + { + field_path = "testId" + order = "ASCENDING" + }, + { + field_path = "pages" + order = "ASCENDING" + }, + { + field_path = "rating" + order = "ASCENDING" + }, + ] } } diff --git a/packages/firestore/main.tf b/packages/firestore/main.tf index 397c4d96ebd..d31f6c98a79 100644 --- a/packages/firestore/main.tf +++ b/packages/firestore/main.tf @@ -4,25 +4,46 @@ provider "google" { project = var.projectId } -resource "google_firestore_index" "default-db-index" { +resource "google_firestore_index" "default_db_index" { collection = "composite-index-test-collection" for_each = local.indexes dynamic "fields" { for_each = distinct(flatten([for k, v in local.indexes : [ for i in each.value : { - field_path = i.field_path - order = i.order + field_path = i.field_path + order = can(i.order) ? i.order : null + array_config = can(i.array_config) ? i.array_config : null }]])) content { - field_path = lookup(fields.value, "field_path", null) - order = lookup(fields.value, "order", null) + field_path = fields.value.field_path + order = fields.value.order + array_config = fields.value.array_config } } +} + +resource "google_firestore_index" "default_db_collection_group_index" { + collection = "composite-index-test-collection" + query_scope = "COLLECTION_GROUP" + for_each = local.collection_group_indexes + dynamic "fields" { + for_each = distinct(flatten([for k, v in local.indexes : [ + for i in each.value : { + field_path = i.field_path + order = can(i.order) ? i.order : null + array_config = can(i.array_config) ? i.array_config : null + }]])) + content { + field_path = fields.value.field_path + order = fields.value.order + array_config = fields.value.array_config + } + } } -resource "google_firestore_index" "named-db-index" { +resource "google_firestore_index" "named_db_index" { collection = "composite-index-test-collection" database = "test-db" @@ -30,12 +51,35 @@ resource "google_firestore_index" "named-db-index" { dynamic "fields" { for_each = distinct(flatten([for k, v in local.indexes : [ for i in each.value : { - field_path = i.field_path - order = i.order + field_path = i.field_path + order = can(i.order) ? i.order : null + array_config = can(i.array_config) ? i.array_config : null + }]])) + content { + field_path = fields.value.field_path + order = fields.value.order + array_config = fields.value.array_config + } + } +} + +resource "google_firestore_index" "named_db_collection_group_index" { + collection = "composite-index-test-collection" + database = "test-db" + query_scope = "COLLECTION_GROUP" + + for_each = local.collection_group_indexes + dynamic "fields" { + for_each = distinct(flatten([for k, v in local.indexes : [ + for i in each.value : { + field_path = i.field_path + order = can(i.order) ? i.order : null + array_config = can(i.array_config) ? i.array_config : null }]])) content { - field_path = lookup(fields.value, "field_path", null) - order = lookup(fields.value, "order", null) + field_path = fields.value.field_path + order = fields.value.order + array_config = fields.value.array_config } } } diff --git a/packages/firestore/test/integration/api/aggregation.test.ts b/packages/firestore/test/integration/api/aggregation.test.ts index 1316ad5c2ea..0baebc2c939 100644 --- a/packages/firestore/test/integration/api/aggregation.test.ts +++ b/packages/firestore/test/integration/api/aggregation.test.ts @@ -1385,156 +1385,4 @@ apiDescribe('Aggregation queries - sum / average', persistence => { expect(snapshot.data().countOfDocs).to.equal(4); }); }); - - // Only run tests that require indexes against the emulator, because we don't - // have a way to dynamically create the indexes when running the tests. - (USE_EMULATOR ? apiDescribe : apiDescribe.skip)( - 'queries requiring indexes', - () => { - it('aggregate query supports collection groups - multi-aggregate', () => { - return withTestDb(persistence, async db => { - const collectionGroupId = doc( - collection(db, 'aggregateQueryTest') - ).id; - const docPaths = [ - `${collectionGroupId}/cg-doc1`, - `abc/123/${collectionGroupId}/cg-doc2`, - `zzz${collectionGroupId}/cg-doc3`, - `abc/123/zzz${collectionGroupId}/cg-doc4`, - `abc/123/zzz/${collectionGroupId}` - ]; - const batch = writeBatch(db); - for (const docPath of docPaths) { - batch.set(doc(db, docPath), { x: 2 }); - } - await batch.commit(); - const snapshot = await getAggregateFromServer( - collectionGroup(db, collectionGroupId), - { - count: count(), - sum: sum('x'), - avg: average('x') - } - ); - expect(snapshot.data().count).to.equal(2); - expect(snapshot.data().sum).to.equal(4); - expect(snapshot.data().avg).to.equal(2); - }); - }); - - it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { - const testDocs = { - a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, - b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, - c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, - d: { author: 'authorD', title: 'titleD', pages: 50 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer(coll, { - totalPages: sum('pages'), - averagePages: average('pages'), - averageYear: average('year'), - count: count() - }); - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().averageYear).to.equal(2007); - expect(snapshot.data().count).to.equal(3); - }); - }); - - it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: 5 - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: 4 - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: Number.NaN - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: 0 - } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer(coll, { - totalRating: sum('rating'), - totalPages: sum('pages'), - averageYear: average('year') - }); - expect(snapshot.data().totalRating).to.be.NaN; - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averageYear).to.equal(2000); - }); - }); - - it('performs aggregates when using `array-contains-any` operator getAggregationFromServer', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: [5, 1000] - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: [4] - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: [2222, 3] - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: [0] - } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer( - query(coll, where('rating', 'array-contains-any', [5, 3])), - { - totalRating: sum('rating'), - averageRating: average('rating'), - totalPages: sum('pages'), - averagePages: average('pages'), - countOfDocs: count() - } - ); - expect(snapshot.data().totalRating).to.equal(0); - expect(snapshot.data().averageRating).to.be.null; - expect(snapshot.data().totalPages).to.equal(200); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().countOfDocs).to.equal(2); - }); - }); - } - ); }); diff --git a/packages/firestore/test/integration/api/composite_index_query.test.ts b/packages/firestore/test/integration/api/composite_index_query.test.ts index 19264c52adb..da70ac1ece7 100644 --- a/packages/firestore/test/integration/api/composite_index_query.test.ts +++ b/packages/firestore/test/integration/api/composite_index_query.test.ts @@ -15,13 +15,22 @@ * limitations under the License. */ +import { expect } from 'chai'; + import { CompositeIndexTestHelper } from '../util/composite_index_test_helper'; import { where, orderBy, limit, limitToLast, - or + or, + getAggregateFromServer, + sum, + average, + count, + doc, + writeBatch, + collectionGroup } from '../util/firebase_export'; import { apiDescribe } from '../util/helpers'; @@ -36,6 +45,10 @@ import { apiDescribe } from '../util/helpers'; * To get started, please refer to the instructions provided in the README file. This will guide you * through setting up your local testing environment and updating the Terraform configuration with * any new composite indexes required for your testing scenarios. + * + * Note: Whenever feasible, make use of the current document fields (such as 'a,' 'b,' 'author,' + * 'title') to avoid introducing new composite indexes and surpassing the limit. Refer to the + * guidelines at https://firebase.google.com/docs/firestore/quotas#indexes for further information. */ apiDescribe('Composite Index Queries', persistence => { // OR Query tests only run when the SDK's local cache is configured to use @@ -136,4 +149,157 @@ apiDescribe('Composite Index Queries', persistence => { }); }); }); + + describe('Aggregation queries - sum / average', () => { + it('aggregate query supports collection groups - multi-aggregate', () => { + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestCollection(persistence, async (coll, db) => { + const collectionGroupId = coll.id; + const docPaths = [ + `${collectionGroupId}/cg-doc1`, + `abc/123/${collectionGroupId}/cg-doc2`, + `zzz${collectionGroupId}/cg-doc3`, + `abc/123/zzz${collectionGroupId}/cg-doc4`, + `abc/123/zzz/${collectionGroupId}` + ]; + const batch = writeBatch(db); + for (const docPath of docPaths) { + // Add test specific fields to the document value + batch.set( + doc(db, docPath), + testHelper.addTestSpecificFieldsToDoc({ a: 2 }) + ); + } + await batch.commit(); + const snapshot = await getAggregateFromServer( + testHelper.query(collectionGroup(db, collectionGroupId)), + { + count: count(), + sum: sum('a'), + avg: average('a') + } + ); + expect(snapshot.data().count).to.equal(2); + expect(snapshot.data().sum).to.equal(4); + expect(snapshot.data().avg).to.equal(2); + }); + }); + + it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { + const testDocs = { + a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, + b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, + c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, + d: { author: 'authorD', title: 'titleD', pages: 50 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer(testHelper.query(coll), { + totalPages: sum('pages'), + averagePages: average('pages'), + averageYear: average('year'), + count: count() + }); + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().averageYear).to.equal(2007); + expect(snapshot.data().count).to.equal(3); + }); + }); + + it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: 5 + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: 4 + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: Number.NaN + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: 0 + } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer(testHelper.query(coll), { + totalRating: sum('rating'), + totalPages: sum('pages'), + averageYear: average('year') + }); + expect(snapshot.data().totalRating).to.be.NaN; + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averageYear).to.equal(2000); + }); + }); + + it('performs aggregates when using `array-contains-any` operator getAggregationFromServer', () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: [5, 1000] + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: [4] + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: [2222, 3] + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: [0] + } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer( + testHelper.query(coll, where('rating', 'array-contains-any', [5, 3])), + { + totalRating: sum('rating'), + averageRating: average('rating'), + totalPages: sum('pages'), + averagePages: average('pages'), + countOfDocs: count() + } + ); + expect(snapshot.data().totalRating).to.equal(0); + expect(snapshot.data().averageRating).to.be.null; + expect(snapshot.data().totalPages).to.equal(200); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().countOfDocs).to.equal(2); + }); + }); + }); }); diff --git a/packages/firestore/test/integration/util/composite_index_test_helper.ts b/packages/firestore/test/integration/util/composite_index_test_helper.ts index 1166ca5fbe6..b04bb483558 100644 --- a/packages/firestore/test/integration/util/composite_index_test_helper.ts +++ b/packages/firestore/test/integration/util/composite_index_test_helper.ts @@ -40,7 +40,10 @@ import { doc, and, _AutoId, - _FieldPath + _FieldPath, + newTestFirestore, + newTestApp, + collection } from './firebase_export'; import { batchCommitDocsToCollection, @@ -48,7 +51,12 @@ import { PERSISTENCE_MODE_UNSPECIFIED, PersistenceMode } from './helpers'; -import { COMPOSITE_INDEX_TEST_COLLECTION, DEFAULT_SETTINGS } from './settings'; +import { + COMPOSITE_INDEX_TEST_COLLECTION, + DEFAULT_PROJECT_ID, + DEFAULT_SETTINGS, + TARGET_DB_ID +} from './settings'; /** * This helper class is designed to facilitate integration testing of Firestore queries that @@ -89,6 +97,23 @@ export class CompositeIndexTestHelper { ); } + // Runs a test on COMPOSITE_INDEX_TEST_COLLECTION. + async withTestCollection( + persistence: PersistenceMode | typeof PERSISTENCE_MODE_UNSPECIFIED, + fn: (collection: CollectionReference, db: Firestore) => Promise + ): Promise { + const settings = { ...DEFAULT_SETTINGS }; + if (persistence !== PERSISTENCE_MODE_UNSPECIFIED) { + settings.localCache = persistence.asLocalCacheFirestoreSettings(); + } + const db = newTestFirestore( + newTestApp(DEFAULT_PROJECT_ID), + settings, + TARGET_DB_ID + ); + return fn(collection(db, COMPOSITE_INDEX_TEST_COLLECTION), db); + } + // Hash the document key with testId. private toHashedId(docId: string): string { return docId + '-' + this.testId; @@ -99,7 +124,7 @@ export class CompositeIndexTestHelper { } // Adds test-specific fields to a document, including the testId and expiration date. - private addTestSpecificFieldsToDoc(doc: DocumentData): DocumentData { + addTestSpecificFieldsToDoc(doc: DocumentData): DocumentData { return { ...doc, [this.TEST_ID_FIELD]: this.testId,