Skip to content

Commit da5a95a

Browse files
authored
Optimize multi-index and FTS index insert/update on SQL Providers (#64)
* Optimize multi-index and FTS index on sqlite We can do the insert and deletes of multi-key indexes in one pass instead of executing multiple queries * Bump package.json version
1 parent 1581827 commit da5a95a

File tree

2 files changed

+79
-36
lines changed

2 files changed

+79
-36
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nosqlprovider",
3-
"version": "0.6.19",
3+
"version": "0.6.20",
44
"description": "A cross-browser/platform indexeddb-like client library",
55
"author": "David de Regt <David.de.Regt@microsoft.com>",
66
"scripts": {

src/SqlProviderBase.ts

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Abstract helpers for all NoSqlProvider DbProviders that are based on SQL backings.
77
*/
88

9+
import assert = require('assert');
910
import _ = require('lodash');
1011
import SyncTasks = require('synctasks');
1112

@@ -63,6 +64,12 @@ function indexUsesSeparateTable(indexSchema: NoSqlProvider.IndexSchema, supports
6364
return indexSchema.multiEntry || (!!indexSchema.fullText && supportsFTS3);
6465
}
6566

67+
function generateParamPlaceholder(count: number): string {
68+
assert.ok(count >= 1, 'Must provide at least one parameter to SQL statement');
69+
// Generate correct count of ?'s and slice off trailing comma
70+
return _.repeat('?,', count).slice(0, -1);
71+
}
72+
6673
const FakeFTSJoinToken = '^$^';
6774

6875
// Limit LIMIT numbers to a reasonable size to not break queries.
@@ -209,10 +216,7 @@ export abstract class SqlProviderBase extends NoSqlProvider.DbProvider {
209216
}
210217

211218
// Generate as many '?' as there are params
212-
let placeholder = '?';
213-
for (let i = 1; i < metasToDelete.length; i++) {
214-
placeholder += ',?';
215-
}
219+
const placeholder = generateParamPlaceholder(metasToDelete.length);
216220

217221
return trans.runQuery('DELETE FROM metadata WHERE name IN (' + placeholder + ')',
218222
_.map(metasToDelete, meta => meta.key));
@@ -788,10 +792,8 @@ class SqlStore implements NoSqlProvider.DbStore {
788792
startTime = Date.now();
789793
}
790794

791-
const qmarks = _.map(joinedKeys!!!, k => '?');
792-
793795
let promise = this._trans.internal_getResultsFromQuery('SELECT nsp_data FROM ' + this._schema.name + ' WHERE nsp_pk IN (' +
794-
qmarks.join(',') + ')', joinedKeys!!!);
796+
generateParamPlaceholder(joinedKeys.length) + ')', joinedKeys);
795797
if (this._verbose) {
796798
promise = promise.finally(() => {
797799
console.log('SqlStore (' + this._schema.name + ') getMultiple: (' + (Date.now() - startTime) + 'ms): Count: ' +
@@ -870,6 +872,9 @@ class SqlStore implements NoSqlProvider.DbStore {
870872

871873
// Also prepare mulltiEntry and FullText indexes
872874
if (_.some(this._schema.indexes, index => indexUsesSeparateTable(index, this._supportsFTS3))) {
875+
const keysToDeleteByIndex: { [indexIndex: number]: string[] } = {};
876+
const dataToInsertByIndex: { [indexIndex: number]: string[] } = {};
877+
873878
_.each(items, (item, itemIndex) => {
874879
const key = _.attempt(() => {
875880
return NoSqlProviderUtils.getSerializedKeyForKeypath(item, this._schema.primaryKeyPath)!!!;
@@ -879,50 +884,91 @@ class SqlStore implements NoSqlProvider.DbStore {
879884
return;
880885
}
881886

882-
_.each(this._schema.indexes, index => {
887+
_.each(this._schema.indexes, (index, indexIndex) => {
883888
let serializedKeys: string[];
884889

885890
if (index.fullText && this._supportsFTS3) {
886891
// FTS3 terms go in a separate virtual table...
887-
serializedKeys = [FullTextSearchHelpers.getFullTextIndexWordsForItem(<string> index.keyPath, item).join(' ')];
892+
serializedKeys = [FullTextSearchHelpers.getFullTextIndexWordsForItem(<string>index.keyPath, item).join(' ')];
888893
} else if (index.multiEntry) {
889894
// Have to extract the multiple entries into the alternate table...
890895
const valsRaw = NoSqlProviderUtils.getValueForSingleKeypath(item, <string>index.keyPath);
891896
if (valsRaw) {
892-
const err = _.attempt(() => {
893-
serializedKeys = _.map(NoSqlProviderUtils.arrayify(valsRaw), val =>
897+
const serializedKeysOrErr = _.attempt(() => {
898+
return _.map(NoSqlProviderUtils.arrayify(valsRaw), val =>
894899
NoSqlProviderUtils.serializeKeyToString(val, <string>index.keyPath));
895900
});
896-
if (err) {
897-
queries.push(SyncTasks.Rejected<void>(err));
901+
if (_.isError(serializedKeysOrErr)) {
902+
queries.push(SyncTasks.Rejected<void>(serializedKeysOrErr));
898903
return;
899904
}
905+
serializedKeys = serializedKeysOrErr;
906+
} else {
907+
serializedKeys = [];
900908
}
901909
} else {
902910
return;
903911
}
904912

905-
let valArgs: string[] = [], insertArgs: string[] = [];
906-
_.each(serializedKeys!!!, val => {
907-
valArgs.push(index.includeDataInIndex ? '(?, ?, ?)' : '(?, ?)');
908-
insertArgs.push(val);
909-
insertArgs.push(key);
910-
if (index.includeDataInIndex) {
911-
insertArgs.push(datas[itemIndex]);
913+
// Capture insert data
914+
if (serializedKeys.length > 0) {
915+
if (!dataToInsertByIndex[indexIndex]) {
916+
dataToInsertByIndex[indexIndex] = [];
912917
}
913-
});
914-
queries.push(this._trans.internal_nonQuery('DELETE FROM ' + this._schema.name + '_' + index.name +
915-
' WHERE nsp_refpk = ?', [key])
916-
.then(() => {
917-
if (valArgs.length > 0) {
918-
return this._trans.internal_nonQuery('INSERT INTO ' + this._schema.name + '_' + index.name +
919-
' (nsp_key, nsp_refpk' + (index.includeDataInIndex ? ', nsp_data' : '') + ') VALUES ' +
920-
valArgs.join(','), insertArgs);
921-
}
922-
return undefined;
923-
}));
918+
const dataToInsert = dataToInsertByIndex[indexIndex];
919+
_.each(serializedKeys, val => {
920+
dataToInsert.push(val);
921+
dataToInsert.push(key);
922+
if (index.includeDataInIndex) {
923+
dataToInsert.push(datas[itemIndex]);
924+
}
925+
});
926+
}
927+
928+
// Capture delete keys
929+
if (!keysToDeleteByIndex[indexIndex]) {
930+
keysToDeleteByIndex[indexIndex] = [];
931+
}
932+
933+
keysToDeleteByIndex[indexIndex].push(key);
924934
});
925935
});
936+
937+
const deleteQueries: SyncTasks.Promise<void>[] = [];
938+
939+
_.each(keysToDeleteByIndex, (keysToDelete, indedIndex) => {
940+
// We know indexes are defined if we have data to insert for them
941+
// _.each spits dictionary keys out as string, needs to turn into a number
942+
const index = this._schema.indexes!!![Number(indedIndex)];
943+
const itemPageSize = this._trans.internal_getMaxVariables();
944+
for (let i = 0; i < keysToDelete.length; i += itemPageSize) {
945+
const thisPageCount = Math.min(itemPageSize, keysToDelete.length - i);
946+
deleteQueries.push(this._trans.internal_nonQuery('DELETE FROM ' + this._schema.name + '_' + index.name +
947+
' WHERE nsp_refpk IN (' + generateParamPlaceholder(thisPageCount) + ')', keysToDelete.splice(0, thisPageCount)));
948+
}
949+
});
950+
951+
// Delete and insert tracking - cannot insert until delete is completed
952+
queries.push(SyncTasks.all(deleteQueries).then(() => {
953+
const insertQueries: SyncTasks.Promise<void>[] = [];
954+
_.each(dataToInsertByIndex, (data, indexIndex) => {
955+
// We know indexes are defined if we have data to insert for them
956+
// _.each spits dictionary keys out as string, needs to turn into a number
957+
const index = this._schema.indexes!!![Number(indexIndex)];
958+
const insertParamCount = index.includeDataInIndex ? 3 : 2;
959+
const itemPageSize = Math.floor(this._trans.internal_getMaxVariables() / insertParamCount);
960+
// data contains all the input parameters
961+
for (let i = 0; i < (data.length / insertParamCount); i += itemPageSize) {
962+
const thisPageCount = Math.min(itemPageSize, (data.length / insertParamCount)) - i;
963+
const qmarksValues = _.fill(new Array(thisPageCount), generateParamPlaceholder(insertParamCount));
964+
insertQueries.push(this._trans.internal_nonQuery('INSERT INTO ' +
965+
this._schema.name + '_' + index.name + ' (nsp_key, nsp_refpk' + (index.includeDataInIndex ? ', nsp_data' : '') +
966+
') VALUES ' + '(' + qmarksValues.join('),(') + ')', data.splice(0, thisPageCount * insertParamCount)));
967+
}
968+
});
969+
return SyncTasks.all(insertQueries).then(_.noop);
970+
}));
971+
926972
}
927973

928974
let promise = SyncTasks.all(queries);
@@ -981,10 +1027,7 @@ class SqlStore implements NoSqlProvider.DbStore {
9811027
}
9821028

9831029
// Generate as many '?' as there are params
984-
let placeholder = '?';
985-
for (let i = 1; i < params.length; i++) {
986-
placeholder += ',?';
987-
}
1030+
const placeholder = generateParamPlaceholder(params.length);
9881031

9891032
_.each(this._schema.indexes, index => {
9901033
if (indexUsesSeparateTable(index, this._supportsFTS3)) {

0 commit comments

Comments
 (0)