-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathBPlusTree.java
More file actions
2436 lines (2194 loc) · 94.4 KB
/
BPlusTree.java
File metadata and controls
2436 lines (2194 loc) · 94.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package ds.bplus.bptree;
import ds.bplus.util.InvalidBTreeStateException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Collections;
import java.util.InvalidPropertiesFormatException;
import java.util.LinkedList;
import java.util.stream.Collectors;
@SuppressWarnings("WeakerAccess")
public class BPlusTree {
private TreeNode root;
private TreeNode aChild;
private RandomAccessFile treeFile;
private BPlusConfiguration conf;
private LinkedList<Long> freeSlotPool;
private LinkedList<Long> lookupPagesPool;
private long firstPoolNextPointer;
private long totalTreePages;
private long maxPageNumber;
private int deleteIterations;
private BPlusTreePerformanceCounter bPerf = null;
/**
* Super basic constructor, create everything using their
* default values...
*
* @throws IOException is thrown when we fail to open/create the binary tree file
*/
@SuppressWarnings("unused")
public BPlusTree() throws IOException, InvalidBTreeStateException {
this.conf = new BPlusConfiguration();
bPerf = new BPlusTreePerformanceCounter(false);
initializeCommon();
openFile("tree.bin", "rw+", conf);
}
/**
* Basic constructor that creates a B+ Tree instance based
* on an already generated configuration.
*
* Default I/O mode is to *truncate* file
*
* @param conf B+ Tree configuration instance
* @param bPerf performance counter class
* @throws IOException is thrown when we fail to open/create the binary tree file
*/
@SuppressWarnings("unused")
public BPlusTree(BPlusConfiguration conf,
BPlusTreePerformanceCounter bPerf)
throws IOException, InvalidBTreeStateException {
this.conf = conf;
this.bPerf = bPerf;
initializeCommon();
openFile("tree.bin", "rw+", conf);
}
/**
*
* This constructor allows us to create a B+ tree as before
* given our configuration but allows us to tweak the I/O flags
*
*
* @param conf B+ Tree configuration instance
* @param mode mode of file opening
* @param bPerf performance counter class
* @throws IOException is thrown when we fail to open/create the binary tree file
*/
@SuppressWarnings("unused")
public BPlusTree(BPlusConfiguration conf, String mode,
BPlusTreePerformanceCounter bPerf)
throws IOException, InvalidBTreeStateException {
this.conf = conf;
this.bPerf = bPerf;
initializeCommon();
openFile("tree.bin", mode, conf);
}
/**
* This constructor allows the most customization as it
* allows us to set the I/O flags as well as the file name
* of the resulting file.
*
* @param conf B+ Tree configuration instance
* @param mode I/O mode
* @param treeFilePath file path for the file
* @param bPerf performance counter class
* @throws IOException is thrown when we fail to open/create the binary tree file
*/
@SuppressWarnings("unused")
public BPlusTree(BPlusConfiguration conf, String mode,
String treeFilePath, BPlusTreePerformanceCounter bPerf)
throws IOException, InvalidBTreeStateException {
this.conf = conf;
this.bPerf = bPerf;
initializeCommon();
openFile(treeFilePath, mode, conf);
}
/**
* Insert the key into the tree while also providing the flexibility
* of having unique keys or not at will.
*
* @param key key to add
* @param value value of the key
* @param unique allow duplicates for this run?
* @throws IOException is thrown when any of the read/write ops fail.
* @throws InvalidBTreeStateException is thrown when there is an inconsistency in the tree blocks.
* @throws IllegalStateException is thrown we have a null tree
* @throws NumberFormatException is thrown when we have an invalid key value (we only allow >= 0 as keys)
*/
@SuppressWarnings("unused")
public void insertKey(long key, String value, boolean unique)
throws IOException, InvalidBTreeStateException,
IllegalStateException, NumberFormatException {
if(root == null)
{throw new IllegalStateException("Can't insert to null tree");}
if(key < 0)
{throw new NumberFormatException("Can't have negative keys, sorry.");}
value = conditionString(value);
// check if our root is full
if(root.isFull(conf)) {
// allocate a new *internal* node, to be placed as the
// *left* child of the new root
aChild = this.root;
TreeInternalNode node_buf = new TreeInternalNode(TreeNodeType.TREE_ROOT_INTERNAL,
generateFirstAvailablePageIndex(conf));
node_buf.addPointerAt(0, aChild.getPageIndex());
this.root = node_buf;
// split root.
splitTreeNode(node_buf, 0);
writeFileHeader(conf);
insertNonFull(node_buf, key, value, unique);
}
else
{insertNonFull(root, key, value, unique);}
bPerf.incrementTotalInsertions();
}
/**
*
* This function is based on the similar function prototype that
* is given by CLRS for B-Tree but is altered (quite a bit) to
* be able to be used for B+ Trees.
*
* The main difference is that when the split happens *all* keys
* are preserved and the first key of the right node is moved up.
*
* For example say we have the following (order is assumed to be 3):
*
* [ k1 k2 k3 k4 ]
*
* This split would result in the following:
*
* [ k3 ]
* / \
* / \
* / \
* [ k1 k2 ] [ k3 k4 ]
*
* This function requires at least *3* page writes plus the commit of
* the updated page count to the file header. In the case that after
* splitting we have a new root we must commit the new root index
* to the file header as well; this happens transparently inside
* writeNode method and is not explicitly done here.
*
* @param n internal node "parenting" the split
* @param index index in the node n that we need to add the median
*/
private void splitTreeNode(TreeInternalNode n, int index)
throws IOException, InvalidBTreeStateException {
// System.out.println("-- Splitting node with index: " +
// aChild.getPageIndex() + " of type: " +
// aChild.getNodeType().toString());
int setIndex;
TreeNode znode;
long keyToAdd;
TreeNode ynode = aChild; // x.c_{i}
if(ynode.isInternalNode()) {
TreeInternalNode zInternal,
yInternal = (TreeInternalNode) ynode;
zInternal = new TreeInternalNode(TreeNodeType.TREE_INTERNAL_NODE,
generateFirstAvailablePageIndex(conf));
bPerf.incrementTotalInternalNodes();
setIndex = conf.getTreeDegree()-1;
int i;
for(i = 0; i < setIndex; i++) {
zInternal.addToKeyArrayAt(i, yInternal.popKey());
zInternal.addPointerAt(i, yInternal.popPointer());
}
zInternal.addPointerAt(i, yInternal.popPointer());
//keyToAdd = ynode.getFirstKey();
keyToAdd = ynode.popKey();
zInternal.setCurrentCapacity(setIndex);
yInternal.setCurrentCapacity(setIndex);
// it it was the root, invalidate it and make it a regular internal node
if(yInternal.isRoot()) {
yInternal.setNodeType(TreeNodeType.TREE_INTERNAL_NODE);
bPerf.incrementRootSplits();
}
bPerf.incrementInternalNodeSplits();
// update pointer at n_{index+1}
n.addPointerAt(index, zInternal.getPageIndex());
// update key value at n[index]
n.addToKeyArrayAt(index, keyToAdd);
// adjust capacity
n.incrementCapacity(conf);
// update shared child pointer.
aChild = zInternal;
// update reference
znode = zInternal;
}
// we have a leaf
else {
TreeLeaf zLeaf,
yLeaf = (TreeLeaf) ynode,
afterLeaf;
zLeaf = new TreeLeaf(yLeaf.getNextPagePointer(),
yLeaf.getPageIndex(), TreeNodeType.TREE_LEAF,
generateFirstAvailablePageIndex(conf));
// update the previous pointer from the node after ynode
if(yLeaf.getNextPagePointer() != -1) {
afterLeaf = (TreeLeaf) readNode(yLeaf.getNextPagePointer());
afterLeaf.setPrevPagePointer(zLeaf.getPageIndex());
afterLeaf.writeNode(treeFile, conf, bPerf);
}
bPerf.incrementTotalLeaves();
// update pointers in ynode, only have to update next pointer
yLeaf.setNextPagePointer(zLeaf.getPageIndex());
setIndex = conf.getLeafNodeDegree()-1;
for(int i = 0; i < setIndex; i++) {
//long fk = ynode.getLastKey();
//long ovf1 = ((TreeLeaf)ynode).getLastOverflowPointer();
zLeaf.pushToKeyArray(yLeaf.removeLastKey());
zLeaf.pushToValueList(yLeaf.removeLastValue());
zLeaf.pushToOverflowList(yLeaf.removeLastOverflowPointer());
zLeaf.incrementCapacity(conf);
yLeaf.decrementCapacity(conf);
}
// it it was the root, invalidate it and make it a regular leaf
if(yLeaf.isRoot()) {
yLeaf.setNodeType(TreeNodeType.TREE_LEAF);
bPerf.incrementRootSplits();
}
bPerf.incrementTotalLeafSplits();
// update pointer at n_{index+1}
n.addPointerAt(index + 1, zLeaf.getPageIndex());
// update key value at n[index]
n.addToKeyArrayAt(index, zLeaf.getKeyAt(0));
// adjust capacity
n.incrementCapacity(conf);
// update reference
znode = zLeaf;
}
znode.setBeingDeleted(false);
// commit the changes
znode.writeNode(treeFile, conf, bPerf);
ynode.writeNode(treeFile, conf, bPerf);
n.writeNode(treeFile, conf, bPerf);
// commit page counts
updatePageIndexCounts(conf);
}
/**
* This function is responsible handling the creation of overflow pages. We have
* generally two distinct cases which are the following:
*
* * Create an overflow page directly from a B+ TreeLeaf.
* * Add an overflow page directly after an existing one.
*
* In both cases for convenience we update all the required metrics as well as
* push to the newly created page the required value.
*
* @param n node to add the page
* @param index this is only used in the case of a leaf
* @param value value to push in the new page
* @throws IOException is thrown when an I/O operation fails
* @throws InvalidBTreeStateException is thrown when there is an inconsistency in the blocks.
*/
private void createOverflowPage(TreeNode n, int index, String value)
throws IOException, InvalidBTreeStateException {
TreeOverflow novf;
if(n.isOverflow()) {
TreeOverflow ovf = (TreeOverflow)n;
novf = new TreeOverflow(-1L, ovf.getPageIndex(),
generateFirstAvailablePageIndex(conf));
// push the first value
novf.pushToValueList(value);
novf.incrementCapacity(conf);
// update overflow pointer to parent node
ovf.setNextPagePointer(novf.getPageIndex());
// set being deleted to false
novf.setBeingDeleted(false);
// commit changes to new overflow page
novf.writeNode(treeFile, conf, bPerf);
// commit changes to old overflow page
ovf.writeNode(treeFile, conf, bPerf);
} else if(n.isLeaf()) {
TreeLeaf l = (TreeLeaf)n;
novf = new TreeOverflow(-1L, l.getPageIndex(),
generateFirstAvailablePageIndex(conf));
// System.out.println("Creating overflow page with index: " + novf.getPageIndex() +
// " for key: " + l.getKeyAt(index));
// push the first value
novf.pushToValueList(value);
novf.incrementCapacity(conf);
// update overflow pointer to parent node
l.setOverflowPointerAt(index, novf.getPageIndex());
// set being deleted to false
novf.setBeingDeleted(false);
// commit changes to overflow page
novf.writeNode(treeFile, conf, bPerf);
// commit changes to leaf page
l.writeNode(treeFile, conf, bPerf);
// commit page counts
} else {
throw new InvalidBTreeStateException("Expected Leaf or Overflow, " +
"got instead: " + n.getNodeType().toString());
}
bPerf.incrementTotalOverflowPages();
// commit page counts
updatePageIndexCounts(conf);
}
/**
* Binary search implementation for tree blocks; if not found returns the lower/upper bound
* position instead based on `rank`.
* @param n node to search
* @param key key to search
* @param rank rank of the search (for lower/upper bound)
* @return the index of the bound or found key.
*/
private int binSearchBlock(TreeNode n, long key, Rank rank) {
return binSearchRec(n, 0, n.getCurrentCapacity() - 1, key, rank);
}
/**
* Binary search implementation for tree blocks.
*
* @param n node to search
* @param l left (lower-part) array index
* @param r right (upper-part) array index
* @param key key to search
* @param rank rank of the search (for lower/upper bound)
* @return the index of the bound or found key.
*/
private int binSearchRec(TreeNode n, int l, int r, long key, Rank rank) {
int m;
long mkey;
if (l > r) {
switch (rank) {
case Pred:
return l == 0 ? l : l - 1;
case Succ:
return l > 0 && l == n.getCurrentCapacity() ? l - 1 : l;
//case Exact: return l;
default:
return l;
}
} else {
m = (l + r) / 2;
mkey = n.getKeyAt(m);
}
if (mkey < key) {
return binSearchRec(n, m + 1, r, key, rank);
} else if (mkey > key) {
return binSearchRec(n, l, m - 1, key, rank);
} else { // this is equal
return Rank.PlusOne == rank ? m + 1 : m;
}
}
/**
* This function is inspired from the one given in CLRS for inserting a key to
* a B-Tree but as splitTreeNode has been (heavily) modified in order to be used
* in our B+ Tree. It supports handling duplicate keys (if enabled) as well.
*
* It is able to insert the (Key, Value) pairs using only one pass through the tree.
*
* @param n current node
* @param key key to add
* @param value value paired with the key
* @param unique allow duplicate entries for this time?
* @throws IOException is thrown when an I/O operation fails
*/
private void insertNonFull(TreeNode n, long key, String value, boolean unique)
throws IOException, InvalidBTreeStateException {
boolean useChild = true;
int i = binSearchBlock(n, key, Rank.PlusOne);
// check if we have a leaf
if(n.isLeaf()) {
TreeLeaf l = (TreeLeaf)n;
// before we add it, let's check if the key already exists
// and if it does pull up (or create) the overflow page and
// add the value there.
//
// Not that we do *not* add the key if we have a true unique flag
// this is to adjust for a corner case due to indexing
int iadj = (n.getCurrentCapacity() > 0 &&
i == 0 && n.getFirstKey() > key) ? i : i-1;
if(n.getCurrentCapacity() > 0 && n.getKeyAt(iadj) == key) {
if(unique) {
//System.out.println("Duplicate entry found and unique " +
// "flag enabled, can't add");
return;
}
//System.out.println("Duplicate found! Adding to overflow page!");
// overflow page does not exist, yet; time to create it!
if(l.getOverflowPointerAt(iadj) < 0) {
createOverflowPage(l, iadj, value);
}
// page already exists, so pull it and check if it has
// available space, if it does all is good; otherwise we
// pull the next overflow page or we create another one.
else {
TreeOverflow ovf =
(TreeOverflow) readNode(l.getOverflowPointerAt(iadj));
while(ovf.isFull(conf)) {
// check if we have more, if not create
if(ovf.getNextPagePointer() < 0)
// create page and return
{createOverflowPage(ovf, -1, value); return;}
// load the next page
else
{ovf = (TreeOverflow)readNode(ovf.getNextPagePointer());}
}
// if the loaded page is not full then add it.
ovf.pushToValueList(value);
ovf.incrementCapacity(conf);
ovf.writeNode(treeFile, conf, bPerf);
}
}
// we have a new key insert
else {
// now add the (Key, Value) pair
l.addToValueList(i, value);
l.addToKeyArrayAt(i, key);
// also create a NULL overflow pointer
l.addToOverflowList(i, -1L);
l.incrementCapacity(conf);
// commit the changes
l.writeNode(treeFile, conf, bPerf);
}
} else {
// This requires a bit of explanation; the above while loop
// starts initially from the *end* key parsing the *right*
// child, so initially it's like this:
//
// Step 0:
//
// Key Array: | - | - | - | x |
// Pointer Array: | - | - | - | - | x |
//
// Now if the while loop stops there, we have a *right* child
// pointer, but should it continues we get the following:
//
// Step 1:
//
// Key Array: | - | - | x | - |
// Pointer Array: | - | - | - | x | - |
//
// and finally we reach the special case where we have the
// following:
//
// Final step:
//
// Key Array: | x | - | - | - |
// Pointer Array: | x | - | - | - | - |
//
//
// In this case we have a *left* pointer, which can be
// quite confusing initially... hence the changed naming.
//
//
TreeInternalNode inode = (TreeInternalNode)n;
aChild = readNode(inode.getPointerAt(i));
if (aChild.isOverflow() || aChild.isLookupPageOverflowNode()) {
throw new InvalidBTreeStateException("aChild can't be overflow node");
}
TreeNode nextAfterAChild = null;
if(aChild.isFull(conf)) {
splitTreeNode(inode, i);
if (key >= n.getKeyAt(i)) {
useChild = false;
nextAfterAChild = readNode(inode.getPointerAt(i+1));
}
}
insertNonFull(useChild ? aChild : nextAfterAChild, key, value, unique);
}
}
/**
* Function to parse the overflow pages specifically for the range queries
*
* @param l leaf which contains the key with the overflow page
* @param index index of the key
* @param res where to store the results
* @throws IOException is thrown when an I/O operation fails
*/
private void parseOverflowPages(TreeLeaf l, int index, RangeResult res)
throws IOException {
TreeOverflow ovfPage = (TreeOverflow)readNode(l.getOverflowPointerAt(index));
int icap = 0;
while(icap < ovfPage.getCurrentCapacity()) {
res.getQueryResult().add(new KeyValueWrapper(l.getKeyAt(index),
ovfPage.getValueAt(icap)));
icap++;
// check if we have more pages
if(icap == ovfPage.getCurrentCapacity() &&
ovfPage.getNextPagePointer() != -1L) {
ovfPage = (TreeOverflow)readNode(ovfPage.getNextPagePointer());
icap = 0;
}
}
}
/**
* Handle range search queries with a bit of twist on how we handle duplicate keys.
*
* We have two basic cases depending duplicate keys, which is basically whether
* we actually return them or not.
*
* @param minKey min key of the range
* @param maxKey max key of the range
* @param unique return only *first* encounter of the (Key, Value) pairs or all?
* @return the results packed in a neat class for handling
* @throws IOException is thrown when an I/O operation fails
*/
public RangeResult rangeSearch(long minKey, long maxKey, boolean unique)
throws IOException, InvalidBTreeStateException {
SearchResult sMin = searchKey(minKey, unique);
SearchResult sMax;
RangeResult rangeQueryResult = new RangeResult();
if(sMin.isFound()) {
// read up until we find a key that's greater than maxKey
// or the last entry.
int i = sMin.getIndex();
while(sMin.getLeaf().getKeyAt(i) <= maxKey) {
rangeQueryResult.getQueryResult().
add(new KeyValueWrapper(sMin.getLeaf().getKeyAt(i),
sMin.getLeaf().getValueAt(i)));
// check if we have an overflow page
if(!unique && sMin.getLeaf().getOverflowPointerAt(i) != -1)
{parseOverflowPages(sMin.getLeaf(), i, rangeQueryResult);}
i++;
// check if we need to read the next block
if(i == sMin.getLeaf().getCurrentCapacity()) {
// check if we have a next node to load.
if(sMin.getLeaf().getNextPagePointer() < 0)
// if not just break the loop
{break;}
sMin.setLeaf((TreeLeaf)readNode(sMin.getLeaf().getNextPagePointer()));
i = 0;
}
}
}
// this is the case where both searches might fail to find something, but
// we *might* have something between in the given range. To account for
// that even if we have *not* found something we will return those results
// instead. For example say we have a range of [2, 5] and we only have keys
// from [3, 4], thus both searches for min and max would fail to find a
// matching key in both cases. Thing is to account for that *both* results
// will be stopped at the first key that is less than min and max values
// given even if we did not find anything.
else {
sMax = searchKey(maxKey, unique);
int i = sMax.getIndex();
while(i >= 0 && sMax.getLeaf().getKeyAt(i) >= minKey) {
rangeQueryResult.getQueryResult().
add(new KeyValueWrapper(sMax.getLeaf().getKeyAt(i),
sMax.getLeaf().getValueAt(i)));
// check if we have an overflow page
if(!unique && sMax.getLeaf().getOverflowPointerAt(i) != -1)
{parseOverflowPages(sMax.getLeaf(), i, rangeQueryResult);}
i--;
// check if we need to read the next block
if(i < 0) {
// check if we do have another node to load
if(sMax.getLeaf().getPrevPagePointer() < 0)
// if not just break the loop
{break;}
sMax.setLeaf((TreeLeaf)readNode(sMax.getLeaf().getPrevPagePointer()));
// set it to max length
i = sMax.getLeaf().getCurrentCapacity()-1;
}
}
}
bPerf.incrementTotalRangeQueries();
// finally return the result list (empty or not)
return(rangeQueryResult);
}
/**
* Search inside the B+ Tree data structure for the requested key; based on the
* unique flag we have two choices which are the following:
*
* * unique flag true: return the *first* value that matches the given key
* * unique flag false: return *all* the values that match the given key
*
* The second one including a couple more page accesses as if the key has any
* duplicates they will be stored in one or multiple overflow pages depending
* on the number of duplicates. Hence we have to pay for those disk accesses
* as well.
*
* @param key key to match
* @param unique return *all* matching (Key, Value) pairs or the *first* found
* @return the search result
* @throws IOException is thrown when an I/O operation fails
* @throws InvalidBTreeStateException is thrown when there are inconsistencies in the blocks.
*/
@SuppressWarnings("unused")
public SearchResult searchKey(long key, boolean unique)
throws IOException, InvalidBTreeStateException {
bPerf.incrementTotalSearches();
return(searchKey(this.root, key, unique));
}
/**
* This function performs the actual search as described in searchKey description
* and is recursively called until we reach a leaf.
*
* @param node the node to poke into
* @param key key that we want to match
* @param unique unique results?
* @return the search result
* @throws IOException is thrown when an I/O operation fails
*/
private SearchResult searchKey(TreeNode node, long key, boolean unique)
throws IOException {
// search for the key
int i = binSearchBlock(node, key, Rank.Exact);
// check if we found it
if(node.isLeaf()) {
//i--;
if(i >= 0 && i < node.getCurrentCapacity() && key == node.getKeyAt(i)) {
// we found the key, depending on the unique flag handle accordingly
if(unique || ((TreeLeaf)node).getOverflowPointerAt(i) == -1L )
{return(new SearchResult((TreeLeaf)node, i, true));}
// handle the case of duplicates where actual overflow pages exist
else {
TreeLeaf lbuf = (TreeLeaf)node;
TreeOverflow ovfBuf = (TreeOverflow)readNode(lbuf.getOverflowPointerAt(i));
LinkedList<String> ovfList = new LinkedList<>();
// add the current one
ovfList.add(lbuf.getValueAt(i));
int icap = 0;
// loop through all the overflow pages
while(icap < ovfBuf.getCurrentCapacity()) {
ovfList.add(ovfBuf.getValueAt(icap));
icap++;
// advance if we have another page
if(icap == ovfBuf.getCurrentCapacity() &&
ovfBuf.getNextPagePointer() != -1L) {
ovfBuf = (TreeOverflow)readNode(ovfBuf.getNextPagePointer());
icap = 0;
}
}
// now after populating the list return the search result
return(new SearchResult((TreeLeaf)node, i, ovfList));
}
}
else
// we found nothing, use the unique constructor anyway.
{return(new SearchResult((TreeLeaf)node, i, false));}
}
// probably it's an internal node, descend to a leaf
else {
// padding to account for the last pointer (if needed)
if(i != node.getCurrentCapacity() && key >= node.getKeyAt(i)) {i++;}
TreeNode t = readNode(((TreeInternalNode)node).getPointerAt(i));
return(searchKey(t, key, unique));
}
}
/**
* Function to delete a key from our tree... this function is again
* adopted from CLRS delete method but this was basically written
* from scratch and is loosely based on the actual notes.
*
* -- firstly it deletes the keys (all or first depending on unique flag)
* -- secondly it updates the pool of empty pages
* -- thirdly performs merges if necessary (it capacity falls < degree-1)
* -- finally condenses the file size depending on load
*
* @param key key to delete
* @param unique unique deletions?
* @return the number of deleted keys
* @throws IOException is thrown when an I/O operation fails
* @throws InvalidBTreeStateException is thrown when there are inconsistencies in the blocks.
*/
@SuppressWarnings("unused")
public DeleteResult deleteKey(long key, boolean unique)
throws IOException, InvalidBTreeStateException {
if(root.isEmpty()) {
return (new DeleteResult(key, (LinkedList<String>) null));
} else
{return(deleteKey(root, null, -1, -1, key, unique));}
}
/**
* That function does the job as described above; to perform easily the deletions
* we store *two* nodes instead on *one* in memory; the parent and the current.
* This also avoids adding information to the actual node file structure to account
* for the "parenting" link.
*
* @param current node that we currently probe
* @param parent parent of the current node
* @param key key to delete
* @param unique unique deletions?
* @return the number of deleted keys
* @throws IOException is thrown when an I/O operation fails
* @throws InvalidBTreeStateException is thrown when there are inconsistencies in the blocks.
*/
public DeleteResult deleteKey(TreeNode current, TreeInternalNode parent,
int parentPointerIndex, int parentKeyIndex,
long key, boolean unique)
throws IOException, InvalidBTreeStateException {
// check if we need to consolidate
if(current.isTimeToMerge(conf)) {
//System.out.println("Parent needs merging (internal node)");
TreeNode mres = mergeOrRedistributeTreeNodes(current, parent,
parentPointerIndex, parentKeyIndex);
if(mres != null) {
current = mres;
}
}
LinkedList<String> rvals;
// search for the key
int i = binSearchBlock(current, key, Rank.Succ);
// check if it's an internal node
if(current.isInternalNode()) {
// if it is, descend to a leaf
TreeInternalNode inode = (TreeInternalNode)current;
int idx = i;
// check if we are at the end
if(key >= current.getKeyAt(i)) {
idx++;
}
// read the next node
TreeNode next = readNode(inode.getPointerAt(idx));
// finally return the resulting set
return(deleteKey(next, inode, idx, i/*keyLocation*/, key, unique));
}
// the current node, is a leaf.
else if(current.isLeaf()) {
TreeLeaf l = (TreeLeaf)current;
// check if we actually found the key
if(i == l.getCurrentCapacity()) {
//System.out.println("Key with value: " + key +
// " not found, reached limits");
return (new DeleteResult(key, (LinkedList<String>) null));
} else if(key != l.getKeyAt(i)) {
//System.out.println("Key with value: " + key + " not found, key mismatch");
//throw new InvalidBTreeStateException("Key not found!");
return (new DeleteResult(key, (LinkedList<String>) null));
}
else {
//System.out.println("Found the key " + key + ", removing it");
rvals = new LinkedList<>();
// we we *have* to make a choice on where to make
// a read trade off.
// check if we have an overflow page
if(l.getOverflowPointerAt(i) != -1) {
TreeOverflow ovf = null;
TreeOverflow povf =
(TreeOverflow)readNode(l.getOverflowPointerAt(i));
// handle singular deletes
if(unique) {
// descend to the last page
while(povf.getNextPagePointer() != -1L) {
ovf = povf;
povf = (TreeOverflow)readNode(povf.getNextPagePointer());
}
// remove from the overflow page the value
rvals.add(povf.removeLastValue());
povf.decrementCapacity(conf);
// if the page is empty, delete it.
if(povf.isEmpty()) {
if (ovf == null) {
l.setOverflowPointerAt(i, -1L);
l.writeNode(treeFile, conf, bPerf);
}
else {
ovf.setNextPagePointer(-1L);
ovf.writeNode(treeFile, conf, bPerf);
}
// now delete the page
deletePage(povf.getPageIndex(), false);
}
// we don't have to delete the page, so let's
// update it instead.
else
{povf.writeNode(treeFile, conf, bPerf);}
// return the result
return(new DeleteResult(key, rvals));
}
// we have to delete all the values
else {
// here to save reads/writes we just
// "delete-as-we-read"
while(povf.getCurrentCapacity() > 0) {
rvals.add(povf.removeLastValue());
povf.decrementCapacity(conf);
// check if it's time to remove the page
if(povf.isEmpty()) {
deletePage(povf.getPageIndex(), false);
if(povf.getNextPagePointer() != -1L)
{povf = (TreeOverflow)readNode(povf.getNextPagePointer());}
}
}
}
}
}
// we reached here because either we have no overflow page
// or non-unique deletes with overflow pages. We should
// reach this point after we purged all the overflow pages.
rvals.add(((TreeLeaf)current).removeEntryAt(i, conf));
current.writeNode(treeFile, conf, bPerf);
}
else {
throw new IllegalStateException("Read unknown or " +
"overflow page while descending");
}
return(new DeleteResult(key, rvals));
}
/**
* Check if the node has the specified parent
*
* @param node node can be internal or leaf
* @param parent parent is always internal node
* @param pindex index to check
* @return true if it is, false if it's not
*/
private boolean isParent(TreeNode node, TreeInternalNode parent, int pindex) {
return(parent.getCurrentCapacity() >= pindex && pindex >= 0 &&
(node.getPageIndex() == parent.getPointerAt(pindex)));
}
/**
* Simple helper function to check if we can re-distribute the node
* values.
*
* @param with node to check the capacity
* @return the number of positions to check
* @throws InvalidBTreeStateException is thrown when there are inconsistencies in the blocks.
*/
private int canRedistribute(TreeNode with)
throws InvalidBTreeStateException {
if(with != null) {
if(with.isInternalNode()) {
if(isValidAfterRemoval(((TreeInternalNode)with), 1)) {return(1);}
} else if(with.isLeaf()) {
if(isValidAfterRemoval(((TreeLeaf)with), 1)) {return(1);}
} else
{throw new InvalidBTreeStateException("Not leaf or internal node found");}
}
return(-1);
}
/**
* Check if the internal node fulfills the B+ Tree invariant after removing
* <code>remove</code> number of elements
*
* @param node node to check
* @param remove elements to be removed
* @return true if does, false if it fails the condition
*/
private boolean isValidAfterRemoval(TreeInternalNode node, int remove)
{return((node.getCurrentCapacity()-remove) >= conf.getMinInternalNodeCapacity());}
/**
* Check if the leaf node fulfills the B+ Tree invariant after removing
* <code>remove</code> number of elements
*
* @param node node to check
* @param remove elements to be removed
* @return true if does, false if it fails the condition
*/
private boolean isValidAfterRemoval(TreeLeaf node, int remove)
{return((node.getCurrentCapacity()-remove) >= conf.getMinLeafNodeCapacity());}
/**
* Function that is responsible to redistribute values among two leaf nodes
* while updating the referring key of the parent node (always an internal node).
*
*
* We have two distinct cases which are the following:
*
* This case is when we use the prev pointer:
*
* |--------| <----- |--------|
* | with | | to |
* |--------| -----> |--------|
*
* In this case we *remove* the *last* n elements from with
* and *push* them (in the order removed) into the destination node
*
* The parent key-pointer is updated with the first value of the
* receiving node.
*
* The other case is when we use the next pointer:
*
* |--------| <----- |--------|
* | to | | with |
* |--------| -----> |--------|
*
* In this case we *remove* the *first* n elements from with and
* add them *last* (in the order removed) into the destination node.
*
* The parent key-pointer is updated with the first value of the
* node we retrieved the values.
*
*
*
* @param to node to receive (Key, Value) pairs
* @param with node that we take the (Key, Value) pairs
* @param left if left is true, then we use prev leaf else next
* @param parent the internal node parenting both
* @param parentKeyIndex index of the parent that refers to this pair
* @throws IOException is thrown when an I/O operation fails
* @throws InvalidBTreeStateException is thrown when there are inconsistencies in the blocks.
*/
private void redistributeNodes(TreeLeaf to, TreeLeaf with,
boolean left, TreeInternalNode parent,
int parentKeyIndex)
throws IOException, InvalidBTreeStateException {
long key;
// handle the case when redistributing using prev