Skip to content

Commit 9c61e81

Browse files
[Multi-Tab] Adding Schema Migration (#485)
1 parent 4159289 commit 9c61e81

File tree

4 files changed

+262
-48
lines changed

4 files changed

+262
-48
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ import { AutoId } from '../util/misc';
2525
import { IndexedDbMutationQueue } from './indexeddb_mutation_queue';
2626
import { IndexedDbQueryCache } from './indexeddb_query_cache';
2727
import { IndexedDbRemoteDocumentCache } from './indexeddb_remote_document_cache';
28-
import { ALL_STORES, DbOwner, DbOwnerKey } from './indexeddb_schema';
29-
import { createOrUpgradeDb, SCHEMA_VERSION } from './indexeddb_schema';
28+
import {
29+
ALL_STORES,
30+
createOrUpgradeDb,
31+
DbOwner,
32+
DbOwnerKey,
33+
SCHEMA_VERSION
34+
} from './indexeddb_schema';
3035
import { LocalSerializer } from './local_serializer';
3136
import { MutationQueue } from './mutation_queue';
3237
import { Persistence } from './persistence';

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 158 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,53 +22,48 @@ import { assert } from '../util/assert';
2222

2323
import { encode, EncodedResourcePath } from './encoded_resource_path';
2424

25-
export const SCHEMA_VERSION = 1;
26-
27-
/** Performs database creation and (in the future) upgrades between versions. */
28-
export function createOrUpgradeDb(db: IDBDatabase, oldVersion: number): void {
29-
assert(oldVersion === 0, 'Unexpected upgrade from version ' + oldVersion);
30-
31-
db.createObjectStore(DbMutationQueue.store, {
32-
keyPath: DbMutationQueue.keyPath
33-
});
34-
35-
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their
36-
// types. https://github.com/Microsoft/TypeScript/issues/14322
37-
db.createObjectStore(
38-
DbMutationBatch.store,
39-
// tslint:disable-next-line:no-any
40-
{ keyPath: DbMutationBatch.keyPath as any }
41-
);
25+
/**
26+
* Schema Version for the Web client (containing the Mutation Queue, the Query
27+
* and the Remote Document Cache) and Multi-Tab Support.
28+
*/
29+
export const SCHEMA_VERSION = 2;
4230

43-
const targetDocumentsStore = db.createObjectStore(
44-
DbTargetDocument.store,
45-
// tslint:disable-next-line:no-any
46-
{ keyPath: DbTargetDocument.keyPath as any }
47-
);
48-
targetDocumentsStore.createIndex(
49-
DbTargetDocument.documentTargetsIndex,
50-
DbTargetDocument.documentTargetsKeyPath,
51-
{ unique: true }
31+
/**
32+
* Performs database creation and schema upgrades.
33+
*
34+
* Note that in production, this method is only ever used to upgrade the schema
35+
* to SCHEMA_VERSION. Different versions are only used for testing and
36+
* local feature development.
37+
*/
38+
export function createOrUpgradeDb(
39+
db: IDBDatabase,
40+
fromVersion: number,
41+
toVersion: number
42+
): void {
43+
// This function currently supports migrating to schema version 1 (Mutation
44+
// Queue, Query and Remote Document Cache) and schema version 2 (Multi-Tab).
45+
assert(
46+
fromVersion < toVersion && fromVersion >= 0 && toVersion <= 2,
47+
'Unexpected schema upgrade from v${fromVersion} to v{toVersion}.'
5248
);
5349

54-
const targetStore = db.createObjectStore(DbTarget.store, {
55-
keyPath: DbTarget.keyPath
56-
});
57-
// NOTE: This is unique only because the TargetId is the suffix.
58-
targetStore.createIndex(
59-
DbTarget.queryTargetsIndexName,
60-
DbTarget.queryTargetsKeyPath,
61-
{ unique: true }
62-
);
50+
if (fromVersion < 1 && toVersion >= 1) {
51+
createOwnerStore(db);
52+
createMutationQueue(db);
53+
createQueryCache(db);
54+
createRemoteDocumentCache(db);
55+
}
6356

64-
// NOTE: keys for these stores are specified explicitly rather than using a
65-
// keyPath.
66-
db.createObjectStore(DbDocumentMutation.store);
67-
db.createObjectStore(DbRemoteDocument.store);
68-
db.createObjectStore(DbOwner.store);
69-
db.createObjectStore(DbTargetGlobal.store);
57+
if (fromVersion < 2 && toVersion >= 2) {
58+
createClientMetadataStore(db);
59+
createTargetChangeStore(db);
60+
}
7061
}
7162

63+
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their types.
64+
// https://github.com/Microsoft/TypeScript/issues/14322
65+
type KeyPath = any; // tslint:disable-line:no-any
66+
7267
/**
7368
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects.
7469
*/
@@ -94,6 +89,10 @@ export class DbOwner {
9489
constructor(public ownerId: string, public leaseTimestampMs: number) {}
9590
}
9691

92+
function createOwnerStore(db: IDBDatabase): void {
93+
db.createObjectStore(DbOwner.store);
94+
}
95+
9796
/** Object keys in the 'mutationQueues' store are userId strings. */
9897
export type DbMutationQueueKey = string;
9998

@@ -183,6 +182,18 @@ export class DbMutationBatch {
183182
*/
184183
export type DbDocumentMutationKey = [string, EncodedResourcePath, BatchId];
185184

185+
function createMutationQueue(db: IDBDatabase): void {
186+
db.createObjectStore(DbMutationQueue.store, {
187+
keyPath: DbMutationQueue.keyPath
188+
});
189+
190+
db.createObjectStore(DbMutationBatch.store, {
191+
keyPath: DbMutationBatch.keyPath as KeyPath
192+
});
193+
194+
db.createObjectStore(DbDocumentMutation.store);
195+
}
196+
186197
/**
187198
* An object to be stored in the 'documentMutations' store in IndexedDb.
188199
*
@@ -241,6 +252,10 @@ export class DbDocumentMutation {
241252
*/
242253
export type DbRemoteDocumentKey = string[];
243254

255+
function createRemoteDocumentCache(db: IDBDatabase): void {
256+
db.createObjectStore(DbRemoteDocument.store);
257+
}
258+
244259
/**
245260
* Represents the known absence of a document at a particular version.
246261
* Stored in IndexedDb as part of a DbRemoteDocument object.
@@ -455,11 +470,101 @@ export class DbTargetGlobal {
455470
) {}
456471
}
457472

473+
function createQueryCache(db: IDBDatabase): void {
474+
const targetDocumentsStore = db.createObjectStore(DbTargetDocument.store, {
475+
keyPath: DbTargetDocument.keyPath as KeyPath
476+
});
477+
targetDocumentsStore.createIndex(
478+
DbTargetDocument.documentTargetsIndex,
479+
DbTargetDocument.documentTargetsKeyPath,
480+
{ unique: true }
481+
);
482+
483+
const targetStore = db.createObjectStore(DbTarget.store, {
484+
keyPath: DbTarget.keyPath
485+
});
486+
487+
// NOTE: This is unique only because the TargetId is the suffix.
488+
targetStore.createIndex(
489+
DbTarget.queryTargetsIndexName,
490+
DbTarget.queryTargetsKeyPath,
491+
{ unique: true }
492+
);
493+
db.createObjectStore(DbTargetGlobal.store);
494+
}
495+
496+
/**
497+
* An object representing the changes at a particular snapshot version for the
498+
* given target. This is used to facilitate storing query changelogs in the
499+
* targetChanges object store.
500+
*
501+
* PORTING NOTE: This is used for change propagation during multi-tab syncing
502+
* and not needed on iOS and Android.
503+
*/
504+
export class DbTargetChange {
505+
/** Name of the IndexedDb object store. */
506+
static store = 'targetChanges';
507+
508+
/** Keys are automatically assigned via the targetId and snapshotVersion. */
509+
static keyPath = ['targetId', 'snapshotVersion'];
510+
511+
constructor(
512+
/**
513+
* The targetId identifying a target.
514+
*/
515+
public targetId: TargetId,
516+
/**
517+
* The snapshot version for this change.
518+
*/
519+
public snapshotVersion: DbTimestamp,
520+
/**
521+
* The keys of the changed documents in this snapshot.
522+
*/
523+
public changes: {
524+
added?: EncodedResourcePath[];
525+
modified?: EncodedResourcePath[];
526+
removed?: EncodedResourcePath[];
527+
}
528+
) {}
529+
}
530+
531+
function createTargetChangeStore(db: IDBDatabase): void {
532+
db.createObjectStore(DbTargetChange.store, {
533+
keyPath: DbTargetChange.keyPath as KeyPath
534+
});
535+
}
536+
458537
/**
459-
* The list of all IndexedDB stored used by the SDK. This is used when creating
460-
* transactions so that access across all stores is done atomically.
538+
* A record of the metadata state of each client.
539+
*
540+
* PORTING NOTE: This is used to synchronize multi-tab state and does not need
541+
* to be ported to iOS or Android.
461542
*/
462-
export const ALL_STORES = [
543+
export class DbClientMetadata {
544+
/** Name of the IndexedDb object store. */
545+
static store = 'clientMetadata';
546+
547+
/** Keys are automatically assigned via the clientKey properties. */
548+
static keyPath = ['clientKey'];
549+
550+
constructor(
551+
/** The auto-generated client key assigned at client startup. */
552+
public clientKey: string,
553+
/** The last time this state was updated. */
554+
public updateTimeMs: DbTimestamp,
555+
/** Whether this client is running in a foreground tab. */
556+
public inForeground: boolean
557+
) {}
558+
}
559+
560+
function createClientMetadataStore(db: IDBDatabase): void {
561+
db.createObjectStore(DbClientMetadata.store, {
562+
keyPath: DbClientMetadata.keyPath as KeyPath
563+
});
564+
}
565+
566+
// Visible for testing
567+
export const V1_STORES = [
463568
DbMutationQueue.store,
464569
DbMutationBatch.store,
465570
DbDocumentMutation.store,
@@ -469,3 +574,12 @@ export const ALL_STORES = [
469574
DbTargetGlobal.store,
470575
DbTargetDocument.store
471576
];
577+
578+
const V2_STORES = [DbClientMetadata.store, DbTargetChange.store];
579+
580+
/**
581+
* The list of all default IndexedDB stores used throughout the SDK. This is
582+
* used when creating transactions so that access across all stores is done
583+
* atomically.
584+
*/
585+
export const ALL_STORES = [...V1_STORES, ...V2_STORES];

packages/firestore/src/local/simple_db.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { debug } from '../util/log';
1919
import { AnyDuringMigration } from '../util/misc';
2020

2121
import { PersistencePromise } from './persistence_promise';
22+
import { SCHEMA_VERSION } from './indexeddb_schema';
2223

2324
const LOG_TAG = 'SimpleDb';
2425

@@ -34,7 +35,11 @@ export class SimpleDb {
3435
static openOrCreate(
3536
name: string,
3637
version: number,
37-
runUpgrade: (db: IDBDatabase, oldVersion: number) => void
38+
runUpgrade: (
39+
db: IDBDatabase,
40+
fromVersion: number,
41+
toVersion: number
42+
) => void
3843
): Promise<SimpleDb> {
3944
assert(
4045
SimpleDb.isAvailable(),
@@ -70,7 +75,7 @@ export class SimpleDb {
7075
// cheating and just passing the raw IndexedDB in, since
7176
// createObjectStore(), etc. are synchronous.
7277
const db = (event.target as IDBOpenDBRequest).result;
73-
runUpgrade(db, event.oldVersion);
78+
runUpgrade(db, event.oldVersion, SCHEMA_VERSION);
7479
};
7580
}).toPromise();
7681
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { expect } from 'chai';
18+
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
19+
import {
20+
ALL_STORES,
21+
createOrUpgradeDb,
22+
V1_STORES
23+
} from '../../../src/local/indexeddb_schema';
24+
import { Deferred } from '../../../src/util/promise';
25+
import { SimpleDb } from '../../../src/local/simple_db';
26+
27+
const INDEXEDDB_TEST_DATABASE = 'schemaTest';
28+
29+
function withDb(schemaVersion, fn: (db: IDBDatabase) => void): Promise<void> {
30+
return new Promise<IDBDatabase>((resolve, reject) => {
31+
const request = window.indexedDB.open(
32+
INDEXEDDB_TEST_DATABASE,
33+
schemaVersion
34+
);
35+
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
36+
const db = (event.target as IDBOpenDBRequest).result;
37+
createOrUpgradeDb(db, event.oldVersion, schemaVersion);
38+
};
39+
request.onsuccess = (event: Event) => {
40+
resolve((event.target as IDBOpenDBRequest).result);
41+
};
42+
request.onerror = (event: ErrorEvent) => {
43+
reject((event.target as IDBOpenDBRequest).error);
44+
};
45+
}).then(db => {
46+
fn(db);
47+
db.close();
48+
});
49+
}
50+
51+
function getAllObjectStores(db: IDBDatabase): String[] {
52+
const objectStores: String[] = [];
53+
for (let i = 0; i < db.objectStoreNames.length; ++i) {
54+
objectStores.push(db.objectStoreNames.item(i));
55+
}
56+
objectStores.sort();
57+
return objectStores;
58+
}
59+
60+
describe('IndexedDbSchema: createOrUpgradeDb', () => {
61+
if (!IndexedDbPersistence.isAvailable()) {
62+
console.warn('No IndexedDB. Skipping createOrUpgradeDb() tests.');
63+
return;
64+
}
65+
66+
beforeEach(() => SimpleDb.delete(INDEXEDDB_TEST_DATABASE));
67+
68+
it('can install schema version 1', () => {
69+
return withDb(1, db => {
70+
expect(db.version).to.be.equal(1);
71+
expect(getAllObjectStores(db)).to.have.members(V1_STORES);
72+
});
73+
});
74+
75+
it('can install schema version 2', () => {
76+
return withDb(2, db => {
77+
expect(db.version).to.be.equal(2);
78+
expect(getAllObjectStores(db)).to.have.members(ALL_STORES);
79+
});
80+
});
81+
82+
it('can upgrade from schema version 1 to 2', () => {
83+
return withDb(1, () => {}).then(() =>
84+
withDb(2, db => {
85+
expect(db.version).to.be.equal(2);
86+
expect(getAllObjectStores(db)).to.have.members(ALL_STORES);
87+
})
88+
);
89+
});
90+
});

0 commit comments

Comments
 (0)