diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..c63c3cca8df 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. \ No newline at end of file diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 93f0612f31d..8b98826d53a 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -18,6 +18,25 @@ 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) => field.fieldPath !== "__name__" || field.order === types.Order.DESCENDING, + ); + + return { + ...index, + fields, + }; + }); + } + /** * Deploy an index specification to the specified project. * @param options the CLI options. @@ -183,7 +202,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..0c0a7ca7533 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -447,6 +447,129 @@ describe("IndexSpecMatching", () => { }); }); +describe("IndexListingWithNameFields", () => { + it("should filter out __name__ fields with ASCENDING 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, + }, + ]; + + 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: 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 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 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[] = [ + { + 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([]);