-
Notifications
You must be signed in to change notification settings - Fork 938
[Multi-Tab] Adding Schema Migration #485
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
Changes from 22 commits
eb44929
07e44e7
3e1053f
5e84782
fd50301
be3b463
de237c5
53e56b5
7b3bedb
7b97c0a
c176eff
9154aad
6b072ff
662365f
dd21376
5e54b3a
cb81df8
7991970
2c6d6e1
f16bb4f
038c159
b70303c
34ab25a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,53 +22,48 @@ import { assert } from '../util/assert'; | |
|
||
import { encode, EncodedResourcePath } from './encoded_resource_path'; | ||
|
||
export const SCHEMA_VERSION = 1; | ||
|
||
/** Performs database creation and (in the future) upgrades between versions. */ | ||
export function createOrUpgradeDb(db: IDBDatabase, oldVersion: number): void { | ||
assert(oldVersion === 0, 'Unexpected upgrade from version ' + oldVersion); | ||
|
||
db.createObjectStore(DbMutationQueue.store, { | ||
keyPath: DbMutationQueue.keyPath | ||
}); | ||
|
||
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their | ||
// types. https://github.com/Microsoft/TypeScript/issues/14322 | ||
db.createObjectStore( | ||
DbMutationBatch.store, | ||
// tslint:disable-next-line:no-any | ||
{ keyPath: DbMutationBatch.keyPath as any } | ||
); | ||
/** | ||
* Schema Version for the Web client (containing the Mutation Queue, the Query | ||
* and the Remote Document Cache) and Multi-Tab Support. | ||
*/ | ||
export const SCHEMA_VERSION = 2; | ||
|
||
const targetDocumentsStore = db.createObjectStore( | ||
DbTargetDocument.store, | ||
// tslint:disable-next-line:no-any | ||
{ keyPath: DbTargetDocument.keyPath as any } | ||
); | ||
targetDocumentsStore.createIndex( | ||
DbTargetDocument.documentTargetsIndex, | ||
DbTargetDocument.documentTargetsKeyPath, | ||
{ unique: true } | ||
/** | ||
* Performs database creation and schema upgrades. | ||
* | ||
* Note that in production, this method is only ever used to upgrade the schema | ||
* to SCHEMA_VERSION. Different versions are only used for testing and | ||
* local feature development. | ||
*/ | ||
export function createOrUpgradeDb( | ||
db: IDBDatabase, | ||
fromVersion: number, | ||
toVersion: number | ||
): void { | ||
// This function currently supports migrating to schema version 1 (Mutation | ||
// Queue, Query and Remote Document Cache) and schema version 2 (Multi-Tab). | ||
assert( | ||
fromVersion < toVersion && fromVersion >= 0 && toVersion <= 2, | ||
'Unexpected schema upgrade from v${fromVersion} to v{toVersion}.' | ||
); | ||
|
||
const targetStore = db.createObjectStore(DbTarget.store, { | ||
keyPath: DbTarget.keyPath | ||
}); | ||
// NOTE: This is unique only because the TargetId is the suffix. | ||
targetStore.createIndex( | ||
DbTarget.queryTargetsIndexName, | ||
DbTarget.queryTargetsKeyPath, | ||
{ unique: true } | ||
); | ||
if (fromVersion < 1 && toVersion >= 1) { | ||
createOwnerStore(db); | ||
createMutationQueue(db); | ||
createQueryCache(db); | ||
createRemoteDocumentCache(db); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're not doing the switch / case pattern, I think you should still organize similarly (in particular put version 1 stuff before version 2), and I think we need to have a simple pattern that makes it obvious how to add code whenever we bump SCHEMA_VERSION... As-is, the Perhaps:
Or you could do a for loop:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used code snippet version 1. |
||
|
||
// NOTE: keys for these stores are specified explicitly rather than using a | ||
// keyPath. | ||
db.createObjectStore(DbDocumentMutation.store); | ||
db.createObjectStore(DbRemoteDocument.store); | ||
db.createObjectStore(DbOwner.store); | ||
db.createObjectStore(DbTargetGlobal.store); | ||
if (fromVersion < 2 && toVersion >= 2) { | ||
createClientMetadataStore(db); | ||
createTargetChangeStore(db); | ||
} | ||
} | ||
|
||
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their types. | ||
// https://github.com/Microsoft/TypeScript/issues/14322 | ||
type KeyPath = any; // tslint:disable-line:no-any | ||
|
||
/** | ||
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects. | ||
*/ | ||
|
@@ -94,6 +89,10 @@ export class DbOwner { | |
constructor(public ownerId: string, public leaseTimestampMs: number) {} | ||
} | ||
|
||
function createOwnerStore(db: IDBDatabase): void { | ||
db.createObjectStore(DbOwner.store); | ||
} | ||
|
||
/** Object keys in the 'mutationQueues' store are userId strings. */ | ||
export type DbMutationQueueKey = string; | ||
|
||
|
@@ -183,6 +182,18 @@ export class DbMutationBatch { | |
*/ | ||
export type DbDocumentMutationKey = [string, EncodedResourcePath, BatchId]; | ||
|
||
function createMutationQueue(db: IDBDatabase): void { | ||
db.createObjectStore(DbMutationQueue.store, { | ||
keyPath: DbMutationQueue.keyPath | ||
}); | ||
|
||
db.createObjectStore(DbMutationBatch.store, { | ||
keyPath: DbMutationBatch.keyPath as KeyPath | ||
}); | ||
|
||
db.createObjectStore(DbDocumentMutation.store); | ||
} | ||
|
||
/** | ||
* An object to be stored in the 'documentMutations' store in IndexedDb. | ||
* | ||
|
@@ -241,6 +252,10 @@ export class DbDocumentMutation { | |
*/ | ||
export type DbRemoteDocumentKey = string[]; | ||
|
||
function createRemoteDocumentCache(db: IDBDatabase): void { | ||
db.createObjectStore(DbRemoteDocument.store); | ||
} | ||
|
||
/** | ||
* Represents the known absence of a document at a particular version. | ||
* Stored in IndexedDb as part of a DbRemoteDocument object. | ||
|
@@ -455,11 +470,101 @@ export class DbTargetGlobal { | |
) {} | ||
} | ||
|
||
function createQueryCache(db: IDBDatabase): void { | ||
const targetDocumentsStore = db.createObjectStore(DbTargetDocument.store, { | ||
keyPath: DbTargetDocument.keyPath as KeyPath | ||
}); | ||
targetDocumentsStore.createIndex( | ||
DbTargetDocument.documentTargetsIndex, | ||
DbTargetDocument.documentTargetsKeyPath, | ||
{ unique: true } | ||
); | ||
|
||
const targetStore = db.createObjectStore(DbTarget.store, { | ||
keyPath: DbTarget.keyPath | ||
}); | ||
|
||
// NOTE: This is unique only because the TargetId is the suffix. | ||
targetStore.createIndex( | ||
DbTarget.queryTargetsIndexName, | ||
DbTarget.queryTargetsKeyPath, | ||
{ unique: true } | ||
); | ||
db.createObjectStore(DbTargetGlobal.store); | ||
} | ||
|
||
/** | ||
* An object representing the changes at a particular snapshot version for the | ||
* given target. This is used to facilitate storing query changelogs in the | ||
* targetChanges object store. | ||
* | ||
* PORTING NOTE: This is used for change propagation during multi-tab syncing | ||
* and not needed on iOS and Android. | ||
*/ | ||
export class DbTargetChange { | ||
/** Name of the IndexedDb object store. */ | ||
static store = 'targetChanges'; | ||
|
||
/** Keys are automatically assigned via the targetId and snapshotVersion. */ | ||
static keyPath = ['targetId', 'snapshotVersion']; | ||
|
||
constructor( | ||
/** | ||
* The targetId identifying a target. | ||
*/ | ||
public targetId: TargetId, | ||
/** | ||
* The snapshot version for this change. | ||
*/ | ||
public snapshotVersion: DbTimestamp, | ||
/** | ||
* The keys of the changed documents in this snapshot. | ||
*/ | ||
public changes: { | ||
added?: EncodedResourcePath[]; | ||
modified?: EncodedResourcePath[]; | ||
removed?: EncodedResourcePath[]; | ||
} | ||
) {} | ||
} | ||
|
||
function createTargetChangeStore(db: IDBDatabase): void { | ||
db.createObjectStore(DbTargetChange.store, { | ||
keyPath: DbTargetChange.keyPath as KeyPath | ||
}); | ||
} | ||
|
||
/** | ||
* The list of all IndexedDB stored used by the SDK. This is used when creating | ||
* transactions so that access across all stores is done atomically. | ||
* A record of the metadata state of each client. | ||
* | ||
* PORTING NOTE: This is used to synchronize multi-tab state and does not need | ||
* to be ported to iOS or Android. | ||
*/ | ||
export const ALL_STORES = [ | ||
export class DbClientMetadata { | ||
/** Name of the IndexedDb object store. */ | ||
static store = 'clientMetadata'; | ||
|
||
/** Keys are automatically assigned via the clientKey properties. */ | ||
static keyPath = ['clientKey']; | ||
|
||
constructor( | ||
/** The auto-generated client key assigned at client startup. */ | ||
public clientKey: string, | ||
/** The last time this state was updated. */ | ||
public updateTimeMs: DbTimestamp, | ||
/** Whether this client is running in a foreground tab. */ | ||
public inForeground: boolean | ||
) {} | ||
} | ||
|
||
function createClientMetadataStore(db: IDBDatabase): void { | ||
db.createObjectStore(DbClientMetadata.store, { | ||
keyPath: DbClientMetadata.keyPath as KeyPath | ||
}); | ||
} | ||
|
||
// Visible for testing | ||
export const V1_STORES = [ | ||
DbMutationQueue.store, | ||
DbMutationBatch.store, | ||
DbDocumentMutation.store, | ||
|
@@ -469,3 +574,12 @@ export const ALL_STORES = [ | |
DbTargetGlobal.store, | ||
DbTargetDocument.store | ||
]; | ||
|
||
const V2_STORES = [DbClientMetadata.store, DbTargetChange.store]; | ||
|
||
/** | ||
* The list of all default IndexedDB stores used throughout the SDK. This is | ||
* used when creating transactions so that access across all stores is done | ||
* atomically. | ||
*/ | ||
export const DEFAULT_STORES = [...V1_STORES, ...V2_STORES]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put back to ALL_STORES? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { expect } from 'chai'; | ||
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; | ||
import { | ||
DEFAULT_STORES, | ||
createOrUpgradeDb, | ||
V1_STORES | ||
} from '../../../src/local/indexeddb_schema'; | ||
import { Deferred } from '../../../src/util/promise'; | ||
import { SimpleDb } from '../../../src/local/simple_db'; | ||
|
||
const INDEXEDDB_TEST_DATABASE = 'schemaTest'; | ||
|
||
function withDb(schemaVersion, fn: (db: IDBDatabase) => void): Promise<void> { | ||
return new Promise<IDBDatabase>((resolve, reject) => { | ||
const request = window.indexedDB.open( | ||
INDEXEDDB_TEST_DATABASE, | ||
schemaVersion | ||
); | ||
request.onupgradeneeded = (event: IDBVersionChangeEvent) => { | ||
const db = (event.target as IDBOpenDBRequest).result; | ||
createOrUpgradeDb(db, event.oldVersion, schemaVersion); | ||
}; | ||
request.onsuccess = (event: Event) => { | ||
resolve((event.target as IDBOpenDBRequest).result); | ||
}; | ||
request.onerror = (event: ErrorEvent) => { | ||
reject((event.target as IDBOpenDBRequest).error); | ||
}; | ||
}).then(db => { | ||
fn(db); | ||
db.close(); | ||
}); | ||
} | ||
|
||
function getAllObjectStores(db: IDBDatabase): String[] { | ||
const objectStores: String[] = []; | ||
for (let i = 0; i < db.objectStoreNames.length; ++i) { | ||
objectStores.push(db.objectStoreNames.item(i)); | ||
} | ||
objectStores.sort(); | ||
return objectStores; | ||
} | ||
|
||
describe('IndexedDbSchema: createOrUpgradeDb', () => { | ||
if (!IndexedDbPersistence.isAvailable()) { | ||
console.warn('No IndexedDB. Skipping createOrUpgradeDb() tests.'); | ||
return; | ||
} | ||
|
||
beforeEach(() => SimpleDb.delete(INDEXEDDB_TEST_DATABASE)); | ||
|
||
it('can install schema version 1', () => { | ||
return withDb(1, db => { | ||
expect(db.version).to.be.equal(1); | ||
expect(getAllObjectStores(db)).to.have.members(V1_STORES); | ||
}); | ||
}); | ||
|
||
it('can install schema version 2', () => { | ||
return withDb(2, db => { | ||
expect(db.version).to.be.equal(2); | ||
expect(getAllObjectStores(db)).to.have.members(DEFAULT_STORES); | ||
}); | ||
}); | ||
|
||
it('can upgrade from schema version 1 to 2', () => { | ||
return withDb(1, () => {}).then(() => | ||
withDb(2, db => { | ||
expect(db.version).to.be.equal(2); | ||
expect(getAllObjectStores(db)).to.have.members(DEFAULT_STORES); | ||
}) | ||
); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 => SCHEMA_VERSION ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually changed
SCHEMA_VERSION
toDEFAULT_SCHEMA_VERSION
and reset the value back to 1. This way, I can merge this code to master, and Greg can build the GC migration on top of this.