Skip to content

feat: Firestore Enterprise edition indexes #8939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 12, 2025
123 changes: 120 additions & 3 deletions src/firestore/api-sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
undefined,
];

const API_SCOPE_SEQUENCE = [
API.ApiScope.ANY_API,
API.ApiScope.DATASTORE_MODE_API,
API.ApiScope.MONGODB_COMPATIBLE_API,
undefined,
];

const DENSITY_SEQUENCE = [
API.Density.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];
Expand All @@ -20,6 +35,10 @@
* 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) {
Expand All @@ -30,7 +49,27 @@
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);
}

/**
Expand All @@ -40,6 +79,10 @@
* 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
Expand All @@ -57,7 +100,27 @@
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);
}

/**
Expand Down Expand Up @@ -88,8 +151,8 @@
*/
export function compareApiBackup(a: Backup, b: Backup): number {
// the location is embedded in the name (projects/myproject/locations/mylocation/backups/mybackup)
const aLocation = a.name!.split("/")[3];

Check warning on line 154 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const bLocation = b.name!.split("/")[3];

Check warning on line 155 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
if (aLocation && bLocation && aLocation !== bLocation) {
return aLocation > bLocation ? 1 : -1;
}
Expand All @@ -99,7 +162,7 @@
}

// Name should always be unique and present
return a.name! > b.name! ? 1 : -1;

Check warning on line 165 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion

Check warning on line 165 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
}

/**
Expand All @@ -115,7 +178,7 @@
}

// Name should always be unique and present
return a.name! > b.name! ? 1 : -1;

Check warning on line 181 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion

Check warning on line 181 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
}

/**
Expand Down Expand Up @@ -215,17 +278,71 @@
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 {
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);
}

function compareOrder(a?: API.Order, b?: API.Order): number {
return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b);
}

function compareBoolean(a?: boolean, b?: boolean) {

Check warning on line 333 in src/firestore/api-sort.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (a === b) {
return 0;
}
if (a === undefined) {
return -1;
}
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);
}
Expand Down
8 changes: 8 additions & 0 deletions src/firestore/api-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/firestore/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_COMPATIBLE_API = "MONGODB_COMPATIBLE_API",
}

export enum Density {
DENSITY_UNSPECIFIED = "DENSITY_UNSPECIFIED",
SPARSE_ALL = "SPARSE_ALL",
SPARSE_ANY = "SPARSE_ANY",
DENSE = "DENSE",
}

export enum Order {
ASCENDING = "ASCENDING",
DESCENDING = "DESCENDING",
Expand Down Expand Up @@ -49,6 +62,10 @@ export interface Index {
queryScope: QueryScope;
fields: IndexField[];
state?: State;
apiScope?: ApiScope;
density?: Density;
multikey?: boolean;
unique?: boolean;
}

/**
Expand Down
88 changes: 86 additions & 2 deletions src/firestore/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* 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

Check warning on line 25 in src/firestore/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid JSDoc tag (preference). Replace "returns" JSDoc tag with "return"
*/
public static processIndexes(indexes: types.Index[]): types.Index[] {
return indexes.map((index: types.Index): types.Index => {
Expand Down Expand Up @@ -244,6 +244,10 @@
collectionGroup: util.parseIndexName(index.name).collectionGroupId,
queryScope: index.queryScope,
fields: index.fields,
apiScope: index.apiScope,
density: index.density,
multikey: index.multikey,
unique: index.unique,
};
});

Expand All @@ -266,6 +270,10 @@
order: firstField.order,
arrayConfig: firstField.arrayConfig,
queryScope: index.queryScope,
apiScope: index.apiScope,
density: index.density,
multikey: index.multikey,
unique: index.unique,
};
}),
};
Expand Down Expand Up @@ -305,6 +313,26 @@
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) => {
Expand Down Expand Up @@ -353,6 +381,22 @@
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");
}
});
}

Expand All @@ -373,6 +417,10 @@
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,
Expand Down Expand Up @@ -420,6 +468,10 @@
return this.apiClient.post(url, {
fields: index.fields,
queryScope: index.queryScope,
apiScope: index.apiScope,
density: index.density,
multikey: index.multikey,
unique: index.unique,
});
}

Expand All @@ -444,6 +496,22 @@
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;
}
Expand All @@ -465,6 +533,10 @@
return false;
}

if (iField.vectorConfig !== sField.vectorConfig) {
return false;
}

i++;
}

Expand Down Expand Up @@ -549,12 +621,24 @@
}

result.indexes = spec.indexes.map((index: any) => {
const i = {
const i: any = {
collectionGroup: index.collectionGroup || index.collectionId,
queryScope: index.queryScope || types.QueryScope.COLLECTION,
fields: [],
};

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 = {
Expand Down
Loading
Loading