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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [Added] Support for Firestore Enterprise database index configurations. (#8939)
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): number {
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
84 changes: 82 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 All @@ -45,7 +45,7 @@

/**
* Deploy an index specification to the specified project.
* @param options the CLI options.

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

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "options.project"
* @param indexes an array of objects, each will be validated and then converted
* to an {@link Spec.Index}.
* @param fieldOverrides an array of objects, each will be validated and then
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,22 @@
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");
}

validator.assertHas(index, "fields");

index.fields.forEach((field: any) => {
Expand Down Expand Up @@ -353,6 +377,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 +413,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 +464,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 +492,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 +529,10 @@
return false;
}

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

i++;
}

Expand Down Expand Up @@ -549,12 +617,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