Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Fix the `__name__` normalization of vector indexes for Firestore standard
edition databases.
- Fixed an issue where the emulator would fail to start when using `firebase-functions` v7+ (#9401).
- Added `functions.list_functions` as a MCP tool (#9369)
- Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185)
Expand Down
33 changes: 29 additions & 4 deletions src/firestore/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,27 @@
*/
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 {
Expand All @@ -44,9 +59,19 @@
};
}

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.

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

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "options.force"

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

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "options.nonInteractive"

Check warning on line 74 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 All @@ -54,11 +79,11 @@
*/
async deploy(
options: { project: string; nonInteractive: boolean; force: boolean },
indexes: any[],

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
fieldOverrides: any[],

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
databaseId = "(default)",
): Promise<void> {
const spec = this.upgradeOldSpec({

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
indexes,
fieldOverrides,
});
Expand All @@ -66,8 +91,8 @@
this.validateSpec(spec);

// Now that the spec is validated we can safely assert these types.
const indexesToDeploy: Spec.Index[] = spec.indexes;

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .indexes on an `any` value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
const fieldOverridesToDeploy: Spec.FieldOverride[] = spec.fieldOverrides;

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .fieldOverrides on an `any` value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

const existingIndexes: types.Index[] = await this.listIndexes(options.project, databaseId);
const existingFieldOverrides: types.Field[] = await this.listFieldOverrides(
Expand Down
311 changes: 311 additions & 0 deletions src/firestore/indexes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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", () => {
Expand Down
Loading