Skip to content

Commit 2b7d3ae

Browse files
authored
Resolve #2011 and #2012 (#2033)
Chech input array for duplicate primary keys before updating the liveQuery cache. This commit also introduces RangeSet.hasKey() method (to simplify seeking of a key in a rangeset), corrects the typing of IntervalTreeNode and removes some unused imports.
1 parent df2f963 commit 2b7d3ae

File tree

4 files changed

+61
-46
lines changed

4 files changed

+61
-46
lines changed

src/helpers/rangeset.ts

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ props(RangeSet.prototype, {
4848
keys.forEach(key => addRange(this, key, key));
4949
return this;
5050
},
51+
hasKey(key: IndexableType) {
52+
const node = getRangeSetIterator(this).next(key).value;
53+
return node && cmp(node.from, key) <= 0 && cmp(node.to, key) >= 0;
54+
},
5155

5256
[iteratorSymbol](): Iterator<IntervalTreeNode, undefined, IndexableType | undefined> {
5357
return getRangeSetIterator(this);

src/live-query/cache/apply-optimistic-ops.ts

+29-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { cmp } from '../../functions/cmp';
2-
import { deepClone, isArray } from '../../functions/utils';
3-
import { RangeSet, rangesOverlap } from '../../helpers/rangeset';
2+
import { isArray } from '../../functions/utils';
3+
import { RangeSet } from '../../helpers/rangeset';
44
import { CacheEntry } from '../../public/types/cache';
55
import {
6-
DBCoreIndex,
76
DBCoreMutateRequest,
87
DBCoreQueryRequest,
98
DBCoreTable,
@@ -29,19 +28,25 @@ export function applyOptimisticOps(
2928

3029
let finalResult = ops.reduce((result, op) => {
3130
let modifedResult = result;
32-
const includedValues =
33-
op.type === 'add' || op.type === 'put'
34-
? op.values.filter((v) => {
35-
const key = extractIndex(v);
36-
return multiEntry && isArray(key) // multiEntry index work like plain index unless key is array
37-
? key.some((k) => isWithinRange(k, queryRange)) // multiEntry and array key
38-
: isWithinRange(key, queryRange); // multiEntry but not array key
39-
}).map(v => {
40-
v = deepClone(v);// v might come from user so we can't just freeze it.
41-
if (immutable) Object.freeze(v);
42-
return v;
43-
})
44-
: [];
31+
const includedValues: any[] = [];
32+
if (op.type === 'add' || op.type === 'put') {
33+
const includedPKs = new RangeSet(); // For ignoring duplicates
34+
for (let i = op.values.length - 1; i >= 0; --i) {
35+
// backwards to prioritize last value of same PK
36+
const value = op.values[i];
37+
const pk = extractPrimKey(value);
38+
if (includedPKs.hasKey(pk)) continue;
39+
const key = extractIndex(value);
40+
if (
41+
multiEntry && isArray(key)
42+
? key.some((k) => isWithinRange(k, queryRange))
43+
: isWithinRange(key, queryRange)
44+
) {
45+
includedPKs.addKey(pk);
46+
includedValues.push(value);
47+
}
48+
}
49+
}
4550
switch (op.type) {
4651
case 'add':
4752
modifedResult = result.concat(
@@ -55,22 +60,22 @@ export function applyOptimisticOps(
5560
op.values.map((v) => extractPrimKey(v))
5661
);
5762
modifedResult = result
58-
.filter((item) => {
59-
const key = req.values ? extractPrimKey(item) : item;
60-
return !rangesOverlap(new RangeSet(key), keySet);
61-
})
63+
.filter(
64+
// Remove all items that are being replaced
65+
(item) => !keySet.hasKey(req.values ? extractPrimKey(item) : item)
66+
)
6267
.concat(
68+
// Add all items that are being put (sorting will be done later)
6369
req.values
6470
? includedValues
6571
: includedValues.map((v) => extractPrimKey(v))
6672
);
6773
break;
6874
case 'delete':
6975
const keysToDelete = new RangeSet().addKeys(op.keys);
70-
modifedResult = result.filter((item) => {
71-
const key = req.values ? extractPrimKey(item) : item;
72-
return !rangesOverlap(new RangeSet(key), keysToDelete);
73-
});
76+
modifedResult = result.filter(
77+
(item) => !keysToDelete.hasKey(req.values ? extractPrimKey(item) : item)
78+
);
7479

7580
break;
7681
case 'deleteRange':

src/public/types/rangeset.d.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export type IntervalTree = IntervalTreeNode | EmptyRange;
44
export interface IntervalTreeNode {
55
from: IndexableType; // lower bound
66
to: IndexableType; // upper bound
7-
l: IntervalTreeNode | null; // left
8-
r: IntervalTreeNode | null; // right
7+
l?: IntervalTreeNode | null; // left
8+
r?: IntervalTreeNode | null; // right
99
d: number; // depth
1010
}
1111
export interface EmptyRange {
@@ -16,6 +16,7 @@ export interface RangeSetPrototype {
1616
add(rangeSet: IntervalTree | {from: IndexableType, to: IndexableType}): RangeSet;
1717
addKey(key: IndexableType): RangeSet;
1818
addKeys(keys: IndexableType[]): RangeSet;
19+
hasKey(key: IndexableType): boolean;
1920
[Symbol.iterator](): Iterator<IntervalTreeNode, undefined, IndexableType | undefined>;
2021
}
2122

test/tests-live-query.js

+25-20
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ promisedTest("optimistic updates that eventually fail must be reverted (Issue #1
271271
let abbaKey = 0;
272272
let lastFriendId = 0;
273273
let barbarFriendId = 0;
274-
let fruitCount = 0; // A bug in Safari <= 13.1 makes it unable to count on the name index (adds 1 extra)
275274
const bulkFriends = [];
276275
for (let i=0; i<51; ++i) {
277276
bulkFriends.push({name: `name${i}`, age: i});
@@ -294,7 +293,7 @@ const mutsAndExpects = () => [
294293
itemsStartsWithAPrimKeys: [-1],
295294
itemsStartsWithAOffset3: [],
296295
itemsStartsWithAKeys: ["A"],
297-
itemsStartsWithACount: fruitCount + 1
296+
itemsStartsWithACount: 1
298297
}
299298
],
300299
// addAuto
@@ -342,7 +341,7 @@ const mutsAndExpects = () => [
342341
itemsStartsWithAPrimKeys: [],
343342
itemsStartsWithAOffset3: [],
344343
itemsStartsWithAKeys: [],
345-
itemsStartsWithACount: fruitCount
344+
itemsStartsWithACount: 0
346345
}
347346
],
348347
[
@@ -355,7 +354,7 @@ const mutsAndExpects = () => [
355354
itemsStartsWithAPrimKeys: [-1],
356355
itemsStartsWithAOffset3: [],
357356
itemsStartsWithAKeys: ["A"],
358-
itemsStartsWithACount: fruitCount + 1
357+
itemsStartsWithACount: 1
359358
}
360359
],
361360
// add again
@@ -367,7 +366,7 @@ const mutsAndExpects = () => [
367366
itemsStartsWithAPrimKeys: [-1, 4, 6, 5],
368367
itemsStartsWithAOffset3: [{id: 5, name: "Assot"}], // offset 3
369368
itemsStartsWithAKeys: ["A", "Abbot", "Ambros", "Assot"],
370-
itemsStartsWithACount: fruitCount + 4
369+
itemsStartsWithACount: 4
371370
}
372371
],
373372
// delete:
@@ -381,7 +380,7 @@ const mutsAndExpects = () => [
381380
itemsStartsWithA: [{id: 4, name: "Abbot"}, {id: 6, name: "Ambros"}, {id: 5, name: "Assot"}],
382381
itemsStartsWithAPrimKeys: [4, 6, 5],
383382
itemsStartsWithAKeys: ["Abbot", "Ambros", "Assot"],
384-
itemsStartsWithACount: fruitCount + 3
383+
itemsStartsWithACount: 3
385384
},
386385
// Allowed extras:
387386
// If hooks is listened to we'll get an even more correct update of the itemsStartsWithAOffset3 query
@@ -400,7 +399,7 @@ const mutsAndExpects = () => [
400399
}, {
401400
// Things that optionally can be matched in result (if no hooks specified):
402401
itemsStartsWithAPrimKeys: [4, 6, 5],
403-
itemsStartsWithACount: fruitCount + 3,
402+
itemsStartsWithACount: 3,
404403
itemsStartsWithAOffset3: []
405404
}
406405
],
@@ -413,7 +412,7 @@ const mutsAndExpects = () => [
413412
itemsStartsWithA: [{id: 4, name: "Abbot"}, {id: 6, name: "Ambros"}],
414413
itemsStartsWithAPrimKeys: [4, 6],
415414
itemsStartsWithAKeys: ["Abbot", "Ambros"],
416-
itemsStartsWithACount: fruitCount + 2
415+
itemsStartsWithACount: 2
417416
}, {
418417
itemsStartsWithAOffset3: [] // This is
419418
}
@@ -543,20 +542,26 @@ const mutsAndExpects = () => [
543542
]
544543
}
545544
],
545+
// Issue 2011 / 2012
546+
[
547+
() => db.items.bulkPut([
548+
{id: 6, name: "One"},
549+
{id: 6, name: "Two"},
550+
{id: 6, name: "Three"}
551+
]),
552+
{
553+
itemsToArray: [{id:1},{id:2},{id:3},{id:4,name:"Abbot"},{id:6,name:"Three"}],
554+
itemsStartsWithA: [{id: 4, name: "Abbot"}],
555+
itemsStartsWithAPrimKeys: [4],
556+
itemsStartsWithAKeys: ["Abbot"],
557+
itemsStartsWithACount: 1
558+
},[
559+
"itemsStartsWithAOffset3" // Should not be updated but need to be ignored because otherwise it fails in dexie-syncable's integration tests that expects it to update to another empty array
560+
]
561+
],
546562
]
547563

548564
promisedTest("Full use case matrix", async ()=>{
549-
// A bug in Safari <= 13.1 makes it unable to count on the name index (adds 1 extra)
550-
fruitCount = await db.items.where('name').startsWith('A').count();
551-
if (fruitCount > 0) console.log("fruitCount: " + fruitCount);
552-
553-
if (isIE) {
554-
// The IE implementation becomes shaky here.
555-
// Maybe becuase we launch several parallel queries to IDB.
556-
ok(true, "Skipping this test for IE - too shaky for the CI");
557-
return;
558-
}
559-
560565
const queries = {
561566
itemsToArray: () => db.items.toArray(),
562567
itemsGet1And2: () => Promise.all(db.items.get(1), db.items.get(-1)),
@@ -591,7 +596,7 @@ promisedTest("Full use case matrix", async ()=>{
591596
itemsStartsWithAPrimKeys: [],
592597
itemsStartsWithAOffset3: [],
593598
itemsStartsWithAKeys: [],
594-
itemsStartsWithACount: fruitCount,
599+
itemsStartsWithACount: 0,
595600

596601
outboundToArray: [
597602
{num: 1, name: "A"},

0 commit comments

Comments
 (0)