From 310c6d401f0fd06551b61665e296e0bf9f6690cd Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 1 Aug 2025 10:55:15 -0700 Subject: [PATCH 1/8] WIP: Add new Firestore index configurations. --- src/firestore/api-sort.ts | 105 +++++++++++++++++- src/firestore/api-spec.ts | 8 ++ src/firestore/api-types.ts | 17 +++ src/firestore/api.ts | 8 ++ .../init/firestore/firestore.indexes.json | 11 ++ 5 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/firestore/api-sort.ts b/src/firestore/api-sort.ts index 3991a831749..5672703aa52 100644 --- a/src/firestore/api-sort.ts +++ b/src/firestore/api-sort.ts @@ -9,6 +9,21 @@ const QUERY_SCOPE_SEQUENCE = [ undefined, ]; +const API_SCOPE_SEQUENCE = [ + API.ApiScope.ANY_API, + API.ApiScope.DATASTORE_MODE_API, + API.ApiScope.MONGODB_COMPAT_API, + undefined, +]; + +const DENSITY_SEQUENCE = [ + API.Density.UNSPECIFIED, + API.Density.SPARSE_ALL, + API.Density.SPARSE_ANY, + API.Density.DENSE, + undefined, +]; + const ORDER_SEQUENCE = [API.Order.ASCENDING, API.Order.DESCENDING, undefined]; const ARRAY_CONFIG_SEQUENCE = [API.ArrayConfig.CONTAINS, undefined]; @@ -20,6 +35,10 @@ const ARRAY_CONFIG_SEQUENCE = [API.ArrayConfig.CONTAINS, undefined]; * 1) The collection group. * 2) The query scope. * 3) The fields list. + * 4) The API scope. + * 5) The index density. + * 6) Whether it's multikey. + * 7) Whether it's unique. */ export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { if (a.collectionGroup !== b.collectionGroup) { @@ -30,7 +49,27 @@ export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { return compareQueryScope(a.queryScope, b.queryScope); } - return compareArrays(a.fields, b.fields, compareIndexField); + let cmp = compareArrays(a.fields, b.fields, compareIndexField); + if (cmp !== 0) { + return cmp; + } + + cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multiKey, b.multiKey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); } /** @@ -40,6 +79,10 @@ export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { * 1) The collection group. * 2) The query scope. * 3) The fields list. + * 4) The API scope. + * 5) The index density. + * 6) Whether it's multikey. + * 7) Whether it's unique. */ export function compareApiIndex(a: API.Index, b: API.Index): number { // When these indexes are used as part of a field override, the name is @@ -57,7 +100,27 @@ export function compareApiIndex(a: API.Index, b: API.Index): number { return compareQueryScope(a.queryScope, b.queryScope); } - return compareArrays(a.fields, b.fields, compareIndexField); + let cmp = compareArrays(a.fields, b.fields, compareIndexField); + if (cmp !== 0) { + return cmp; + } + + cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multiKey, b.multiKey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); } /** @@ -215,17 +278,53 @@ function compareFieldIndex(a: Spec.FieldIndex, b: Spec.FieldIndex): number { return compareArrayConfig(a.arrayConfig, b.arrayConfig); } - return 0; + let cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multiKey, b.multiKey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); } function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number { return QUERY_SCOPE_SEQUENCE.indexOf(a) - QUERY_SCOPE_SEQUENCE.indexOf(b); } +function compareApiScope(a?: API.ApiScope, b?: API.ApiScope): number { + return API_SCOPE_SEQUENCE.indexOf(a) - API_SCOPE_SEQUENCE.indexOf(b); +} + +function compareDensity(a?: API.Density, b?: API.Density): number { + return DENSITY_SEQUENCE.indexOf(a) - DENSITY_SEQUENCE.indexOf(b); +} + function compareOrder(a?: API.Order, b?: API.Order): number { return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b); } +function compareBoolean(a?: boolean, b?: boolean) { + if (a === undefined) { + if (b === undefined) { + return 0; + } else { + return 1; + } + } else if (b === undefined) { + return -1; + } + return Number(a) - Number(b); +} + function compareArrayConfig(a?: API.ArrayConfig, b?: API.ArrayConfig): number { return ARRAY_CONFIG_SEQUENCE.indexOf(a) - ARRAY_CONFIG_SEQUENCE.indexOf(b); } diff --git a/src/firestore/api-spec.ts b/src/firestore/api-spec.ts index da491b8438a..c77a370df0b 100644 --- a/src/firestore/api-spec.ts +++ b/src/firestore/api-spec.ts @@ -13,6 +13,10 @@ export interface Index { collectionGroup: string; queryScope: API.QueryScope; fields: API.IndexField[]; + apiScope?: API.ApiScope; + density?: API.Density; + multiKey?: boolean; + unique?: boolean; } /** @@ -32,6 +36,10 @@ export interface FieldIndex { queryScope: API.QueryScope; order?: API.Order; arrayConfig?: API.ArrayConfig; + apiScope?: API.ApiScope; + density?: API.Density; + multiKey?: boolean; + unique?: boolean; } /** diff --git a/src/firestore/api-types.ts b/src/firestore/api-types.ts index 701d7e4097c..aeffb78ded9 100644 --- a/src/firestore/api-types.ts +++ b/src/firestore/api-types.ts @@ -15,6 +15,19 @@ export enum QueryScope { COLLECTION_GROUP = "COLLECTION_GROUP", } +export enum ApiScope { + ANY_API = "ANY_API", + DATASTORE_MODE_API = "DATASTORE_MODE_API", + MONGODB_COMPAT_API = "MONGODB_COMPAT_API", +} + +export enum Density { + UNSPECIFIED = "UNSPECIFIED", + SPARSE_ALL = "SPARSE_ALL", + SPARSE_ANY = "SPARSE_ANY", + DENSE = "DENSE", +} + export enum Order { ASCENDING = "ASCENDING", DESCENDING = "DESCENDING", @@ -49,6 +62,10 @@ export interface Index { queryScope: QueryScope; fields: IndexField[]; state?: State; + apiScope?: ApiScope; + density?: Density; + multiKey?: boolean; + unique?: boolean; } /** diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 1e8b147a41f..45b998350d0 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -244,6 +244,10 @@ export class FirestoreApi { collectionGroup: util.parseIndexName(index.name).collectionGroupId, queryScope: index.queryScope, fields: index.fields, + apiScope: index.apiScope, + density: index.density, + multiKey: index.multiKey, + unique: index.unique, }; }); @@ -266,6 +270,10 @@ export class FirestoreApi { order: firstField.order, arrayConfig: firstField.arrayConfig, queryScope: index.queryScope, + apiScope: index.apiScope, + density: index.density, + multiKey: index.multiKey, + unique: index.unique, }; }), }; diff --git a/templates/init/firestore/firestore.indexes.json b/templates/init/firestore/firestore.indexes.json index 4ff4fab5c7c..74c16d3beae 100644 --- a/templates/init/firestore/firestore.indexes.json +++ b/templates/init/firestore/firestore.indexes.json @@ -10,6 +10,17 @@ // { "fieldPath": "bar", "mode": "DESCENDING" } // ] // }, + // { + // "collectionGroup": "reviews", + // "queryScope": "COLLECTION_GROUP", + // "apiScope": "MONGODB_COMPAT_API", + // "density": "SPARSE_ANY", + // "multikey": false, + // "unique": false, + // "fields": [ + // { "fieldPath": "baz", "mode": "ASCENDING" } + // ] + // }, // // "fieldOverrides": [ // { From ae4ff9a5d8f550ec94877ae8c69bc262d0f08fec Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 1 Aug 2025 16:49:29 -0700 Subject: [PATCH 2/8] WIP: Continue working on index configuration flow. --- src/firestore/api-sort.ts | 8 ++++---- src/firestore/api-spec.ts | 4 ++-- src/firestore/api-types.ts | 6 +++--- src/firestore/api.ts | 18 +++++++++++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/firestore/api-sort.ts b/src/firestore/api-sort.ts index 5672703aa52..2890a07faf7 100644 --- a/src/firestore/api-sort.ts +++ b/src/firestore/api-sort.ts @@ -12,7 +12,7 @@ const QUERY_SCOPE_SEQUENCE = [ const API_SCOPE_SEQUENCE = [ API.ApiScope.ANY_API, API.ApiScope.DATASTORE_MODE_API, - API.ApiScope.MONGODB_COMPAT_API, + API.ApiScope.MONGODB_COMPATIBLE_API, undefined, ]; @@ -64,7 +64,7 @@ export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { return cmp; } - cmp = compareBoolean(a.multiKey, b.multiKey); + cmp = compareBoolean(a.multikey, b.multikey); if (cmp !== 0) { return cmp; } @@ -115,7 +115,7 @@ export function compareApiIndex(a: API.Index, b: API.Index): number { return cmp; } - cmp = compareBoolean(a.multiKey, b.multiKey); + cmp = compareBoolean(a.multikey, b.multikey); if (cmp !== 0) { return cmp; } @@ -288,7 +288,7 @@ function compareFieldIndex(a: Spec.FieldIndex, b: Spec.FieldIndex): number { return cmp; } - cmp = compareBoolean(a.multiKey, b.multiKey); + cmp = compareBoolean(a.multikey, b.multikey); if (cmp !== 0) { return cmp; } diff --git a/src/firestore/api-spec.ts b/src/firestore/api-spec.ts index c77a370df0b..c3ed1f52984 100644 --- a/src/firestore/api-spec.ts +++ b/src/firestore/api-spec.ts @@ -15,7 +15,7 @@ export interface Index { fields: API.IndexField[]; apiScope?: API.ApiScope; density?: API.Density; - multiKey?: boolean; + multikey?: boolean; unique?: boolean; } @@ -38,7 +38,7 @@ export interface FieldIndex { arrayConfig?: API.ArrayConfig; apiScope?: API.ApiScope; density?: API.Density; - multiKey?: boolean; + multikey?: boolean; unique?: boolean; } diff --git a/src/firestore/api-types.ts b/src/firestore/api-types.ts index aeffb78ded9..be899a70c2f 100644 --- a/src/firestore/api-types.ts +++ b/src/firestore/api-types.ts @@ -18,11 +18,11 @@ export enum QueryScope { export enum ApiScope { ANY_API = "ANY_API", DATASTORE_MODE_API = "DATASTORE_MODE_API", - MONGODB_COMPAT_API = "MONGODB_COMPAT_API", + MONGODB_COMPATIBLE_API = "MONGODB_COMPATIBLE_API", } export enum Density { - UNSPECIFIED = "UNSPECIFIED", + UNSPECIFIED = "DENSITY_UNSPECIFIED", SPARSE_ALL = "SPARSE_ALL", SPARSE_ANY = "SPARSE_ANY", DENSE = "DENSE", @@ -64,7 +64,7 @@ export interface Index { state?: State; apiScope?: ApiScope; density?: Density; - multiKey?: boolean; + multikey?: boolean; unique?: boolean; } diff --git a/src/firestore/api.ts b/src/firestore/api.ts index 45b998350d0..fe0b57014af 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -246,7 +246,7 @@ export class FirestoreApi { fields: index.fields, apiScope: index.apiScope, density: index.density, - multiKey: index.multiKey, + multikey: index.multikey, unique: index.unique, }; }); @@ -272,7 +272,7 @@ export class FirestoreApi { queryScope: index.queryScope, apiScope: index.apiScope, density: index.density, - multiKey: index.multiKey, + multikey: index.multikey, unique: index.unique, }; }), @@ -381,6 +381,10 @@ export class FirestoreApi { const indexes = spec.indexes.map((index) => { return { queryScope: index.queryScope, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, fields: [ { fieldPath: spec.fieldPath, @@ -428,6 +432,10 @@ export class FirestoreApi { return this.apiClient.post(url, { fields: index.fields, queryScope: index.queryScope, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, }); } @@ -557,10 +565,14 @@ export class FirestoreApi { } result.indexes = spec.indexes.map((index: any) => { - const i = { + const i: Spec.Index = { collectionGroup: index.collectionGroup || index.collectionId, queryScope: index.queryScope || types.QueryScope.COLLECTION, fields: [], + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, }; if (index.fields) { From c78ea5cb8acc36d12503cd074fb9e01180a8cc1e Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Mon, 4 Aug 2025 13:59:02 -0700 Subject: [PATCH 3/8] Fix firestore.indexes.json template example. --- .../init/firestore/firestore.indexes.json | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/templates/init/firestore/firestore.indexes.json b/templates/init/firestore/firestore.indexes.json index 74c16d3beae..1e26ade496c 100644 --- a/templates/init/firestore/firestore.indexes.json +++ b/templates/init/firestore/firestore.indexes.json @@ -1,5 +1,5 @@ { - // Example: + // Example (Standard Edition): // // "indexes": [ // { @@ -10,17 +10,6 @@ // { "fieldPath": "bar", "mode": "DESCENDING" } // ] // }, - // { - // "collectionGroup": "reviews", - // "queryScope": "COLLECTION_GROUP", - // "apiScope": "MONGODB_COMPAT_API", - // "density": "SPARSE_ANY", - // "multikey": false, - // "unique": false, - // "fields": [ - // { "fieldPath": "baz", "mode": "ASCENDING" } - // ] - // }, // // "fieldOverrides": [ // { @@ -32,6 +21,33 @@ // }, // ] // ] + // + // Example (Enterprise Edition): + // + // "indexes": [ + // { + // "collectionGroup": "reviews", + // "queryScope": "COLLECTION_GROUP", + // "apiScope": "MONGODB_COMPATIBLE_API", + // "density": "DENSE", + // "multikey": false, + // "unique": false, + // "fields": [ + // { "fieldPath": "baz", "mode": "ASCENDING" } + // ] + // }, + // { + // "collectionGroup": "items", + // "queryScope": "COLLECTION_GROUP", + // "apiScope": "MONGODB_COMPATIBLE_API", + // "density": "SPARSE_ANY", + // "multikey": true, + // "unique": false, + // "fields": [ + // { "fieldPath": "baz", "mode": "ASCENDING" } + // ] + // }, + // ] "indexes": [], "fieldOverrides": [] } \ No newline at end of file From 703a46fc9c140a04406d85a3acab67cdf1f16e66 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Mon, 4 Aug 2025 17:34:44 -0700 Subject: [PATCH 4/8] minor fixes and adding tests. --- src/firestore/api-sort.ts | 32 ++- src/firestore/api-types.ts | 2 +- src/firestore/api.ts | 76 ++++++- src/firestore/indexes.spec.ts | 383 ++++++++++++++++++++++++++++++++++ src/firestore/validator.ts | 2 +- 5 files changed, 480 insertions(+), 15 deletions(-) diff --git a/src/firestore/api-sort.ts b/src/firestore/api-sort.ts index 2890a07faf7..37f91867a93 100644 --- a/src/firestore/api-sort.ts +++ b/src/firestore/api-sort.ts @@ -17,7 +17,7 @@ const API_SCOPE_SEQUENCE = [ ]; const DENSITY_SEQUENCE = [ - API.Density.UNSPECIFIED, + API.Density.DENSITY_UNSPECIFIED, API.Density.SPARSE_ALL, API.Density.SPARSE_ANY, API.Density.DENSE, @@ -301,10 +301,28 @@ function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number { } function compareApiScope(a?: API.ApiScope, b?: API.ApiScope): number { + if (a === b) { + return 0; + } + if (a === undefined) { + return -1; + } + if (b === undefined) { + return 1; + } return API_SCOPE_SEQUENCE.indexOf(a) - API_SCOPE_SEQUENCE.indexOf(b); } function compareDensity(a?: API.Density, b?: API.Density): number { + if (a === b) { + return 0; + } + if (a === undefined) { + return -1; + } + if (b === undefined) { + return 1; + } return DENSITY_SEQUENCE.indexOf(a) - DENSITY_SEQUENCE.indexOf(b); } @@ -313,15 +331,15 @@ function compareOrder(a?: API.Order, b?: API.Order): number { } function compareBoolean(a?: boolean, b?: boolean) { + if (a === b) { + return 0; + } if (a === undefined) { - if (b === undefined) { - return 0; - } else { - return 1; - } - } else if (b === undefined) { return -1; } + if (b === undefined) { + return 1; + } return Number(a) - Number(b); } diff --git a/src/firestore/api-types.ts b/src/firestore/api-types.ts index be899a70c2f..ec662016c46 100644 --- a/src/firestore/api-types.ts +++ b/src/firestore/api-types.ts @@ -22,7 +22,7 @@ export enum ApiScope { } export enum Density { - UNSPECIFIED = "DENSITY_UNSPECIFIED", + DENSITY_UNSPECIFIED = "DENSITY_UNSPECIFIED", SPARSE_ALL = "SPARSE_ALL", SPARSE_ANY = "SPARSE_ANY", DENSE = "DENSE", diff --git a/src/firestore/api.ts b/src/firestore/api.ts index fe0b57014af..a3152f95c07 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -313,6 +313,26 @@ export class FirestoreApi { validator.assertHas(index, "queryScope"); validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); + if (index.apiScope) { + validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); + } + + if (index.density) { + validator.assertEnum(index, "density", Object.keys(types.Density)); + } + + if (index.multikey) { + validator.assertType("multikey", index.multikey, "boolean"); + } + + if (index.unique) { + validator.assertType("unique", index.unique, "boolean"); + } + + if (index.density) { + validator.assertEnum(index, "density", Object.keys(types.Density)); + } + validator.assertHas(index, "fields"); index.fields.forEach((field: any) => { @@ -361,6 +381,22 @@ export class FirestoreApi { if (index.queryScope) { validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); } + + if (index.apiScope) { + validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); + } + + if (index.density) { + validator.assertEnum(index, "density", Object.keys(types.Density)); + } + + if (index.multikey) { + validator.assertType("multikey", index.multikey, "boolean"); + } + + if (index.unique) { + validator.assertType("unique", index.unique, "boolean"); + } }); } @@ -460,6 +496,22 @@ export class FirestoreApi { return false; } + if (index.apiScope !== spec.apiScope) { + return false; + } + + if (index.density !== spec.density) { + return false; + } + + if (index.multikey !== spec.multikey) { + return false; + } + + if (index.unique !== spec.unique) { + return false; + } + if (index.fields.length !== spec.fields.length) { return false; } @@ -481,6 +533,10 @@ export class FirestoreApi { return false; } + if (iField.vectorConfig !== sField.vectorConfig) { + return false; + } + i++; } @@ -565,16 +621,24 @@ export class FirestoreApi { } result.indexes = spec.indexes.map((index: any) => { - const i: Spec.Index = { + const i: any = { collectionGroup: index.collectionGroup || index.collectionId, queryScope: index.queryScope || types.QueryScope.COLLECTION, - fields: [], - apiScope: index.apiScope, - density: index.density, - multikey: index.multikey, - unique: index.unique, }; + if (index.apiScope) { + i.apiScope = index.apiScope; + } + if (index.density) { + i.density = index.density; + } + if (index.multikey !== undefined) { + i.multikey = index.multikey; + } + if (index.unique !== undefined) { + i.unique = index.unique; + } + if (index.fields) { i.fields = index.fields.map((field: any) => { const f: any = { diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts index 129989fe739..637b36d77dd 100644 --- a/src/firestore/indexes.spec.ts +++ b/src/firestore/indexes.spec.ts @@ -36,6 +36,103 @@ describe("IndexValidation", () => { idx.validateSpec(VALID_SPEC); }); + it("should accept a valid index spec with apiScope, density, multikey, and unique", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + unique: true, + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", order: "DESCENDING" }, + { fieldPath: "baz", arrayConfig: "CONTAINS" }, + ], + }, + ], + }; + idx.validateSpec(spec); + }); + + it("should reject an index spec with invalid apiScope", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "UNKNOWN", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw( + FirebaseError, + /Field "apiScope" must be one of ANY_API, DATASTORE_MODE_API, MONGODB_COMPATIBLE_API/, + ); + }); + + it("should reject an index spec with invalid density", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "UNKNOWN", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw( + FirebaseError, + /Field "density" must be one of DENSITY_UNSPECIFIED, SPARSE_ALL, SPARSE_ANY, DENSE/, + ); + }); + + it("should reject an index spec with invalid multikey", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: "multikey", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw(FirebaseError, /Property "multikey" must be of type boolean/); + }); + + it("should reject an index spec with invalid unique", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + unique: "true", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw(FirebaseError, /Property "unique" must be of type boolean/); + }); + it("should not change a valid v1beta2 index spec after upgrade", () => { const upgraded = idx.upgradeOldSpec(VALID_SPEC); expect(upgraded).to.eql(VALID_SPEC); @@ -229,6 +326,177 @@ describe("IndexSpecMatching", () => { expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(true); }); + it("should identify a positive index spec match with apiScope, density, multikey, and unique", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + apiScope: API.ApiScope.ANY_API, + density: API.Density.DENSE, + multikey: true, + unique: true, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + state: API.State.READY, + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + unique: true, + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(true); + }); + + it("should identify a negative index spec match with different apiScope", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + apiScope: API.ApiScope.ANY_API, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "DATASTORE_MODE_API", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with missing apiScope", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "DATASTORE_MODE_API", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with different density", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + density: API.Density.DENSE, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + density: "SPARSE_ALL", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with missing density", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + density: API.Density.DENSE, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with different multikey", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + multikey: true, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + multikey: false, + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with missing multikey", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + multikey: false, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with different unique", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + unique: true, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + unique: false, + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a negative index spec match with missing unique", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + unique: false, + fields: [], + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + it("should identify a negative index spec match", () => { const apiIndex = { name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", @@ -661,6 +929,121 @@ describe("IndexSorting", () => { expect([b, a, e, d, c].sort(sort.compareSpecIndex)).to.eql([a, b, c, d, e]); }); + it("should correctly sort an array of Spec indexes with apiScope, density, multikey, and unique", () => { + const a: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.ANY_API, + }; + + const b: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.ANY_API, + }; + + const c: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.DATASTORE_MODE_API, + }; + + const d: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + }; + + const e: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSITY_UNSPECIFIED, + }; + + const f: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.SPARSE_ALL, + }; + + const g: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.SPARSE_ANY, + }; + + const h: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + }; + + const i: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: false, + }; + + const j: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + }; + + const k: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + unique: false, + }; + + const l: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + unique: true, + }; + + expect([l, k, j, i, h, g, f, e, d, c, b, a].sort(sort.compareSpecIndex)).to.eql([ + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + ]); + }); + it("should correcty sort an array of Spec field overrides", () => { // Sorts first because of collectionGroup const a: Spec.FieldOverride = { diff --git a/src/firestore/validator.ts b/src/firestore/validator.ts index 87a3169c141..b3d4a7a43a2 100644 --- a/src/firestore/validator.ts +++ b/src/firestore/validator.ts @@ -36,7 +36,7 @@ export function assertHasOneOf(obj: any, props: string[]): void { export function assertEnum(obj: any, prop: string, valid: any[]): void { const objString = clc.cyan(JSON.stringify(obj)); if (valid.indexOf(obj[prop]) < 0) { - throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); + throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); } } From 661a767e3684b63acb0de1d3a1f9427909dd95cb Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Thu, 7 Aug 2025 10:02:04 -0700 Subject: [PATCH 5/8] Remove the `unique` from the example since it's not implemented yet. --- templates/init/firestore/firestore.indexes.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/init/firestore/firestore.indexes.json b/templates/init/firestore/firestore.indexes.json index 1e26ade496c..0e6de7cb808 100644 --- a/templates/init/firestore/firestore.indexes.json +++ b/templates/init/firestore/firestore.indexes.json @@ -31,7 +31,6 @@ // "apiScope": "MONGODB_COMPATIBLE_API", // "density": "DENSE", // "multikey": false, - // "unique": false, // "fields": [ // { "fieldPath": "baz", "mode": "ASCENDING" } // ] @@ -42,7 +41,6 @@ // "apiScope": "MONGODB_COMPATIBLE_API", // "density": "SPARSE_ANY", // "multikey": true, - // "unique": false, // "fields": [ // { "fieldPath": "baz", "mode": "ASCENDING" } // ] From ac4abbc3dfc2ded5b04234663e440704aa013129 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 8 Aug 2025 15:17:28 -0700 Subject: [PATCH 6/8] address feedback. --- src/firestore/api-sort.ts | 2 +- src/firestore/api.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/firestore/api-sort.ts b/src/firestore/api-sort.ts index 37f91867a93..e4df69762f7 100644 --- a/src/firestore/api-sort.ts +++ b/src/firestore/api-sort.ts @@ -330,7 +330,7 @@ function compareOrder(a?: API.Order, b?: API.Order): number { return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b); } -function compareBoolean(a?: boolean, b?: boolean) { +function compareBoolean(a?: boolean, b?: boolean): number { if (a === b) { return 0; } diff --git a/src/firestore/api.ts b/src/firestore/api.ts index a3152f95c07..13564878a44 100644 --- a/src/firestore/api.ts +++ b/src/firestore/api.ts @@ -329,10 +329,6 @@ export class FirestoreApi { validator.assertType("unique", index.unique, "boolean"); } - if (index.density) { - validator.assertEnum(index, "density", Object.keys(types.Density)); - } - validator.assertHas(index, "fields"); index.fields.forEach((field: any) => { From 5024c1c87e738de78ca5824d0f11397bedcd4f0a Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 8 Aug 2025 16:06:03 -0700 Subject: [PATCH 7/8] Add changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..f2e4dce0f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- [Added] Support for Firestore Enterprise database index configurations. (#8939) \ No newline at end of file From 7f07caf4565cfb78aed15458f851679ba03eea6c Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 8 Aug 2025 16:18:52 -0700 Subject: [PATCH 8/8] prettier. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e4dce0f76..8a25fa7c0fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- [Added] Support for Firestore Enterprise database index configurations. (#8939) \ No newline at end of file +- [Added] Support for Firestore Enterprise database index configurations. (#8939)