From dd84568ad8425252850b529150a2cde8159679d6 Mon Sep 17 00:00:00 2001 From: Ruoxuan Guo Date: Wed, 29 Oct 2025 20:44:30 +0000 Subject: [PATCH 1/3] fix the __name__ normalization for vector index --- src/firestore/api.ts | 33 +++- src/firestore/indexes.spec.ts | 311 ++++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+), 4 deletions(-) diff --git a/src/firestore/api.ts b/src/firestore/api.ts index c3d47a0e823..f255fae31a1 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -30,12 +30,27 @@ export class FirestoreApi { */ public static processIndex(index: Spec.Index): Spec.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. - const fields = index.fields; + // this matches the direction of the last non-name non-vector field in the index. + let fields = index.fields; + const suffixOrder: types.Order = FirestoreApi.lastIndexFieldOrder(fields); + const nameSuffix = { fieldPath: "__name__", order: suffixOrder } as types.IndexField; + const lastField = index.fields?.[index.fields.length - 1]; + if (lastField.vectorConfig) { + // lastField is vector field, refer to the second from last field + const vectorField = lastField; + fields = fields.slice(0, -1); + + if (fields.length === 0 || fields?.[fields.length - 1].fieldPath !== "__name__") { + fields.push(nameSuffix); + } + fields.push(vectorField); + return { + ...index, + fields, + }; + } if (lastField?.fieldPath !== "__name__") { - const defaultDirection = index.fields?.[index.fields.length - 1]?.order; - const nameSuffix = { fieldPath: "__name__", order: defaultDirection } as types.IndexField; fields.push(nameSuffix); } return { @@ -44,6 +59,16 @@ export class FirestoreApi { }; } + public static lastIndexFieldOrder(fields: types.IndexField[]): types.Order { + let lastIndexFieldOrder: types.Order = types.Order.ASCENDING; + for (const field of fields) { + if (field.order) { + lastIndexFieldOrder = field.order; + } + } + return lastIndexFieldOrder; + } + /** * Deploy an index specification to the specified project. * @param options the CLI options. diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 9ae6466cf7c..dbe50c40111 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -909,6 +909,78 @@ describe("Normalize __name__ field for database indexes", () => { expect(result.fields[1].order).to.equal(API.Order.ASCENDING); }); + it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(2); + expect(result.fields[0].fieldPath).to.equal("arr"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[2].fieldPath).to.equal("vector"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(4); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[3].fieldPath).to.equal("vector"); + expect(result.fields[2].order).to.equal(API.Order.ASCENDING); + }); + it("No-op if exists __name__ field as the last field with non-default sort order", () => { const mockSpecIndex = { collectionGroup: "collection", @@ -927,6 +999,78 @@ describe("Normalize __name__ field for database indexes", () => { expect(result.fields[1].order).to.equal(API.Order.DESCENDING); }); + it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(2); + expect(result.fields[0].fieldPath).to.equal("arr"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[2].fieldPath).to.equal("vector"); + expect(result.fields[1].order).to.equal(API.Order.DESCENDING); + }); + + it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(4); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[3].fieldPath).to.equal("vector"); + expect(result.fields[2].order).to.equal(API.Order.DESCENDING); + }); + it("should attach __name__ suffix with the default order if not exists, ascending", () => { const mockSpecIndex = { collectionGroup: "collection", @@ -942,6 +1086,102 @@ describe("Normalize __name__ field for database indexes", () => { expect(result.fields[1].order).to.equal(API.Order.ASCENDING); }); + it("should attach __name__ suffix with the default order if not exists, ascending, with array contains", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[2].order).to.equal(API.Order.ASCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[2].fieldPath).to.equal("vector"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, with array contains and vector field, default to ascending", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("arr"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[2].fieldPath).to.equal("vector"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, with index field with ascending order, array contains and vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(4); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[3].fieldPath).to.equal("vector"); + expect(result.fields[2].order).to.equal(API.Order.ASCENDING); + }); + it("should attach __name__ suffix with the default order if not exists, descending", () => { const mockSpecIndex = { collectionGroup: "collection", @@ -960,6 +1200,77 @@ describe("Normalize __name__ field for database indexes", () => { expect(result.fields[2].fieldPath).to.equal("__name__"); expect(result.fields[2].order).to.equal(API.Order.DESCENDING); }); + + it("should attach __name__ suffix with the default order if not exists, descending, with array contains", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.DESCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[2].order).to.equal(API.Order.DESCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.DESCENDING }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[2].fieldPath).to.equal("vector"); + expect(result.fields[1].order).to.equal(API.Order.DESCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, with index field with descending order, array contains and vector field", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.DESCENDING }, + { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS }, + { + fieldPath: "vector", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(4); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("arr"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[3].fieldPath).to.equal("vector"); + expect(result.fields[2].order).to.equal(API.Order.DESCENDING); + }); }); describe("IndexSorting", () => { From 7ea3c617c9e57700ae3747c72f431ae939436fe0 Mon Sep 17 00:00:00 2001 From: Ruoxuan Guo Date: Wed, 29 Oct 2025 21:15:55 +0000 Subject: [PATCH 2/3] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee3d69a119..b92c8cd9f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- Fix the __name__ normalization of vector indexes for Firestore standard + edition databases. - Added `functions.list_functions` as a MCP tool (#9369) - Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185) - Improved error messages for Firebase AI Logic provisioning during 'firebase init' (#9377) From 70007ea3e8542075c4c907146db7d082142f4e35 Mon Sep 17 00:00:00 2001 From: Ruoxuan Guo Date: Wed, 29 Oct 2025 21:30:42 +0000 Subject: [PATCH 3/3] fix lint of CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b92c8cd9f2a..6140bd9c587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -- Fix the __name__ normalization of vector indexes for Firestore standard +- Fix the `__name__` normalization of vector indexes for Firestore standard edition databases. - Added `functions.list_functions` as a MCP tool (#9369) - Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185)