diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..0a1081dcaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +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. diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 93f0612f31d..1e8b147a41f 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -18,6 +18,31 @@ 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 => { + // 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, + }; + }); + } + /** * Deploy an index specification to the specified project. * @param options the CLI options. @@ -183,7 +208,7 @@ export class FirestoreApi { return []; } - return indexes; + return FirestoreApi.processIndexes(indexes); } /** diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 7e4f99661b8..129989fe739 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -447,6 +447,146 @@ describe("IndexSpecMatching", () => { }); }); +describe("IndexListingWithNameFields", () => { + 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", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + 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 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", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + ], + 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.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[] = [ + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + state: API.State.READY, + }, + { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/def456", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + ], + state: API.State.READY, + }, + ]; + + const result = FirestoreApi.processIndexes(mockIndexes); + + // 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(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 handle indexes with no __name__ fields", () => { + 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", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + 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("bar"); + }); +}); + describe("IndexSorting", () => { it("should be able to handle empty arrays", () => { expect(([] as Spec.Index[]).sort(sort.compareSpecIndex)).to.eql([]);