Skip to content

Commit a21612e

Browse files
authored
Enable batched upgrades in SQL providers. Aim to pull 1MB of data out… (#59)
* Enable batched upgrades in SQL providers. Aim to pull 1MB of data out of the DB at a time to avoid potential OOM conditions when upgrading large databases Allow consumers of NoSqlProvider to provide an estimated size for their database objects. This will help do better batching without having to do an initial transaction to see what object sizes look like
1 parent 74efd8e commit a21612e

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

src/NoSqlProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export interface StoreSchema {
3535
name: string;
3636
indexes?: IndexSchema[];
3737
primaryKeyPath: KeyPathType;
38+
// Estimated object size to enable batched data migration. Default = 200
39+
estimatedObjBytes?: number;
3840
}
3941

4042
// Schema representing a whole database (a collection of stores). Change your version number whenever you change your schema or

src/SqlProviderBase.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ const schemaVersionKey = 'schemaVersion';
4343
// This was taked from the sqlite documentation
4444
const SQLITE_MAX_SQL_LENGTH_IN_BYTES = 1000000;
4545

46+
const DB_SIZE_ESIMATE_DEFAULT = 200;
47+
const DB_MIGRATION_MAX_BYTE_TARGET = 1000000;
48+
4649
interface IndexMetadata {
4750
key: string;
4851
storeName: string;
@@ -434,11 +437,20 @@ export abstract class SqlProviderBase extends NoSqlProvider.DbProvider {
434437
// Migrate the data over using our existing put functions
435438
// (since it will do the right things with the indexes)
436439
// and delete the temp table.
437-
const jsMigrator = () => {
440+
const jsMigrator = (batchOffset = 0): SyncTasks.Promise<any> => {
441+
let esimatedSize = storeSchema.estimatedObjBytes || DB_SIZE_ESIMATE_DEFAULT;
442+
let batchSize = Math.max(1, Math.floor(DB_MIGRATION_MAX_BYTE_TARGET / esimatedSize));
438443
let store = trans.getStore(storeSchema.name);
439-
return trans.internal_getResultsFromQuery('SELECT nsp_data FROM temp_' + storeSchema.name)
440-
.then(objs => {
441-
return store.put(objs);
444+
return trans.internal_getResultsFromQuery('SELECT nsp_data FROM temp_' + storeSchema.name + ' LIMIT ' +
445+
batchSize + ' OFFSET ' + batchOffset)
446+
.then(objs => {
447+
return store.put(objs).then(() => {
448+
// Are we done migrating?
449+
if (objs.length < batchSize) {
450+
return undefined;
451+
}
452+
return jsMigrator(batchOffset + batchSize);
453+
});
442454
});
443455
};
444456

@@ -449,7 +461,9 @@ export abstract class SqlProviderBase extends NoSqlProvider.DbProvider {
449461
])
450462
.then(createTempTable)
451463
.then(tableMaker)
452-
.then(jsMigrator)
464+
.then(() => {
465+
return jsMigrator();
466+
})
453467
.then(dropTempTable)
454468
);
455469
}

src/tests/NoSqlProviderTests.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { IndexedDbProvider } from '../IndexedDbProvider';
1212
import { WebSqlProvider } from '../WebSqlProvider';
1313

1414
import NoSqlProviderUtils = require('../NoSqlProviderUtils');
15-
1615
import { SqlTransaction } from '../SqlProviderBase';
1716

1817
// Don't trap exceptions so we immediately see them with a stack trace
@@ -1200,6 +1199,76 @@ describe('NoSqlProvider', function () {
12001199
});
12011200
});
12021201

1202+
function testBatchUpgrade(expectedCallCount: number, itemByteSize: number): SyncTasks.Promise<void> {
1203+
const recordCount = 5000;
1204+
const data: { [id: string]: { id: string, tt: string } } = {};
1205+
_.times(recordCount, num => {
1206+
data[num.toString()] = {
1207+
id: num.toString(),
1208+
tt: 'tt' + num.toString()
1209+
};
1210+
});
1211+
return openProvider(provName, {
1212+
version: 1,
1213+
stores: [
1214+
{
1215+
name: 'test',
1216+
primaryKeyPath: 'id',
1217+
estimatedObjBytes: itemByteSize
1218+
}
1219+
]
1220+
}, true).then(prov => {
1221+
return prov.put('test', _.values(data)).then(() => {
1222+
return prov.close();
1223+
});
1224+
}).then(() => {
1225+
let transactionSpy: sinon.SinonSpy | undefined;
1226+
if (provName.indexOf('sql') !== -1) {
1227+
// Check that we batch the upgrade by spying on number of queries indirectly
1228+
// This only affects sql-based tests
1229+
transactionSpy = sinon.spy(SqlTransaction.prototype, 'internal_getResultsFromQuery');
1230+
}
1231+
return openProvider(provName, {
1232+
version: 2,
1233+
stores: [
1234+
{
1235+
name: 'test',
1236+
primaryKeyPath: 'id',
1237+
estimatedObjBytes: itemByteSize,
1238+
indexes: [{
1239+
name: 'ind1',
1240+
keyPath: 'tt'
1241+
}]
1242+
}
1243+
]
1244+
}, false).then(prov => {
1245+
if (transactionSpy) {
1246+
assert.equal(transactionSpy.callCount, expectedCallCount, 'Incorrect transaction count');
1247+
transactionSpy.restore();
1248+
}
1249+
return prov.getAll('test', undefined).then((records: any) => {
1250+
assert.equal(records.length, _.keys(data).length, 'Incorrect record count');
1251+
_.each(records, dbRecordToValidate => {
1252+
const originalRecord = data[dbRecordToValidate.id];
1253+
assert.ok(!!originalRecord);
1254+
assert.equal(originalRecord.id, dbRecordToValidate.id);
1255+
assert.equal(originalRecord.tt, dbRecordToValidate.tt);
1256+
});
1257+
}).then(() => {
1258+
return prov.close();
1259+
});
1260+
});
1261+
});
1262+
}
1263+
1264+
it('Add index - Large records - batched upgrade', () => {
1265+
return testBatchUpgrade(51, 10000);
1266+
});
1267+
1268+
it('Add index - small records - No batch upgrade', () => {
1269+
return testBatchUpgrade(1, 1);
1270+
});
1271+
12031272
if (provName.indexOf('indexeddb') !== 0) {
12041273
// This migration works on indexeddb because we don't check the types and the browsers silently accept it but just
12051274
// neglect to index the field...

0 commit comments

Comments
 (0)