Skip to content

Commit fcf1f64

Browse files
author
Greg Soltis
authored
Implement sequence number migration (#1374)
* Implement sequence number migration
1 parent 8375f67 commit fcf1f64

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SnapshotVersion } from '../core/snapshot_version';
2323
import { BATCHID_UNKNOWN } from '../model/mutation_batch';
2424
import { encode, EncodedResourcePath } from './encoded_resource_path';
2525
import { removeMutationBatch } from './indexeddb_mutation_queue';
26+
import { getHighestListenSequenceNumber } from './indexeddb_query_cache';
2627
import { dbDocumentSize } from './indexeddb_remote_document_cache';
2728
import { LocalSerializer } from './local_serializer';
2829
import { PersistencePromise } from './persistence_promise';
@@ -39,8 +40,10 @@ import { SimpleDbSchemaConverter, SimpleDbTransaction } from './simple_db';
3940
* https://github.com/firebase/firebase-ios-sdk/issues/1548
4041
* 4. Multi-Tab Support.
4142
* 5. Removal of held write acks.
43+
* 6. Create document global for tracking document cache size.
44+
* 7. Ensure every cached document has a sentinel row with a sequence number.
4245
*/
43-
export const SCHEMA_VERSION = 6;
46+
export const SCHEMA_VERSION = 7;
4447

4548
/** Performs database creation and schema upgrades. */
4649
export class SchemaConverter implements SimpleDbSchemaConverter {
@@ -115,6 +118,10 @@ export class SchemaConverter implements SimpleDbSchemaConverter {
115118
});
116119
}
117120

121+
if (fromVersion < 7 && toVersion >= 7) {
122+
p = p.next(() => this.ensureSequenceNumbers(txn));
123+
}
124+
118125
return p;
119126
}
120127

