Skip to content

Commit ee10ddc

Browse files
authored
fix: index-spec matching for optional values and vectorConfig. (#9003)
* fix: index-spec matching for optional values and vectorConfig. The few new optional values for indexes were added recently via #8939. When comparing the local index with the server index, the default value of optional index configurations should be taken into account depending on the database edition (standard or enterprise). I also found that the matching logic for vectorConfig was not working correctly, so I fixed that as well. Fixes #8859. * Fix typos. * Address feedback. * Address feedback. * add CHANGELOG
1 parent 3a663a1 commit ee10ddc

File tree

5 files changed

+288
-132
lines changed

5 files changed

+288
-132
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- Fixed a bug when deploying firestore indexes failed due to broken index comparison logic (#8859)
12
- Added prefix support for multi-instance Cloud Functions extension parameters. (#8911)
23
- Fixed a bug when `firebase deploy --only dataconnect` doesn't include GQL in nested folders (#8981)
34
- Make it possible to init a dataconnect project in non interactive mode (#8993)

src/firestore/api.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as utils from "../utils";
55
import * as validator from "./validator";
66

77
import * as types from "./api-types";
8+
import { DatabaseEdition, Density } from "./api-types";
89
import * as Spec from "./api-spec";
910
import * as sort from "./api-sort";
1011
import * as util from "./util";
@@ -13,6 +14,7 @@ import { firestoreOrigin } from "../api";
1314
import { FirebaseError } from "../error";
1415
import { Client } from "../apiv2";
1516
import { PrettyPrint } from "./pretty-print";
17+
import { optionalValueMatches } from "../functional";
1618

1719
export class FirestoreApi {
1820
apiClient = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" });
@@ -74,8 +76,10 @@ export class FirestoreApi {
7476
databaseId,
7577
);
7678

79+
const database = await this.getDatabase(options.project, databaseId);
80+
const edition = database.databaseEdition ?? DatabaseEdition.STANDARD;
7781
const indexesToDelete = existingIndexes.filter((index) => {
78-
return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec));
82+
return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec, edition));
7983
});
8084

8185
// We only want to delete fields where there is nothing in the local file with the same
@@ -127,7 +131,7 @@ export class FirestoreApi {
127131
}
128132

129133
for (const index of indexesToDeploy) {
130-
const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index));
134+
const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index, edition));
131135
if (exists) {
132136
logger.debug(`Skipping existing index: ${JSON.stringify(index)}`);
133137
} else {
@@ -325,8 +329,11 @@ export class FirestoreApi {
325329
validator.assertType("multikey", index.multikey, "boolean");
326330
}
327331

328-
if (index.unique) {
332+
if (index.unique !== undefined) {
329333
validator.assertType("unique", index.unique, "boolean");
334+
// TODO(b/439901837): Remove this check and update indexMatchesSpec once
335+
// unique index configuration is supported.
336+
throw new FirebaseError("The `unique` index configuration is not supported yet.");
330337
}
331338

332339
validator.assertHas(index, "fields");
@@ -479,10 +486,51 @@ export class FirestoreApi {
479486
return this.apiClient.delete(`/${url}`);
480487
}
481488

489+
/**
490+
* Returns true if the given ApiScope values match.
491+
* If either one is undefined, the default value is used for comparison.
492+
* @param lhs the first ApiScope value.
493+
* @param rhs the second ApiScope value.
494+
*/
495+
optionalApiScopeMatches(
496+
lhs: types.ApiScope | undefined,
497+
rhs: types.ApiScope | undefined,
498+
): boolean {
499+
return optionalValueMatches<types.ApiScope>(lhs, rhs, types.ApiScope.ANY_API);
500+
}
501+
502+
/**
503+
* Returns true if the given Density values match.
504+
* If either one is undefined, the default value is used for comparison based on Database Edition.
505+
* @param lhs the first Density value.
506+
* @param rhs the second Density value.
507+
* @param edition the database edition used to determine the default value.
508+
*/
509+
optionalDensityMatches(
510+
lhs: Density | undefined,
511+
rhs: Density | undefined,
512+
edition: types.DatabaseEdition,
513+
): boolean {
514+
const defaultValue =
515+
edition === DatabaseEdition.STANDARD ? types.Density.SPARSE_ALL : types.Density.DENSE;
516+
return optionalValueMatches<types.Density>(lhs, rhs, defaultValue);
517+
}
518+
519+
/**
520+
* Returns true if the given Multikey values match.
521+
* If either one is undefined, the default value is used for comparison.
522+
* @param lhs the first Multikey value.
523+
* @param rhs the second Multikey value.
524+
*/
525+
optionalMultikeyMatches(lhs: boolean | undefined, rhs: boolean | undefined): boolean {
526+
const defaultValue = false;
527+
return optionalValueMatches<boolean>(lhs, rhs, defaultValue);
528+
}
529+
482530
/**
483531
* Determine if an API Index and a Spec Index are functionally equivalent.
484532
*/
485-
indexMatchesSpec(index: types.Index, spec: Spec.Index): boolean {
533+
indexMatchesSpec(index: types.Index, spec: Spec.Index, edition: types.DatabaseEdition): boolean {
486534
const collection = util.parseIndexName(index.name).collectionGroupId;
487535
if (collection !== spec.collectionGroup) {
488536
return false;
@@ -492,21 +540,24 @@ export class FirestoreApi {
492540
return false;
493541
}
494542

495-
if (index.apiScope !== spec.apiScope) {
543+
// apiScope is an optional value and may be missing in firestore.indexes.json,
544+
// and may also be missing from the server value (when default is picked).
545+
if (!this.optionalApiScopeMatches(index.apiScope, spec.apiScope)) {
496546
return false;
497547
}
498548

499-
if (index.density !== spec.density) {
549+
// density is an optional value and may be missing in firestore.indexes.json,
550+
// and may also be missing from the server value (when default is picked).
551+
if (!this.optionalDensityMatches(index.density, spec.density, edition)) {
500552
return false;
501553
}
502-
503-
if (index.multikey !== spec.multikey) {
554+
// multikey is an optional value and may be missing in firestore.indexes.json,
555+
// and may also be missing from the server value (when default is picked).
556+
if (!this.optionalMultikeyMatches(index.multikey, spec.multikey)) {
504557
return false;
505558
}
506559

507-
if (index.unique !== spec.unique) {
508-
return false;
509-
}
560+
// TODO(b/439901837): Compare `unique` index configuration when it's supported.
510561

511562
if (index.fields.length !== spec.fields.length) {
512563
return false;
@@ -529,7 +580,8 @@ export class FirestoreApi {
529580
return false;
530581
}
531582

532-
if (iField.vectorConfig !== sField.vectorConfig) {
583+
// Note: vectorConfig is an object, and using '!==' should not be used.
584+
if (!utils.deepEqual(iField.vectorConfig, sField.vectorConfig)) {
533585
return false;
534586
}
535587

0 commit comments

Comments
 (0)