Skip to content

Commit 7d9f111

Browse files
Schema migration that drops held write acks (#1149)
1 parent 9dbf6b0 commit 7d9f111

9 files changed

+401
-122
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ import { IndexedDbQueryCache } from './indexeddb_query_cache';
2626
import { IndexedDbRemoteDocumentCache } from './indexeddb_remote_document_cache';
2727
import {
2828
ALL_STORES,
29-
createOrUpgradeDb,
3029
DbClientMetadataKey,
3130
DbClientMetadata,
3231
DbPrimaryClient,
3332
DbPrimaryClientKey,
34-
SCHEMA_VERSION
33+
SCHEMA_VERSION,
34+
SchemaConverter
3535
} from './indexeddb_schema';
3636
import { LocalSerializer } from './local_serializer';
3737
import { MutationQueue } from './mutation_queue';
@@ -241,7 +241,11 @@ export class IndexedDbPersistence implements Persistence {
241241
assert(!this.started, 'IndexedDbPersistence double-started!');
242242
assert(this.window !== null, "Expected 'window' to be defined");
243243

244-
return SimpleDb.openOrCreate(this.dbName, SCHEMA_VERSION, createOrUpgradeDb)
244+
return SimpleDb.openOrCreate(
245+
this.dbName,
246+
SCHEMA_VERSION,
247+
new SchemaConverter(this.serializer)
248+
)
245249
.then(db => {
246250
this.simpleDb = db;
247251
})

packages/firestore/src/local/indexeddb_schema.ts

+109-49
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ import { ResourcePath } from '../model/path';
2121
import { assert } from '../util/assert';
2222

2323
import { encode, EncodedResourcePath } from './encoded_resource_path';
24-
import { SimpleDbTransaction } from './simple_db';
24+
import { SimpleDbSchemaConverter, SimpleDbTransaction } from './simple_db';
2525
import { PersistencePromise } from './persistence_promise';
2626
import { SnapshotVersion } from '../core/snapshot_version';
27+
import { BATCHID_UNKNOWN } from '../model/mutation_batch';
28+
import { IndexedDbMutationQueue } from './indexeddb_mutation_queue';
29+
import { LocalSerializer } from './local_serializer';
30+
import { IndexedDbTransaction } from './indexeddb_persistence';
2731

2832
/**
2933
* Schema Version for the Web client:
@@ -35,66 +39,122 @@ import { SnapshotVersion } from '../core/snapshot_version';
3539
* to limbo resolution. Addresses
3640
* https://github.com/firebase/firebase-ios-sdk/issues/1548
3741
* 4. Multi-Tab Support.
42+
* 5. Removal of held write acks (not yet active).
3843
*/
3944
export const SCHEMA_VERSION = 4;
45+
// TODO(mrschmidt): As SCHEMA_VERSION becomes 5, uncomment the assert in
46+
// `createOrUpgrade`.
4047

41-
/**
42-
* Performs database creation and schema upgrades.
43-
*
44-
* Note that in production, this method is only ever used to upgrade the schema
45-
* to SCHEMA_VERSION. Different values of toVersion are only used for testing
46-
* and local feature development.
47-
*/
48-
export function createOrUpgradeDb(
49-
db: IDBDatabase,
50-
txn: SimpleDbTransaction,
51-
fromVersion: number,
52-
toVersion: number
53-
): PersistencePromise<void> {
54-
assert(
55-
fromVersion < toVersion && fromVersion >= 0 && toVersion <= SCHEMA_VERSION,
56-
'Unexpected schema upgrade from v${fromVersion} to v{toVersion}.'
57-
);
48+
/** Performs database creation and schema upgrades. */
49+
export class SchemaConverter implements SimpleDbSchemaConverter {
50+
constructor(private readonly serializer: LocalSerializer) {}
5851

59-
if (fromVersion < 1 && toVersion >= 1) {
60-
createPrimaryClientStore(db);
61-
createMutationQueue(db);
62-
createQueryCache(db);
63-
createRemoteDocumentCache(db);
64-
}
52+
/**
53+
* Performs database creation and schema upgrades.
54+
*
55+
* Note that in production, this method is only ever used to upgrade the schema
56+
* to SCHEMA_VERSION. Different values of toVersion are only used for testing
57+
* and local feature development.
58+
*/
59+
createOrUpgrade(
60+
db: IDBDatabase,
61+
txn: SimpleDbTransaction,
62+
fromVersion: number,
63+
toVersion: number
64+
): PersistencePromise<void> {
65+
// assert(
66+
// fromVersion < toVersion && fromVersion >= 0 && toVersion <= SCHEMA_VERSION,
67+
// `Unexpected schema upgrade from v${fromVersion} to v{toVersion}.`
68+
// );
69+
70+
if (fromVersion < 1 && toVersion >= 1) {
71+
createPrimaryClientStore(db);
72+
createMutationQueue(db);
73+
createQueryCache(db);
74+
createRemoteDocumentCache(db);
75+
}
6576

66-
// Migration 2 to populate the targetGlobal object no longer needed since
67-
// migration 3 unconditionally clears it.
77+
// Migration 2 to populate the targetGlobal object no longer needed since
78+
// migration 3 unconditionally clears it.
79+
80+
let p = PersistencePromise.resolve();
81+
if (fromVersion < 3 && toVersion >= 3) {
82+
// Brand new clients don't need to drop and recreate--only clients that
83+
// potentially have corrupt data.
84+
if (fromVersion !== 0) {
85+
dropQueryCache(db);
86+
createQueryCache(db);
87+
}
88+
p = p.next(() => writeEmptyTargetGlobalEntry(txn));
89+
}
6890

69-
let p = PersistencePromise.resolve();
70-
if (fromVersion < 3 && toVersion >= 3) {
71-
// Brand new clients don't need to drop and recreate--only clients that
72-
// potentially have corrupt data.
73-
if (fromVersion !== 0) {
74-
dropQueryCache(db);
75-
createQueryCache(db);
91+
if (fromVersion < 4 && toVersion >= 4) {
92+
if (fromVersion !== 0) {
93+
// Schema version 3 uses auto-generated keys to generate globally unique
94+
// mutation batch IDs (this was previously ensured internally by the
95+
// client). To migrate to the new schema, we have to read all mutations
96+
// and write them back out. We preserve the existing batch IDs to guarantee
97+
// consistency with other object stores. Any further mutation batch IDs will
98+
// be auto-generated.
99+
p = p.next(() => upgradeMutationBatchSchemaAndMigrateData(db, txn));
100+
}
101+
102+
p = p.next(() => {
103+
createClientMetadataStore(db);
104+
createRemoteDocumentChangesStore(db);
105+
});
76106
}
77-
p = p.next(() => writeEmptyTargetGlobalEntry(txn));
78-
}
79107

80-
if (fromVersion < 4 && toVersion >= 4) {
81-
if (fromVersion !== 0) {
82-
// Schema version 3 uses auto-generated keys to generate globally unique
83-
// mutation batch IDs (this was previously ensured internally by the
84-
// client). To migrate to the new schema, we have to read all mutations
85-
// and write them back out. We preserve the existing batch IDs to guarantee
86-
// consistency with other object stores. Any further mutation batch IDs will
87-
// be auto-generated.
88-
p = p.next(() => upgradeMutationBatchSchemaAndMigrateData(db, txn));
108+
if (fromVersion < 5 && toVersion >= 5) {
109+
p = p.next(() => this.removeAcknowledgedMutations(txn));
89110
}
90111

91-
p = p.next(() => {
92-
createClientMetadataStore(db);
93-
createRemoteDocumentChangesStore(db);
94-
});
112+
return p;
95113
}
96114

97-
return p;
115+
private removeAcknowledgedMutations(
116+
txn: SimpleDbTransaction
117+
): PersistencePromise<void> {
118+
const queuesStore = txn.store<DbMutationQueueKey, DbMutationQueue>(
119+
DbMutationQueue.store
120+
);
121+
const mutationsStore = txn.store<DbMutationBatchKey, DbMutationBatch>(
122+
DbMutationBatch.store
123+
);
124+
125+
const indexedDbTransaction = new IndexedDbTransaction(txn);
126+
return queuesStore.loadAll().next(queues => {
127+
return PersistencePromise.forEach(queues, queue => {
128+
const mutationQueue = new IndexedDbMutationQueue(
129+
queue.userId,
130+
this.serializer
131+
);
132+
const range = IDBKeyRange.bound(
133+
[queue.userId, BATCHID_UNKNOWN],
134+
[queue.userId, queue.lastAcknowledgedBatchId]
135+
);
136+
137+
return mutationsStore
138+
.loadAll(DbMutationBatch.userMutationsIndex, range)
139+
.next(dbBatches => {
140+
return PersistencePromise.forEach(dbBatches, dbBatch => {
141+
assert(
142+
dbBatch.userId === queue.userId,
143+
`Cannot process batch ${dbBatch.batchId} from unexpected user`
144+
);
145+
const batch = this.serializer.fromDbMutationBatch(dbBatch);
146+
return mutationQueue.removeMutationBatch(
147+
indexedDbTransaction,
148+
batch
149+
);
150+
});
151+
})
152+
.next(() =>
153+
mutationQueue.performConsistencyCheck(indexedDbTransaction)
154+
);
155+
});
156+
});
157+
}
98158
}
99159

100160
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their types.

packages/firestore/src/local/simple_db.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import { Code, FirestoreError } from '../util/error';
2525

2626
const LOG_TAG = 'SimpleDb';
2727

28+
export interface SimpleDbSchemaConverter {
29+
createOrUpgrade(
30+
db: IDBDatabase,
31+
txn: SimpleDbTransaction,
32+
fromVersion: number,
33+
toVersion: number
34+
): PersistencePromise<void>;
35+
}
36+
2837
/**
2938
* Provides a wrapper around IndexedDb with a simplified interface that uses
3039
* Promise-like return values to chain operations. Real promises cannot be used
@@ -37,12 +46,7 @@ export class SimpleDb {
3746
static openOrCreate(
3847
name: string,
3948
version: number,
40-
runUpgrade: (
41-
db: IDBDatabase,
42-
txn: SimpleDbTransaction,
43-
fromVersion: number,
44-
toVersion: number
45-
) => PersistencePromise<void>
49+
schemaConverter: SimpleDbSchemaConverter
4650
): Promise<SimpleDb> {
4751
assert(
4852
SimpleDb.isAvailable(),
@@ -87,12 +91,14 @@ export class SimpleDb {
8791
// we wrap that in a SimpleDbTransaction to allow use of our friendlier
8892
// API for schema migration operations.
8993
const txn = new SimpleDbTransaction(request.transaction);
90-
runUpgrade(db, txn, event.oldVersion, SCHEMA_VERSION).next(() => {
91-
debug(
92-
LOG_TAG,
93-
'Database upgrade to version ' + SCHEMA_VERSION + ' complete'
94-
);
95-
});
94+
schemaConverter
95+
.createOrUpgrade(db, txn, event.oldVersion, SCHEMA_VERSION)
96+
.next(() => {
97+
debug(
98+
LOG_TAG,
99+
'Database upgrade to version ' + SCHEMA_VERSION + ' complete'
100+
);
101+
});
96102
};
97103
}).toPromise();
98104
}

packages/firestore/test/unit/local/encoded_resource_path.test.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as EncodedResourcePath from '../../../src/local/encoded_resource_path';
1919
import { PersistencePromise } from '../../../src/local/persistence_promise';
2020
import {
2121
SimpleDb,
22+
SimpleDbSchemaConverter,
2223
SimpleDbStore,
2324
SimpleDbTransaction
2425
} from '../../../src/local/simple_db';
@@ -28,6 +29,18 @@ import { path } from '../../util/helpers';
2829
let db: SimpleDb;
2930
const sep = '\u0001\u0001';
3031

32+
class EncodedResourcePathSchemaConverter implements SimpleDbSchemaConverter {
33+
createOrUpgrade(
34+
db: IDBDatabase,
35+
txn: SimpleDbTransaction,
36+
fromVersion: number,
37+
toVersion: number
38+
): PersistencePromise<void> {
39+
db.createObjectStore('test');
40+
return PersistencePromise.resolve();
41+
}
42+
}
43+
3144
describe('EncodedResourcePath', () => {
3245
if (!SimpleDb.isAvailable()) {
3346
console.warn('No IndexedDB. Skipping EncodedResourcePath tests.');
@@ -39,10 +52,11 @@ describe('EncodedResourcePath', () => {
3952
beforeEach(() => {
4053
return SimpleDb.delete(dbName)
4154
.then(() => {
42-
return SimpleDb.openOrCreate(dbName, 1, db => {
43-
db.createObjectStore('test');
44-
return PersistencePromise.resolve();
45-
});
55+
return SimpleDb.openOrCreate(
56+
dbName,
57+
1,
58+
new EncodedResourcePathSchemaConverter()
59+
);
4660
})
4761
.then(simpleDb => {
4862
db = simpleDb;

0 commit comments

Comments
 (0)