@@ -172,6 +179,53 @@ export class SchemaConverter implements SimpleDbSchemaConverter {
172179
});
173180
});
174181
}
182+
183+
/**
184+
* Ensures that every document in the remote document cache has a corresponding sentinel row
185+
* with a sequence number. Missing rows are given the most recently used sequence number.
186+
*/
187+
private ensureSequenceNumbers(
188+
txn: SimpleDbTransaction
189+
): PersistencePromise<void> {
190+
const documentTargetStore = txn.store<
191+
DbTargetDocumentKey,
192+
DbTargetDocument
193+
>(DbTargetDocument.store);
194+
const documentsStore = txn.store<DbRemoteDocumentKey, DbRemoteDocument>(
195+
DbRemoteDocument.store
196+
);
197+
198+
return getHighestListenSequenceNumber(txn).next(currentSequenceNumber => {
199+
const writeSentinelKey = (
200+
path: ResourcePath
201+
): PersistencePromise<void> => {
202+
return documentTargetStore.put(
203+
new DbTargetDocument(0, encode(path), currentSequenceNumber)
204+
);
205+
};
206+
207+
const promises: Array<PersistencePromise<void>> = [];
208+
return documentsStore
209+
.iterate((key, doc) => {
210+
const path = new ResourcePath(key);
211+
const docSentinelKey = sentinelKey(path);
212+
promises.push(
213+
documentTargetStore.get(docSentinelKey).next(maybeSentinel => {
214+
if (!maybeSentinel) {
215+
return writeSentinelKey(path);
216+
} else {
217+
return PersistencePromise.resolve();
218+
}
219+
})
220+
);
221+
})
222+
.next(() => PersistencePromise.waitFor(promises));
223+
});
224+
}
225+
}
226+
227+
function sentinelKey(path: ResourcePath): DbTargetDocumentKey {
228+
return [0, encode(path)];
175229
}
176230

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

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { expect } from 'chai';
1818
import { PersistenceSettings } from '../../../src/api/database';
1919
import { SnapshotVersion } from '../../../src/core/snapshot_version';
20+
import { decode, encode } from '../../../src/local/encoded_resource_path';
2021
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
2122
import {
2223
DbDocumentMutation,
@@ -32,6 +33,8 @@ import {
3233
DbRemoteDocumentGlobalKey,
3334
DbRemoteDocumentKey,
3435
DbTarget,
36+
DbTargetDocument,
37+
DbTargetDocumentKey,
3538
DbTargetGlobal,
3639
DbTargetGlobalKey,
3740
DbTargetKey,
@@ -521,6 +524,92 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => {
521524
});
522525
});
523526
});
527+
528+
it('can upgrade from version 6 to 7', async () => {
529+
const oldSequenceNumber = 1;
530+
// Set the highest sequence number to this value so that untagged documents
531+
// will pick up this value.
532+
const newSequenceNumber = 2;
533+
await withDb(6, db => {
534+
const serializer = TEST_SERIALIZER;
535+
536+
const sdb = new SimpleDb(db);
537+
return sdb.runTransaction('readwrite', V6_STORES, txn => {
538+
const targetGlobalStore = txn.store<DbTargetGlobalKey, DbTargetGlobal>(
539+
DbTargetGlobal.store
540+
);
541+
const remoteDocumentStore = txn.store<
542+
DbRemoteDocumentKey,
543+
DbRemoteDocument
544+
>(DbRemoteDocument.store);
545+
const targetDocumentStore = txn.store<
546+
DbTargetDocumentKey,
547+
DbTargetDocument
548+
>(DbTargetDocument.store);
549+
return targetGlobalStore
550+
.get(DbTargetGlobal.key)
551+
.next(metadata => {
552+
expect(metadata).to.not.be.null;
553+
metadata!.highestListenSequenceNumber = newSequenceNumber;
554+
return targetGlobalStore.put(DbTargetGlobal.key, metadata!);
555+
})
556+
.next(() => {
557+
// Set up some documents (we only need the keys)
558+
// For the odd ones, add sentinel rows.
559+
const promises: Array<PersistencePromise<void>> = [];
560+
for (let i = 0; i < 10; i++) {
561+
const document = doc('docs/doc_' + i, 1, { foo: 'bar' });
562+
promises.push(
563+
remoteDocumentStore.put(
564+
document.key.path.toArray(),
565+
serializer.toDbRemoteDocument(document)
566+
)
567+
);
568+
if (i % 2 === 1) {
569+
promises.push(
570+
targetDocumentStore.put(
571+
new DbTargetDocument(
572+
0,
573+
encode(document.key.path),
574+
oldSequenceNumber
575+
)
576+
)
577+
);
578+
}
579+
}
580+
return PersistencePromise.waitFor(promises);
581+
});
582+
});
583+
});
584+
585+
// Now run the migration and verify
586+
await withDb(7, db => {
587+
const sdb = new SimpleDb(db);
588+
return sdb.runTransaction('readonly', V6_STORES, txn => {
589+
const targetDocumentStore = txn.store<
590+
DbTargetDocumentKey,
591+
DbTargetDocument
592+
>(DbTargetDocument.store);
593+
const range = IDBKeyRange.bound(
594+
[0],
595+
[1],
596+
/*lowerOpen=*/ false,
597+
/*upperOpen=*/ true
598+
);
599+
return targetDocumentStore.iterate(
600+
{ range },
601+
([_, path], targetDocument) => {
602+
const decoded = decode(path);
603+
const lastSegment = decoded.lastSegment();
604+
const docNum = +lastSegment.split('_')[1];
605+
const expected =
606+
docNum % 2 === 1 ? oldSequenceNumber : newSequenceNumber;
607+
expect(targetDocument.sequenceNumber).to.equal(expected);
608+
}
609+
);
610+
});
611+
});
612+
});
524613
});
525614

526615
describe('IndexedDb: canActAsPrimary', () => {

0 commit comments

Comments
 (0)