From f127fba99115ce41480956012fcd0798de6a9745 Mon Sep 17 00:00:00 2001 From: Torben Wetter Date: Thu, 17 Jul 2025 14:22:07 +0200 Subject: [PATCH 1/6] fix(firestore): smart filtering of __name__ fields in index listings Preserve __name__ fields with DESCENDING order while filtering out implicit ASCENDING ones to fix deployment conflicts. - Fixes duplicate index issues (#7629) - Fixes deployment conflicts (#8859) - Add comprehensive test coverage Fixes #7629, #8859 --- CHANGELOG.md | 1 + src/firestore/api.ts | 17 ++- src/firestore/indexes.spec.ts | 193 ++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..f1b6990b1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- **firestore**: Fixed issue where `__name__` fields with DESCENDING order were incorrectly filtered from index listings, causing duplicate index issues (#7629) and deployment conflicts (#8859). The fix now preserves `__name__` fields with explicit DESCENDING order while filtering out implicit ASCENDING `__name__` fields. \ No newline at end of file diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 93f0612f31d..e3129931b56 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -183,7 +183,22 @@ export class FirestoreApi { return []; } - return indexes; + return indexes.map((index: any): types.Index => { + // Filter out implicit __name__ fields with ASCENDING order, keep explicit DESCENDING ones. + const fields = index.fields.filter((field: types.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === types.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); } /** diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 7e4f99661b8..1b45cdf8197 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -447,6 +447,199 @@ describe("IndexSpecMatching", () => { }); }); +describe("IndexListingWithNameFields", () => { + it("should filter out __name__ fields with ASCENDING order", () => { + const mockIndexes = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: "READY", + }, + ]; + + const result = mockIndexes.map((index: any): API.Index => { + const fields = index.fields.filter((field: API.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === API.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + + expect(result[0].fields).to.have.length(1); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + }); + + it("should keep __name__ fields with DESCENDING order", () => { + const mockIndexes = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "DESCENDING" }, + ], + state: "READY", + }, + ]; + + const result = mockIndexes.map((index: any): API.Index => { + const fields = index.fields.filter((field: API.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === API.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + + expect(result[0].fields).to.have.length(2); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + expect(result[0].fields[1].fieldPath).to.equal("__name__"); + expect(result[0].fields[1].order).to.equal("DESCENDING"); + }); + + it("should distinguish between indexes that differ only by __name__ order", () => { + const mockIndexes = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: "READY", + }, + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/def456", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "DESCENDING" }, + ], + state: "READY", + }, + ]; + + const result = mockIndexes.map((index: any): API.Index => { + const fields = index.fields.filter((field: API.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === API.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + + // First index should have __name__ field filtered out + expect(result[0].fields).to.have.length(1); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + + // Second index should keep __name__ field with DESCENDING order + expect(result[1].fields).to.have.length(2); + expect(result[1].fields[0].fieldPath).to.equal("foo"); + expect(result[1].fields[1].fieldPath).to.equal("__name__"); + expect(result[1].fields[1].order).to.equal("DESCENDING"); + + // The two processed indexes should be different (fixing the duplicate issue) + expect(JSON.stringify(result[0].fields)).to.not.equal(JSON.stringify(result[1].fields)); + }); + + it("should keep all non-__name__ fields regardless of order", () => { + const mockIndexes = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", order: "DESCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: "READY", + }, + ]; + + const result = mockIndexes.map((index: any): API.Index => { + const fields = index.fields.filter((field: API.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === API.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + + expect(result[0].fields).to.have.length(2); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + expect(result[0].fields[0].order).to.equal("ASCENDING"); + expect(result[0].fields[1].fieldPath).to.equal("bar"); + expect(result[0].fields[1].order).to.equal("DESCENDING"); + }); + + it("should handle indexes with no __name__ fields", () => { + const mockIndexes = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + state: "READY", + }, + ]; + + const result = mockIndexes.map((index: any): API.Index => { + const fields = index.fields.filter((field: API.IndexField) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === API.Order.DESCENDING; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + + expect(result[0].fields).to.have.length(2); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + expect(result[0].fields[1].fieldPath).to.equal("bar"); + }); +}); + describe("IndexSorting", () => { it("should be able to handle empty arrays", () => { expect(([] as Spec.Index[]).sort(sort.compareSpecIndex)).to.eql([]); From 5290d418008a5036630ea4d876cca95ab30a36a3 Mon Sep 17 00:00:00 2001 From: Torben Wetter Date: Thu, 17 Jul 2025 15:04:14 +0200 Subject: [PATCH 2/6] refactor(firestore): extract index filtering logic to static method Refactored __name__ field filtering from listIndexes into static method FirestoreApi.processIndexes() to ensure tests verify production code rather than duplicating the implementation. --- src/firestore/api.ts | 39 +++++---- src/firestore/indexes.spec.ts | 148 +++++++++------------------------- 2 files changed, 62 insertions(+), 125 deletions(-) diff --git a/src/firestore/api.ts b/src/firestore/api.ts index e3129931b56..02c8964291e 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -18,6 +18,28 @@ export class FirestoreApi { apiClient = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" }); printer = new PrettyPrint(); + /** + * Process indexes by filtering out implicit __name__ fields with ASCENDING order. + * Keeps explicit __name__ fields with DESCENDING order. + * @param indexes Array of indexes to process + * @returns Processed array of indexes with filtered fields + */ + public static processIndexes(indexes: types.Index[]): types.Index[] { + return indexes.map((index: types.Index): types.Index => { + const fields = index.fields.filter((field) => { + if (field.fieldPath !== "__name__") { + return true; + } + return field.order === types.Order.DESCENDING; + }); + + return { + ...index, + fields, + }; + }); + } + /** * Deploy an index specification to the specified project. * @param options the CLI options. @@ -183,22 +205,7 @@ export class FirestoreApi { return []; } - return indexes.map((index: any): types.Index => { - // Filter out implicit __name__ fields with ASCENDING order, keep explicit DESCENDING ones. - const fields = index.fields.filter((field: types.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === types.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + return FirestoreApi.processIndexes(indexes); } /** diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 1b45cdf8197..0c0a7ca7533 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -449,110 +449,68 @@ describe("IndexSpecMatching", () => { describe("IndexListingWithNameFields", () => { it("should filter out __name__ fields with ASCENDING order", () => { - const mockIndexes = [ + const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "__name__", order: "ASCENDING" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, ], - state: "READY", + state: API.State.READY, }, ]; - const result = mockIndexes.map((index: any): API.Index => { - const fields = index.fields.filter((field: API.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === API.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + const result = FirestoreApi.processIndexes(mockIndexes); expect(result[0].fields).to.have.length(1); expect(result[0].fields[0].fieldPath).to.equal("foo"); }); it("should keep __name__ fields with DESCENDING order", () => { - const mockIndexes = [ + const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "__name__", order: "DESCENDING" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, ], - state: "READY", + state: API.State.READY, }, ]; - const result = mockIndexes.map((index: any): API.Index => { - const fields = index.fields.filter((field: API.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === API.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + const result = FirestoreApi.processIndexes(mockIndexes); expect(result[0].fields).to.have.length(2); expect(result[0].fields[0].fieldPath).to.equal("foo"); expect(result[0].fields[1].fieldPath).to.equal("__name__"); - expect(result[0].fields[1].order).to.equal("DESCENDING"); + expect(result[0].fields[1].order).to.equal(API.Order.DESCENDING); }); it("should distinguish between indexes that differ only by __name__ order", () => { - const mockIndexes = [ + const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "__name__", order: "ASCENDING" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, ], - state: "READY", + state: API.State.READY, }, { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/def456", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "__name__", order: "DESCENDING" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, ], - state: "READY", + state: API.State.READY, }, ]; - const result = mockIndexes.map((index: any): API.Index => { - const fields = index.fields.filter((field: API.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === API.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + const result = FirestoreApi.processIndexes(mockIndexes); // First index should have __name__ field filtered out expect(result[0].fields).to.have.length(1); @@ -562,77 +520,49 @@ describe("IndexListingWithNameFields", () => { expect(result[1].fields).to.have.length(2); expect(result[1].fields[0].fieldPath).to.equal("foo"); expect(result[1].fields[1].fieldPath).to.equal("__name__"); - expect(result[1].fields[1].order).to.equal("DESCENDING"); + expect(result[1].fields[1].order).to.equal(API.Order.DESCENDING); // The two processed indexes should be different (fixing the duplicate issue) expect(JSON.stringify(result[0].fields)).to.not.equal(JSON.stringify(result[1].fields)); }); it("should keep all non-__name__ fields regardless of order", () => { - const mockIndexes = [ + const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", order: "DESCENDING" }, - { fieldPath: "__name__", order: "ASCENDING" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", order: API.Order.DESCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, ], - state: "READY", + state: API.State.READY, }, ]; - const result = mockIndexes.map((index: any): API.Index => { - const fields = index.fields.filter((field: API.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === API.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + const result = FirestoreApi.processIndexes(mockIndexes); expect(result[0].fields).to.have.length(2); expect(result[0].fields[0].fieldPath).to.equal("foo"); - expect(result[0].fields[0].order).to.equal("ASCENDING"); + expect(result[0].fields[0].order).to.equal(API.Order.ASCENDING); expect(result[0].fields[1].fieldPath).to.equal("bar"); - expect(result[0].fields[1].order).to.equal("DESCENDING"); + expect(result[0].fields[1].order).to.equal(API.Order.DESCENDING); }); it("should handle indexes with no __name__ fields", () => { - const mockIndexes = [ + const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", + queryScope: API.QueryScope.COLLECTION, fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", arrayConfig: "CONTAINS" }, + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS }, ], - state: "READY", + state: API.State.READY, }, ]; - const result = mockIndexes.map((index: any): API.Index => { - const fields = index.fields.filter((field: API.IndexField) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === API.Order.DESCENDING; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - }); + const result = FirestoreApi.processIndexes(mockIndexes); expect(result[0].fields).to.have.length(2); expect(result[0].fields[0].fieldPath).to.equal("foo"); From fce1b9091b8bbf57b1d69f6952b27eaa8ad73846 Mon Sep 17 00:00:00 2001 From: Torben Wetter Date: Thu, 17 Jul 2025 15:10:30 +0200 Subject: [PATCH 3/6] refactor(firestore): simplify processIndexes filter logic Simplified the filter expression in processIndexes() to a single boolean expression as suggested in PR feedback for better readability and conciseness. --- src/firestore/api.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 02c8964291e..8b98826d53a 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -26,12 +26,9 @@ export class FirestoreApi { */ public static processIndexes(indexes: types.Index[]): types.Index[] { return indexes.map((index: types.Index): types.Index => { - const fields = index.fields.filter((field) => { - if (field.fieldPath !== "__name__") { - return true; - } - return field.order === types.Order.DESCENDING; - }); + const fields = index.fields.filter( + (field) => field.fieldPath !== "__name__" || field.order === types.Order.DESCENDING, + ); return { ...index, From 7a21aaaf8a85daf483feceab3cc73062642dfa25 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 17 Jul 2025 09:07:37 -0700 Subject: [PATCH 4/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b6990b1aa..c63c3cca8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- **firestore**: Fixed issue where `__name__` fields with DESCENDING order were incorrectly filtered from index listings, causing duplicate index issues (#7629) and deployment conflicts (#8859). The fix now preserves `__name__` fields with explicit DESCENDING order while filtering out implicit ASCENDING `__name__` fields. \ No newline at end of file +- Fixed issue where `__name__` fields with DESCENDING order were incorrectly filtered from index listings, causing duplicate index issues (#7629) and deployment conflicts (#8859). The fix now preserves `__name__` fields with explicit DESCENDING order while filtering out implicit ASCENDING `__name__` fields. \ No newline at end of file From bd6fadce5559350850f5dc971465d72c303616f8 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 17 Jul 2025 11:21:53 -0700 Subject: [PATCH 5/6] Only filters __name__ when order matches order of last non name field and it is the last field --- CHANGELOG.md | 2 +- src/firestore/api.ts | 14 +++++--- src/firestore/indexes.spec.ts | 67 ++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c63c3cca8df..0a1081dcaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Fixed issue where `__name__` fields with DESCENDING order were incorrectly filtered from index listings, causing duplicate index issues (#7629) and deployment conflicts (#8859). The fix now preserves `__name__` fields with explicit DESCENDING order while filtering out implicit ASCENDING `__name__` fields. \ No newline at end of file +- Fixed issue where `__name__` fields with DESCENDING order were incorrectly filtered from index listings, causing duplicate index issues (#7629) and deployment conflicts (#8859). The fix now preserves `__name__` fields with explicit DESCENDING order while filtering out implicit ASCENDING `__name__` fields. diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 8b98826d53a..d3985a12326 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -26,10 +26,16 @@ export class FirestoreApi { */ public static processIndexes(indexes: types.Index[]): types.Index[] { return indexes.map((index: types.Index): types.Index => { - const fields = index.fields.filter( - (field) => field.fieldPath !== "__name__" || field.order === types.Order.DESCENDING, - ); - + // Per https://firebase.google.com/docs/firestore/query-data/index-overview#default_ordering_and_the_name_field + // this matches the direction of the last non-name field in the index. + let fields = index.fields; + const lastField = index.fields[index.fields.length - 1]; + if (lastField.fieldPath === "__name__") { + const defaultDirection = index.fields[index.fields.length - 2]?.order; + if (lastField.order === defaultDirection) { + fields = fields.slice(0, -1); + } + } return { ...index, fields, diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 0c0a7ca7533..129989fe739 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -448,7 +448,7 @@ describe("IndexSpecMatching", () => { }); describe("IndexListingWithNameFields", () => { - it("should filter out __name__ fields with ASCENDING order", () => { + it("should filter out __name__ fields with in the default order, when the default is ASCENDING", () => { const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", @@ -467,7 +467,26 @@ describe("IndexListingWithNameFields", () => { expect(result[0].fields[0].fieldPath).to.equal("foo"); }); - it("should keep __name__ fields with DESCENDING order", () => { + it("should filter out __name__ fields with in the default order, when the default is DESCENDING", () => { + const mockIndexes: API.Index[] = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.DESCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + ], + state: API.State.READY, + }, + ]; + + const result = FirestoreApi.processIndexes(mockIndexes); + + expect(result[0].fields).to.have.length(1); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + }); + + it("should keep __name__ fields with DESCENDING order, when the default is ASCENDING", () => { const mockIndexes: API.Index[] = [ { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", @@ -488,6 +507,27 @@ describe("IndexListingWithNameFields", () => { expect(result[0].fields[1].order).to.equal(API.Order.DESCENDING); }); + it("should keep __name__ fields with ASCENDING order, when the default is DESCENDING", () => { + const mockIndexes: API.Index[] = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.DESCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + state: API.State.READY, + }, + ]; + + const result = FirestoreApi.processIndexes(mockIndexes); + + expect(result[0].fields).to.have.length(2); + expect(result[0].fields[0].fieldPath).to.equal("foo"); + expect(result[0].fields[1].fieldPath).to.equal("__name__"); + expect(result[0].fields[1].order).to.equal(API.Order.ASCENDING); + }); + it("should distinguish between indexes that differ only by __name__ order", () => { const mockIndexes: API.Index[] = [ { @@ -526,29 +566,6 @@ describe("IndexListingWithNameFields", () => { expect(JSON.stringify(result[0].fields)).to.not.equal(JSON.stringify(result[1].fields)); }); - it("should keep all non-__name__ fields regardless of order", () => { - const mockIndexes: API.Index[] = [ - { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { fieldPath: "foo", order: API.Order.ASCENDING }, - { fieldPath: "bar", order: API.Order.DESCENDING }, - { fieldPath: "__name__", order: API.Order.ASCENDING }, - ], - state: API.State.READY, - }, - ]; - - const result = FirestoreApi.processIndexes(mockIndexes); - - expect(result[0].fields).to.have.length(2); - expect(result[0].fields[0].fieldPath).to.equal("foo"); - expect(result[0].fields[0].order).to.equal(API.Order.ASCENDING); - expect(result[0].fields[1].fieldPath).to.equal("bar"); - expect(result[0].fields[1].order).to.equal(API.Order.DESCENDING); - }); - it("should handle indexes with no __name__ fields", () => { const mockIndexes: API.Index[] = [ { From 42fdcfff6bf3ad0e2eb7147c05a0f08be4b971e1 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 17 Jul 2025 11:32:44 -0700 Subject: [PATCH 6/6] Super defensive --- src/firestore/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/firestore/api.ts b/src/firestore/api.ts index d3985a12326..1e8b147a41f 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -29,10 +29,10 @@ export class FirestoreApi { // Per https://firebase.google.com/docs/firestore/query-data/index-overview#default_ordering_and_the_name_field // this matches the direction of the last non-name field in the index. let fields = index.fields; - const lastField = index.fields[index.fields.length - 1]; - if (lastField.fieldPath === "__name__") { - const defaultDirection = index.fields[index.fields.length - 2]?.order; - if (lastField.order === defaultDirection) { + const lastField = index.fields?.[index.fields.length - 1]; + if (lastField?.fieldPath === "__name__") { + const defaultDirection = index.fields?.[index.fields.length - 2]?.order; + if (lastField?.order === defaultDirection) { fields = fields.slice(0, -1); } }