Skip to content

Commit 6c0fda1

Browse files
authored
Merge pull request #33 from CraigMacomber/bugFixes
fix mergeSibling not setting isShared and greedyClone(true) not deeply copying
2 parents eb16cc1 + b39b8ff commit 6c0fda1

File tree

3 files changed

+156
-16
lines changed

3 files changed

+156
-16
lines changed

b+tree.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,10 @@ var BNode = /** @class */ (function () {
13901390
/** Internal node (non-leaf node) ********************************************/
13911391
var BNodeInternal = /** @class */ (function (_super) {
13921392
__extends(BNodeInternal, _super);
1393+
/**
1394+
* This does not mark `children` as shared, so it is the responsibility of the caller
1395+
* to ensure that either children are marked shared, or it are not included in another tree.
1396+
*/
13931397
function BNodeInternal(children, keys) {
13941398
var _this = this;
13951399
if (!keys) {
@@ -1412,7 +1416,7 @@ var BNodeInternal = /** @class */ (function (_super) {
14121416
return this;
14131417
var nu = new BNodeInternal(this.children.slice(0), this.keys.slice(0));
14141418
for (var i = 0; i < nu.children.length; i++)
1415-
nu.children[i] = nu.children[i].greedyClone();
1419+
nu.children[i] = nu.children[i].greedyClone(force);
14161420
return nu;
14171421
};
14181422
BNodeInternal.prototype.minKey = function () {
@@ -1517,11 +1521,21 @@ var BNodeInternal = /** @class */ (function (_super) {
15171521
return newRightSibling;
15181522
}
15191523
};
1524+
/**
1525+
* Inserts `child` at index `i`.
1526+
* This does not mark `child` as shared, so it is the responsibility of the caller
1527+
* to ensure that either child is marked shared, or it is not included in another tree.
1528+
*/
15201529
BNodeInternal.prototype.insert = function (i, child) {
15211530
this.children.splice(i, 0, child);
15221531
this.keys.splice(i, 0, child.maxKey());
15231532
};
1533+
/**
1534+
* Split this node.
1535+
* Modifies this to remove the second half of the items, returning a separate node containing them.
1536+
*/
15241537
BNodeInternal.prototype.splitOffRightSide = function () {
1538+
// assert !this.isShared;
15251539
var half = this.children.length >> 1;
15261540
return new BNodeInternal(this.children.splice(half), this.keys.splice(half));
15271541
};
@@ -1610,11 +1624,24 @@ var BNodeInternal = /** @class */ (function (_super) {
16101624
}
16111625
return false;
16121626
};
1627+
/**
1628+
* Move children from `rhs` into this.
1629+
* `rhs` must be part of this tree, and be removed from it after this call
1630+
* (otherwise isShared for its children could be incorrect).
1631+
*/
16131632
BNodeInternal.prototype.mergeSibling = function (rhs, maxNodeSize) {
16141633
// assert !this.isShared;
16151634
var oldLength = this.keys.length;
16161635
this.keys.push.apply(this.keys, rhs.keys);
1617-
this.children.push.apply(this.children, rhs.children);
1636+
var rhsChildren = rhs.children;
1637+
this.children.push.apply(this.children, rhsChildren);
1638+
if (rhs.isShared) {
1639+
// Because rhs might continue to be used in another tree since it is shared,
1640+
// this is adding a parent to its children instead of just changing what their parent is.
1641+
// Thus they need to be marked as shared.
1642+
for (var i = 0; i < rhsChildren.length; i++)
1643+
rhsChildren[i].isShared = true;
1644+
}
16181645
// If our children are themselves almost empty due to a mass-delete,
16191646
// they may need to be merged too (but only the oldLength-1 and its
16201647
// right sibling should need this).

b+tree.test.ts

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -379,39 +379,121 @@ describe("cloning and sharing tests", () => {
379379
// This tests make a full 3 layer tree (height = 2), so use a small branching factor.
380380
const maxNodeSize = 4;
381381
const tree = new BTree<number, number>(
382-
undefined,
383-
simpleComparator,
384-
maxNodeSize
382+
undefined,
383+
simpleComparator,
384+
maxNodeSize
385385
);
386386
// Build a 3 layer complete tree, all values 0.
387387
for (
388-
let index = 0;
389-
index < maxNodeSize * maxNodeSize * maxNodeSize;
390-
index++
388+
let index = 0;
389+
index < maxNodeSize * maxNodeSize * maxNodeSize;
390+
index++
391391
) {
392-
tree.set(index, 0);
392+
tree.set(index, 0);
393393
}
394394
// Leaf nodes don't count, so this is depth 2
395395
expect(tree.height).toBe(2);
396-
396+
397397
// Edit the tree so it has a node in the second layer with exactly one child (key 0).
398398
tree.deleteRange(1, maxNodeSize * maxNodeSize, false);
399399
expect(tree.height).toBe(2);
400-
400+
401401
// Make a clone that should never be mutated.
402402
const clone = tree.clone();
403-
403+
404404
// Mutate the original tree in such a way that clone gets mutated due to incorrect is shared tracking.
405405
// Delete everything outside of the internal node with only one child, so its child becomes the new root.
406406
tree.deleteRange(maxNodeSize, Number.POSITIVE_INFINITY, false);
407407
expect(tree.height).toBe(0);
408-
408+
409409
// Modify original
410410
tree.set(0, 1);
411-
411+
412412
// Check that clone is not modified as well:
413413
expect(clone.get(0)).toBe(0);
414414
});
415+
416+
test("Regression test for greedyClone(true) not copying all nodes", () => {
417+
const maxNodeSize = 4;
418+
const tree = new BTree<number, number>(
419+
undefined,
420+
simpleComparator,
421+
maxNodeSize
422+
);
423+
// Build a 3 layer tree.
424+
for (
425+
let index = 0;
426+
index < maxNodeSize * maxNodeSize + 1;
427+
index++
428+
) {
429+
tree.set(index, 0);
430+
}
431+
// Leaf nodes don't count, so this is depth 2
432+
expect(tree.height).toBe(2);
433+
const clone = tree.greedyClone(true);
434+
435+
// The bug was that `force` was not passed down. This meant that non-shared nodes below the second layer would not be cloned.
436+
// Thus we check that the third layer of this tree did get cloned.
437+
// Since this depends on private APIs and types,
438+
// and this package currently has no way to expose them to tests without exporting them from the package,
439+
// do some private field access and any casts to make it work.
440+
expect((clone['_root'] as any).children[0].children[0]).not.toBe((tree['_root'] as any).children[0].children[0]);
441+
});
442+
443+
test("Regression test for mergeSibling setting isShared", () => {
444+
// This tests make a 3 layer tree (height = 2), so use a small branching factor.
445+
const maxNodeSize = 4;
446+
const tree = new BTree<number, number>(
447+
undefined,
448+
simpleComparator,
449+
maxNodeSize
450+
);
451+
// Build a 3 layer tree
452+
const count = maxNodeSize * maxNodeSize * maxNodeSize;
453+
for (
454+
let index = 0;
455+
index < count;
456+
index++
457+
) {
458+
tree.set(index, 0);
459+
}
460+
// Leaf nodes don't count, so this is depth 2
461+
expect(tree.height).toBe(2);
462+
463+
// Delete most of the keys so merging interior nodes is possible, marking all nodes as shared.
464+
for (
465+
let index = 0;
466+
index < count;
467+
index++
468+
) {
469+
if (index % 4 !== 0) {
470+
tree.delete(index);
471+
}
472+
}
473+
474+
const deepClone = tree.greedyClone(true);
475+
const cheapClone = tree.clone();
476+
477+
// These two clones should remain unchanged forever.
478+
// The bug this is testing for resulted in the cheap clone getting modified:
479+
// we will compare it against the deep clone to confirm it does not.
480+
481+
// Delete a bunch more nodes, causing merging.
482+
for (
483+
let index = 0;
484+
index < count;
485+
index++
486+
) {
487+
if (index % 16 !== 0) {
488+
tree.delete(index);
489+
}
490+
}
491+
492+
const different: number[] = [];
493+
const onDiff = (k: number) => { different.push(k); }
494+
deepClone.diffAgainst(cheapClone, onDiff, onDiff, onDiff);
495+
expect(different).toEqual([]);
496+
});
415497
});
416498

417499
describe('B+ tree with fanout 32', testBTree.bind(null, 32));

b+tree.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,8 @@ class BNode<K,V> {
12061206
// If this is an internal node, _keys[i] is the highest key in children[i].
12071207
keys: K[];
12081208
values: V[];
1209+
// True if this node might multiple parents (equivalently: could be in multiple b-trees).
1210+
// This means it must be cloned before being mutated to avoid changing an unrelated tree.
12091211
isShared: true | undefined;
12101212
get isLeaf() { return (this as any).children === undefined; }
12111213

@@ -1528,6 +1530,10 @@ class BNodeInternal<K,V> extends BNode<K,V> {
15281530
// keys[i] caches the value of children[i].maxKey().
15291531
children: BNode<K,V>[];
15301532

1533+
/**
1534+
* This does not mark `children` as shared, so it is the responsibility of the caller
1535+
* to ensure that either children are marked shared, or it are not included in another tree.
1536+
*/
15311537
constructor(children: BNode<K,V>[], keys?: K[]) {
15321538
if (!keys) {
15331539
keys = [];
@@ -1550,7 +1556,7 @@ class BNodeInternal<K,V> extends BNode<K,V> {
15501556
return this;
15511557
var nu = new BNodeInternal<K,V>(this.children.slice(0), this.keys.slice(0));
15521558
for (var i = 0; i < nu.children.length; i++)
1553-
nu.children[i] = nu.children[i].greedyClone();
1559+
nu.children[i] = nu.children[i].greedyClone(force);
15541560
return nu;
15551561
}
15561562

@@ -1666,12 +1672,22 @@ class BNodeInternal<K,V> extends BNode<K,V> {
16661672
}
16671673
}
16681674

1675+
/**
1676+
* Inserts `child` at index `i`.
1677+
* This does not mark `child` as shared, so it is the responsibility of the caller
1678+
* to ensure that either child is marked shared, or it is not included in another tree.
1679+
*/
16691680
insert(i: index, child: BNode<K,V>) {
16701681
this.children.splice(i, 0, child);
16711682
this.keys.splice(i, 0, child.maxKey());
16721683
}
16731684

1685+
/**
1686+
* Split this node.
1687+
* Modifies this to remove the second half of the items, returning a separate node containing them.
1688+
*/
16741689
splitOffRightSide() {
1690+
// assert !this.isShared;
16751691
var half = this.children.length >> 1;
16761692
return new BNodeInternal<K,V>(this.children.splice(half), this.keys.splice(half));
16771693
}
@@ -1765,11 +1781,26 @@ class BNodeInternal<K,V> extends BNode<K,V> {
17651781
return false;
17661782
}
17671783

1784+
/**
1785+
* Move children from `rhs` into this.
1786+
* `rhs` must be part of this tree, and be removed from it after this call
1787+
* (otherwise isShared for its children could be incorrect).
1788+
*/
17681789
mergeSibling(rhs: BNode<K,V>, maxNodeSize: number) {
17691790
// assert !this.isShared;
17701791
var oldLength = this.keys.length;
17711792
this.keys.push.apply(this.keys, rhs.keys);
1772-
this.children.push.apply(this.children, (rhs as any as BNodeInternal<K,V>).children);
1793+
const rhsChildren = (rhs as any as BNodeInternal<K,V>).children;
1794+
this.children.push.apply(this.children, rhsChildren);
1795+
1796+
if (rhs.isShared) {
1797+
// Because rhs might continue to be used in another tree since it is shared,
1798+
// this is adding a parent to its children instead of just changing what their parent is.
1799+
// Thus they need to be marked as shared.
1800+
for (var i = 0; i < rhsChildren.length; i++)
1801+
rhsChildren[i].isShared = true;
1802+
}
1803+
17731804
// If our children are themselves almost empty due to a mass-delete,
17741805
// they may need to be merged too (but only the oldLength-1 and its
17751806
// right sibling should need this).

0 commit comments

Comments
 (0)