├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── davidmoten │ └── bplustree │ ├── BPlusTree.java │ ├── Entry.java │ ├── LargeByteBuffer.java │ ├── Serializer.java │ └── internal │ ├── Factory.java │ ├── FactoryProvider.java │ ├── LargeMappedByteBuffer.java │ ├── Leaf.java │ ├── Node.java │ ├── NonLeaf.java │ ├── Options.java │ ├── Split.java │ ├── Util.java │ ├── file │ ├── FactoryFile.java │ ├── LeafFile.java │ ├── NodeFile.java │ └── NonLeafFile.java │ ├── memory │ ├── FactoryMemory.java │ ├── LeafMemory.java │ └── NonLeafMemory.java │ └── util │ ├── LazyList.java │ └── Pool.java └── test └── java └── com └── github └── davidmoten └── bplustree ├── BPlusTreeFileTest.java ├── BPlusTreeTest.java ├── Benchmarks.java ├── FactoryFileTest.java ├── LargeByteBufferDelegating.java ├── NodeWrapper.java ├── SerializerTest.java ├── Testing.java └── internal ├── LargeMappedByteBufferTest.java └── NonLeafTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | uses: davidmoten/workflows/.github/workflows/ci.yml@master 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | 14 | .classpath 15 | .project 16 | .settings 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bplustree 2 |
3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.davidmoten/bplustree/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/com.github.davidmoten/bplustree)
4 | [![codecov](https://codecov.io/gh/davidmoten/bplustree/branch/master/graph/badge.svg)](https://codecov.io/gh/davidmoten/bplustree)
5 | 6 | **Status:** beta 7 | 8 | Disk based B+-tree in java using memory mapped files (size limited only by available disk space). 9 | 10 | ## Features 11 | * size only limited by available disk 12 | * supports range queries 13 | * optionally supports duplicate keys 14 | * much faster read and write than H2 file-based database (because no transactions and different persistence model). 15 | 16 | ## Requirements 17 | 18 | * fast read time for range queries by time and key 19 | * fast insert time 20 | * single node implementation (not distributed) 21 | * use memory-mapped files for speed 22 | * fixed size keys 23 | * variable size values 24 | * very large size storage (>2GB of keys or values) 25 | * optimized for insert in approximate index order 26 | * single threaded 27 | * no transactions 28 | * delete not supported (?) 29 | 30 | ## Getting started 31 | Add this to your pom.xml: 32 | 33 | ```xml 34 | 35 | com.github.davidmoten 36 | bplustree 37 | VERSION_HERE 38 | 39 | ``` 40 | 41 | ## Example 42 | 43 | Lets create a file based index of timestamped strings (for example lines from a log). Timestamps don't have to be unique. 44 | 45 | ```java 46 | BPlusTree tree = 47 | BPlusTree 48 | .file() 49 | .directory(indexDirectory) 50 | .maxLeafKeys(32) 51 | .maxNonLeafKeys(8) 52 | .segmentSizeMB(1) 53 | .keySerializer(Serializer.LONG) 54 | .valueSerializer(Serializer.utf8()) 55 | .naturalOrder(); 56 | 57 | // insert some values 58 | tree.insert(1000L, "hello"); 59 | tree.insert(2000L, "there"); 60 | 61 | // search the tree for values with keys between 0 and 3000 62 | // and print out key value pairs 63 | tree.findEntries(0, 3000).forEach(System.out.println); 64 | 65 | // search the tree for values with keys between 0 and 3000 66 | // and print out values only 67 | tree.find(0, 3000).forEach(System.out.println); 68 | ``` 69 | ## Duplicate keys 70 | Duplicate keys are allowed by default. You can force overwrite of keyed values by setting `.unique(false)` in the builder. 71 | 72 | Note that for efficiency values with duplicate keys are entered into the tree in reverse insert order so to extract the values retaining insert order a special method is used: 73 | 74 | ```java 75 | tree.findOrderPreserving(0, 3000); 76 | ``` 77 | 78 | ## Using bplustree for String keys 79 | Suppose you want to create a B-+ tree with String keys and those keys can have effectively arbitrary length. Keys are stored as fixed size records (unlike values which can be arbitrary in length). You can use hashes to get good find performance and keep the keys small (4 bytes of hash code) by making a tree of type: 80 | 81 | ```java 82 | BPlusTree tree = ... 83 | ``` 84 | So you insert the String hashcode in the key and combine the String with the value. You find records using the hashcode of the String key and then filter the results based on an exact match of the String component of StringAndValue. 85 | 86 | ## Design 87 | B+-tree index is stored across multiple files (of fixed size). Pointers to values are stored in the tree and the values are stored across a separate set of files (of fixed size). 88 | 89 | A LargeByteBuffer abstracts access via Memory Mapped Files to a set of files (ByteBuffer only offers int positions which restricts size to 2GB, LargeByteBuffer offers long positions with no effective limit of size (apart from available disk)). 90 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.github.davidmoten 7 | sonatype-parent 8 | 0.2.3 9 | 10 | bplustree 11 | 0.1.5-SNAPSHOT 12 | ${project.artifactId} 13 | B+-tree using Memory Mapped Files, supports range queries and duplicate keys 14 | jar 15 | 16 | http://github.com/davidmoten/bplustree 17 | 18 | 19 | UTF-8 20 | UTF-8 21 | 1.8 22 | 1.37 23 | 3.5.1 24 | 25 | 2.7 26 | 3.6.0 27 | 2.5.4 28 | 3.11.2 29 | 3.26.0 30 | 2.1 31 | 3.9.0 32 | 3.6.0 33 | 3.2.1 34 | 3.21.0 35 | 2.2 36 | ${project.build.directory}/target/coverage-reports 37 | 38 | 39 | 40 | 41 | 42 | The Apache Software License, Version 2.0 43 | http://www.apache.org/licenses/LICENSE-2.0.txt 44 | repo 45 | A business-friendly OSS license 46 | 47 | 48 | 49 | 50 | Travis 51 | https://travis-ci.org/davidmoten/bplustree 52 | 53 | 54 | 55 | GitHub 56 | https://github.com/davidmoten/bplustree/issues 57 | 58 | 59 | 2019 60 | 61 | 62 | dave 63 | Dave Moten 64 | https://github.com/davidmoten/ 65 | 66 | architect 67 | developer 68 | 69 | +10 70 | 71 | 72 | 73 | 74 | scm:git:https://github.com/davidmoten/bplustree.git 75 | scm:git:https://github.com/davidmoten/bplustree.git 76 | scm:git:https://github.com:davidmoten/bplustree.git 77 | HEAD 78 | 79 | 80 | 81 | 82 | 83 | com.github.davidmoten 84 | guava-mini 85 | 0.1.7 86 | 87 | 88 | 89 | com.github.davidmoten 90 | kool 91 | 0.1.36 92 | test 93 | 94 | 95 | 96 | junit 97 | junit 98 | 4.13.2 99 | test 100 | 101 | 102 | 103 | com.github.davidmoten 104 | junit-extras 105 | 0.4 106 | test 107 | 108 | 109 | 110 | org.openjdk.jmh 111 | jmh-core 112 | ${jmh.version} 113 | test 114 | 115 | 116 | 117 | org.openjdk.jmh 118 | jmh-generator-annprocess 119 | ${jmh.version} 120 | test 121 | 122 | 123 | org.mapdb 124 | mapdb 125 | 3.1.0 126 | test 127 | 128 | 129 | 130 | 131 | 132 | 133 | maven-compiler-plugin 134 | 3.14.0 135 | 136 | ${maven.compiler.target} 137 | ${maven.compiler.target} 138 | 139 | 140 | 141 | 142 | org.jacoco 143 | jacoco-maven-plugin 144 | 0.8.12 145 | 146 | 147 | 148 | prepare-agent 149 | 150 | 151 | 152 | report 153 | test 154 | 155 | report 156 | 157 | 158 | 159 | 160 | 161 | 162 | maven-site-plugin 163 | ${m3.site.version} 164 | 165 | 166 | attach-descriptor 167 | 168 | attach-descriptor 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 183 | 184 | org.apache.maven.plugins 185 | maven-jxr-plugin 186 | ${jxr.version} 187 | 188 | true 189 | 190 | 191 | 192 | org.apache.maven.plugins 193 | maven-checkstyle-plugin 194 | ${checkstyle.version} 195 | 196 | true 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-pmd-plugin 202 | ${pmd.version} 203 | 204 | ${maven.compiler.target} 205 | true 206 | 207 | 208 | 211 | 212 | org.codehaus.mojo 213 | jdepend-maven-plugin 214 | ${jdepend.version} 215 | 216 | 217 | org.apache.maven.plugins 218 | maven-project-info-reports-plugin 219 | ${project.info.version} 220 | 221 | false 222 | false 223 | 224 | 225 | 226 | org.codehaus.mojo 227 | taglist-maven-plugin 228 | ${taglist.version} 229 | 230 | 231 | org.apache.maven.plugins 232 | maven-javadoc-plugin 233 | ${javadoc.version} 234 | 235 | true 236 | 237 | 238 | 240 | 243 | 244 | 245 | 246 | 247 | 248 | benchmark 249 | 250 | 251 | org.openjdk.jmh 252 | jmh-generator-annprocess 253 | ${jmh.version} 254 | test 255 | 256 | 257 | 258 | 259 | 260 | org.codehaus.mojo 261 | exec-maven-plugin 262 | ${exec.version} 263 | 264 | 265 | run-benchmarks 266 | integration-test 267 | 268 | exec 269 | 270 | 271 | test 272 | java 273 | 274 | -classpath 275 | 276 | org.openjdk.jmh.Main 277 | 278 | -f 279 | 1 280 | 290 | -jvmArgs 291 | -Xmx512m 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/BPlusTree.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.io.File; 4 | import java.io.PrintStream; 5 | import java.util.ArrayList; 6 | import java.util.Comparator; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.NoSuchElementException; 10 | import java.util.function.BiFunction; 11 | 12 | import com.github.davidmoten.bplustree.internal.Factory; 13 | import com.github.davidmoten.bplustree.internal.FactoryProvider; 14 | import com.github.davidmoten.bplustree.internal.Leaf; 15 | import com.github.davidmoten.bplustree.internal.Node; 16 | import com.github.davidmoten.bplustree.internal.NonLeaf; 17 | import com.github.davidmoten.bplustree.internal.Options; 18 | import com.github.davidmoten.bplustree.internal.Split; 19 | import com.github.davidmoten.bplustree.internal.file.FactoryFile; 20 | import com.github.davidmoten.bplustree.internal.memory.FactoryMemory; 21 | import com.github.davidmoten.guavamini.Preconditions; 22 | import com.github.davidmoten.guavamini.annotations.VisibleForTesting; 23 | 24 | public final class BPlusTree implements AutoCloseable { 25 | 26 | private static final int MAX_KEYS_NOT_SPECIFIED = -1; 27 | private static final int DEFAULT_NUM_KEYS = 4; 28 | 29 | private final Options options; 30 | private final Factory factory; 31 | 32 | /** 33 | * Pointer to the root node. It may be a leaf or an inner node, but it is never 34 | * null. 35 | */ 36 | private Node root; 37 | 38 | /** Create a new empty tree. */ 39 | private BPlusTree(int maxLeafKeys, int maxInnerKeys, boolean uniqueKeys, Runnable onClose, 40 | Comparator comparator, FactoryProvider factoryProvider) { 41 | this.options = new Options(maxLeafKeys, maxInnerKeys, uniqueKeys, comparator, factoryProvider); 42 | this.factory = options.factoryProvider().createFactory(options); 43 | this.root = factory.loadOrCreateRoot(); 44 | factory.root(root); 45 | } 46 | 47 | public static Builder memory() { 48 | return new Builder(); 49 | } 50 | 51 | public static BuilderFile file() { 52 | return new BuilderFile(); 53 | } 54 | 55 | public static final class BuilderFile { 56 | 57 | BuilderFile() { 58 | // restrict visibility 59 | } 60 | 61 | public BuilderFile2 directory(String directory) { 62 | Preconditions.checkNotNull(directory); 63 | return directory(new File(directory)); 64 | } 65 | 66 | public BuilderFile2 directory(File directory) { 67 | Preconditions.checkNotNull(directory); 68 | return new BuilderFile2(directory); 69 | } 70 | 71 | } 72 | 73 | public static final class BuilderFile2 { 74 | File directory; 75 | int segmentSizeBytes = 50 * 1024 * 1024; 76 | int maxLeafKeys = MAX_KEYS_NOT_SPECIFIED; 77 | int maxNonLeafKeys = MAX_KEYS_NOT_SPECIFIED; 78 | boolean uniqueKeys = false; 79 | Runnable onClose; 80 | 81 | BuilderFile2(File directory) { 82 | this.directory = directory; 83 | } 84 | 85 | public BuilderFile2 clearDirectory() { 86 | clearDirectory(directory); 87 | return this; 88 | } 89 | 90 | private static void clearDirectory(File directory) { 91 | if (directory.exists()) { 92 | for (File f : directory.listFiles()) { 93 | f.delete(); 94 | } 95 | } else { 96 | directory.mkdirs(); 97 | } 98 | } 99 | 100 | public BuilderFile2 segmentSizeBytes(int size) { 101 | Preconditions.checkArgument(size > 0); 102 | this.segmentSizeBytes = size; 103 | return this; 104 | } 105 | 106 | public BuilderFile2 segmentSizeMB(int size) { 107 | Preconditions.checkArgument(size > 0); 108 | return segmentSizeBytes(size * 1024 * 1024); 109 | } 110 | 111 | public BuilderFile2 maxLeafKeys(int maxLeafKeys) { 112 | this.maxLeafKeys = maxLeafKeys; 113 | return this; 114 | } 115 | 116 | public BuilderFile2 maxNonLeafKeys(int maxNonLeafKeys) { 117 | this.maxNonLeafKeys = maxNonLeafKeys; 118 | return this; 119 | } 120 | 121 | public BuilderFile2 uniqueKeys(boolean uniqueKeys) { 122 | this.uniqueKeys = uniqueKeys; 123 | return this; 124 | } 125 | 126 | public BuilderFile2 uniqueKeys() { 127 | return uniqueKeys(true); 128 | } 129 | 130 | public BuilderFile2 maxKeys(int maxKeys) { 131 | maxLeafKeys(maxKeys); 132 | return maxNonLeafKeys(maxKeys); 133 | } 134 | 135 | public BuilderFile2 deleteOnClose() { 136 | return onClose(() -> clearDirectory(directory)); 137 | } 138 | 139 | public BuilderFile2 onClose(Runnable onClose) { 140 | this.onClose = onClose; 141 | return this; 142 | } 143 | 144 | public BuilderFile3 keySerializer(Serializer serializer) { 145 | Preconditions.checkArgument(serializer.maxSize() > 0, "key serializer must have non-zero maxSize"); 146 | return new BuilderFile3(this, serializer); 147 | } 148 | } 149 | 150 | public static final class BuilderFile3 { 151 | 152 | private BuilderFile2 b; 153 | private final Serializer keySerializer; 154 | 155 | BuilderFile3(BuilderFile2 b, Serializer serializer) { 156 | this.b = b; 157 | this.keySerializer = serializer; 158 | } 159 | 160 | public BuilderFile4 valueSerializer(Serializer valueSerializer) { 161 | return new BuilderFile4(b, keySerializer, valueSerializer); 162 | } 163 | } 164 | 165 | public static final class BuilderFile4 { 166 | 167 | private final BuilderFile2 b; 168 | private final Serializer keySerializer; 169 | private final Serializer valueSerializer; 170 | 171 | BuilderFile4(BuilderFile2 b, Serializer keySerializer, Serializer valueSerializer) { 172 | this.b = b; 173 | this.keySerializer = keySerializer; 174 | this.valueSerializer = valueSerializer; 175 | } 176 | 177 | @SuppressWarnings("unchecked") 178 | public BPlusTree naturalOrder() { 179 | return comparator((Comparator) (Comparator) Comparator.naturalOrder()); 180 | } 181 | 182 | public BPlusTree comparator(Comparator comparator) { 183 | FactoryProvider factoryProvider = options -> new FactoryFile(options, b.directory, 184 | keySerializer, valueSerializer, b.segmentSizeBytes, b.onClose); 185 | 186 | if (b.maxLeafKeys == MAX_KEYS_NOT_SPECIFIED) { 187 | if (b.maxNonLeafKeys == MAX_KEYS_NOT_SPECIFIED) { 188 | b.maxLeafKeys = DEFAULT_NUM_KEYS; 189 | b.maxNonLeafKeys = DEFAULT_NUM_KEYS; 190 | } else { 191 | b.maxLeafKeys = b.maxNonLeafKeys; 192 | } 193 | } else if (b.maxNonLeafKeys == MAX_KEYS_NOT_SPECIFIED) { 194 | b.maxNonLeafKeys = b.maxLeafKeys; 195 | } 196 | 197 | return new BPlusTree(b.maxLeafKeys, b.maxNonLeafKeys, b.uniqueKeys, b.onClose, comparator, 198 | factoryProvider); 199 | } 200 | 201 | } 202 | 203 | public static final class Builder { 204 | 205 | private int maxLeafKeys = MAX_KEYS_NOT_SPECIFIED; 206 | private int maxInnerKeys = MAX_KEYS_NOT_SPECIFIED; 207 | 208 | private boolean uniqueKeys = false; 209 | 210 | Builder() { 211 | // prevent instantiation 212 | } 213 | 214 | public Builder maxLeafKeys(int maxLeafKeys) { 215 | this.maxLeafKeys = maxLeafKeys; 216 | return this; 217 | } 218 | 219 | public Builder maxNonLeafKeys(int maxInnerKeys) { 220 | this.maxInnerKeys = maxInnerKeys; 221 | return this; 222 | } 223 | 224 | public Builder maxKeys(int maxKeys) { 225 | maxLeafKeys(maxKeys); 226 | return maxNonLeafKeys(maxKeys); 227 | } 228 | 229 | public Builder uniqueKeys(boolean uniqueKeys) { 230 | this.uniqueKeys = uniqueKeys; 231 | return this; 232 | } 233 | 234 | @SuppressWarnings("unchecked") 235 | public BPlusTree naturalOrder() { 236 | return comparator((Comparator) (Comparator) Comparator.naturalOrder()); 237 | } 238 | 239 | public Builder uniqueKeys() { 240 | return uniqueKeys(true); 241 | } 242 | 243 | public BPlusTree comparator(Comparator comparator) { 244 | FactoryProvider factoryProvider = options -> new FactoryMemory(options); 245 | if (maxLeafKeys == MAX_KEYS_NOT_SPECIFIED) { 246 | if (maxInnerKeys == MAX_KEYS_NOT_SPECIFIED) { 247 | maxLeafKeys = DEFAULT_NUM_KEYS; 248 | maxInnerKeys = DEFAULT_NUM_KEYS; 249 | } else { 250 | maxLeafKeys = maxInnerKeys; 251 | } 252 | } else if (maxInnerKeys == MAX_KEYS_NOT_SPECIFIED) { 253 | maxInnerKeys = maxLeafKeys; 254 | } 255 | 256 | return new BPlusTree(maxLeafKeys, maxInnerKeys, uniqueKeys, null, comparator, factoryProvider); 257 | } 258 | 259 | } 260 | 261 | public void insert(K key, V value) { 262 | Split result = root.insert(key, value); 263 | if (result != null) { 264 | // The root is split into two parts. 265 | // We create a new root pointing to them 266 | NonLeaf node = // 267 | factory // 268 | .createNonLeaf(); 269 | node.setNumKeys(1); 270 | node.setKey(0, result.key); 271 | node.setChild(0, result.left); 272 | node.setChild(1, result.right); 273 | root = node; 274 | factory.root(root); 275 | // commit changing the root node which shouldn't happen very often 276 | factory.commit(); 277 | } 278 | } 279 | 280 | /** 281 | * Looks for the given key. If it is not found, it returns null. If it is found, 282 | * it returns the associated value. 283 | * 284 | * @param key 285 | * key to find 286 | * @return the first matching value or null if not found 287 | */ 288 | public V findFirst(K key) { 289 | Leaf leaf = findFirstLeaf(key); 290 | int idx = leaf.getLocation(key); 291 | if (idx < leaf.numKeys() && leaf.key(idx).equals(key)) { 292 | return leaf.value(idx); 293 | } else { 294 | return null; 295 | } 296 | } 297 | 298 | public Iterable find(K key) { 299 | 300 | return find(key, key, true); 301 | } 302 | 303 | private Leaf findFirstLeaf(K key) { 304 | Node node = root; 305 | while (node instanceof NonLeaf) { // need to traverse down to the leaf 306 | NonLeaf inner = (NonLeaf) node; 307 | int idx = inner.getLocation(key); 308 | node = inner.child(idx); 309 | } 310 | return (Leaf) node; 311 | } 312 | 313 | /** 314 | * Returns a key ordered sequence of values whose keys are >= start and < 315 | * finish. Note that the insert order of duplicate keys may not be preserved. 316 | * 317 | * @param startInclusive 318 | * inclusive end of search 319 | * @param finishExclusive 320 | * exclusive end of search 321 | * @return in-order sequence of values whose keys are >= start and < 322 | * finish 323 | */ 324 | public Iterable find(K startInclusive, K finishExclusive) { 325 | return find(startInclusive, finishExclusive, false); 326 | } 327 | 328 | public Iterable find(K startInclusive, K finish, boolean isFinishInclusive) { 329 | return find(startInclusive, finish, isFinishInclusive, (k, v) -> v); 330 | } 331 | 332 | public Iterable> findEntries(K startInclusive, K finishExclusive) { 333 | return findEntries(startInclusive, finishExclusive, false); 334 | } 335 | 336 | public Iterable> findEntries(K startInclusive, K finish, boolean isFinishInclusive) { 337 | return find(startInclusive, finish, isFinishInclusive, (k, v) -> Entry.create(k, v)); 338 | } 339 | 340 | public Iterable find(K startInclusive, K finish, boolean isFinishInclusive, 341 | BiFunction mapper) { 342 | return new Iterable() { 343 | 344 | @Override 345 | public Iterator iterator() { 346 | return new Iterator() { 347 | Leaf leaf = findFirstLeaf(startInclusive); 348 | int numKeys = leaf.numKeys(); 349 | int idx = leaf.getLocation(startInclusive); 350 | R value; 351 | 352 | @Override 353 | public boolean hasNext() { 354 | load(); 355 | return value != null; 356 | } 357 | 358 | @Override 359 | public R next() { 360 | load(); 361 | R v = value; 362 | value = null; 363 | if (v == null) { 364 | throw new NoSuchElementException(); 365 | } else { 366 | return v; 367 | } 368 | } 369 | 370 | private void load() { 371 | if (value != null) { 372 | return; 373 | } 374 | while (true) { 375 | if (leaf == null) { 376 | return; 377 | } else if (idx < numKeys) { 378 | K key = leaf.key(idx); 379 | int c = options.comparator().compare(key, finish); 380 | if (c < 0 || (c == 0 && isFinishInclusive)) { 381 | value = mapper.apply(key, leaf.value(idx)); 382 | idx++; 383 | } else { 384 | // don't search further 385 | leaf = null; 386 | } 387 | return; 388 | } else { 389 | leaf = leaf.next(); 390 | if (leaf != null) { 391 | numKeys = leaf.numKeys(); 392 | } 393 | idx = 0; 394 | } 395 | } 396 | } 397 | 398 | }; 399 | } 400 | 401 | }; 402 | } 403 | 404 | // /** 405 | // * For the situation when uniqueness is false, when entries are inserted with 406 | // * the same key they are inserted before the last entry. As a consequence if 407 | // we 408 | // * want to preserve the insert order in the returned values from a find then 409 | // we 410 | // * need to collect entries with the same key and then emit them in reverse 411 | // * order. If there are a lot of keys with the same value then an 412 | // * {@link OutOfMemoryError} might be thrown. 413 | // * 414 | // * @param startInclusive start of the key range, inclusive 415 | // * @param finishExclusive finish of the key range, exclusive 416 | // * @return values of entries in searched for key range preserving insert order 417 | // */ 418 | // public Iterable findOrderPreserving(K startInclusive, K finishExclusive) { 419 | // return findOrderPreserving(startInclusive, finishExclusive, false); 420 | // } 421 | // 422 | // private static final int VALUES_MAX_SIZE = 256; 423 | // 424 | // /** 425 | // * For the situation when uniqueness is false, when entries are inserted with 426 | // * the same key they are inserted before the last entry. As a consequence if 427 | // we 428 | // * want to preserve the insert order in the returned values from a find then 429 | // we 430 | // * need to collect entries with the same key and then emit them in reverse 431 | // * order. If there are a lot of keys with the same value then an 432 | // * {@link OutOfMemoryError} might be thrown. 433 | // * 434 | // * @param startInclusive start of the key range, inclusive 435 | // * @param finish finish of the key range 436 | // * @param isFinishInclusive if true then finish is inclusive otherwise 437 | // exclusive 438 | // * @return values of entries in searched for key range preserving insert order 439 | // */ 440 | // public Iterable findOrderPreserving(K startInclusive, K finish, boolean 441 | // isFinishInclusive) { 442 | // return findEntriesOrderPreserving(startInclusive, finish, isFinishInclusive, 443 | // (k, v) -> v); 444 | // } 445 | // 446 | // /** 447 | // * For the situation when uniqueness is false, when entries are inserted with 448 | // * the same key they are inserted before the last entry. As a consequence if 449 | // we 450 | // * want to preserve the insert order in the returned values from a find then 451 | // we 452 | // * need to collect entries with the same key and then emit them in reverse 453 | // * order. If there are a lot of keys with the same value then an 454 | // * {@link OutOfMemoryError} might be thrown. 455 | // * 456 | // * @param startInclusive start of the key range, inclusive 457 | // * @param finish finish of the key range 458 | // * @param isFinishInclusive if true then finish is inclusive otherwise 459 | // exclusive 460 | // * @return values of entries in searched for key range preserving insert order 461 | // */ 462 | // public Iterable> findEntriesOrderPreserving(K startInclusive, K 463 | // finish, 464 | // boolean isFinishInclusive) { 465 | // return findEntriesOrderPreserving(startInclusive, finish, isFinishInclusive, 466 | // (k, v) -> Entry.create(k, v)); 467 | // } 468 | // 469 | // /** 470 | // * For the situation when uniqueness is false, when entries are inserted with 471 | // * the same key they are inserted before the last entry. As a consequence if 472 | // we 473 | // * want to preserve the insert order in the returned values from a find then 474 | // we 475 | // * need to collect entries with the same key and then emit them in reverse 476 | // * order. If there are a lot of keys with the same value then an 477 | // * {@link OutOfMemoryError} might be thrown. 478 | // * 479 | // * @param startInclusive start of the key range, inclusive 480 | // * @param finish finish of the key range 481 | // * @param isFinishInclusive if true then finish is inclusive otherwise 482 | // exclusive 483 | // * @param mapper maps key value pairs to the stream result 484 | // * @param the type of streamed result that the key and 485 | // * value are mapped to 486 | // * @return values of entries in searched for key range preserving insert order 487 | // * maps the key and value to the streamed result 488 | // */ 489 | // public Iterable findEntriesOrderPreserving(K startInclusive, K finish, 490 | // boolean isFinishInclusive, BiFunction 491 | // mapper) { 492 | // return new Iterable() { 493 | // 494 | // @Override 495 | // public Iterator iterator() { 496 | // return new Iterator() { 497 | // Leaf leaf = findFirstLeaf(startInclusive); 498 | // int idx = leaf.getLocation(startInclusive); 499 | // K currentKey; 500 | // List values = new ArrayList<>(); 501 | // int valuesIdx = 0; 502 | // List nextValues = new ArrayList<>(); 503 | // 504 | // @Override 505 | // public boolean hasNext() { 506 | // load(); 507 | // return valuesIdx < values.size(); 508 | // } 509 | // 510 | // @Override 511 | // public R next() { 512 | // load(); 513 | // int size = values.size(); 514 | // if (valuesIdx >= size) { 515 | // throw new NoSuchElementException(); 516 | // } else { 517 | // // emit in reverse order 518 | // // clear the value from the list to enable early GC 519 | // R v = values.set(size - valuesIdx - 1, null); 520 | // valuesIdx++; 521 | // return v; 522 | // } 523 | // } 524 | // 525 | // private void load() { 526 | // if (valuesIdx < values.size()) { 527 | // return; 528 | // } 529 | // valuesIdx = 0; 530 | // values = clear(values, VALUES_MAX_SIZE); 531 | // // swap values and nextValues 532 | // List temp = values; 533 | // values = nextValues; 534 | // nextValues = temp; 535 | // while (true) { 536 | // if (leaf == null) { 537 | // return; 538 | // } else if (idx < leaf.numKeys()) { 539 | // K key = leaf.key(idx); 540 | // int c = options.comparator().compare(key, finish); 541 | // if (c < 0 || (c == 0 && isFinishInclusive)) { 542 | // if (currentKey == null) { 543 | // currentKey = key; 544 | // } 545 | // R r = mapper.apply(key, leaf.value(idx)); 546 | // if (options.comparator().compare(currentKey, key) == 0) { 547 | // values.add(r); 548 | // idx++; 549 | // } else { 550 | // // key has changed 551 | // currentKey = key; 552 | // nextValues.add(r); 553 | // idx++; 554 | // // key has changed so we have found the next key sequence 555 | // return; 556 | // } 557 | // } else { 558 | // // don't search further 559 | // leaf = null; 560 | // return; 561 | // } 562 | // } else { 563 | // leaf = leaf.next(); 564 | // idx = 0; 565 | // } 566 | // } 567 | // } 568 | // 569 | // }; 570 | // } 571 | // 572 | // }; 573 | // } 574 | 575 | @VisibleForTesting 576 | static List clear(List values, int maxSize) { 577 | if (values.size() > maxSize) { 578 | // if values has grown a lot in size we don't want to hang on to that much 579 | // memory permanently so we resize values 580 | return new ArrayList<>(); 581 | } else { 582 | values.clear(); 583 | return values; 584 | } 585 | } 586 | 587 | @VisibleForTesting 588 | Leaf firstLeaf(Node node) { 589 | if (node instanceof Leaf) { 590 | return (Leaf) node; 591 | } else { 592 | NonLeaf n = (NonLeaf) node; 593 | return firstLeaf(n.child(0)); 594 | } 595 | } 596 | 597 | public Iterable findAll() { 598 | return findAll((k, v) -> v); 599 | } 600 | 601 | public Iterable findAll(BiFunction mapper) { 602 | return new Iterable() { 603 | 604 | @Override 605 | public Iterator iterator() { 606 | return new Iterator() { 607 | 608 | Leaf leaf = firstLeaf(root); 609 | int index = 0; 610 | 611 | @Override 612 | public boolean hasNext() { 613 | return leaf != null && index < leaf.numKeys(); 614 | } 615 | 616 | @Override 617 | public R next() { 618 | moveBeyondLeafEnd(); 619 | if (leaf == null) { 620 | throw new NoSuchElementException(); 621 | } else { 622 | R r = mapper.apply(leaf.key(index), leaf.value(index)); 623 | index++; 624 | moveBeyondLeafEnd(); 625 | return r; 626 | } 627 | } 628 | 629 | private void moveBeyondLeafEnd() { 630 | while (leaf != null && index == leaf.numKeys()) { 631 | leaf = leaf.next(); 632 | index = 0; 633 | } 634 | } 635 | }; 636 | } 637 | }; 638 | } 639 | 640 | public void print() { 641 | print(System.out); 642 | } 643 | 644 | public void print(PrintStream out) { 645 | print(root, 0, out); 646 | } 647 | 648 | private static void print(Node node, int level, PrintStream out) { 649 | if (node instanceof Leaf) { 650 | print((Leaf) node, level, out); 651 | } else { 652 | print((NonLeaf) node, level, out); 653 | } 654 | } 655 | 656 | private static void print(Leaf node, int level, PrintStream out) { 657 | out.print(indent(level)); 658 | out.print("Leaf: "); 659 | int n = node.numKeys(); 660 | for (int i = 0; i < n; i++) { 661 | if (i > 0) { 662 | out.print(", "); 663 | } 664 | out.print(node.key(i)); 665 | out.print("->"); 666 | out.print(node.value(i)); 667 | } 668 | if (node.next() != null) { 669 | out.print("| -> " + node.next().keys()); 670 | } 671 | out.println(); 672 | } 673 | 674 | private static void print(NonLeaf node, int level, PrintStream out) { 675 | out.print(indent(level)); 676 | out.println("NonLeaf"); 677 | int n = node.numKeys(); 678 | for (int i = 0; i < n; i++) { 679 | Node nd = node.child(i); 680 | print(nd, level + 1, out); 681 | out.print(indent(level) + node.key(i)); 682 | out.println(); 683 | } 684 | if (node.child(n) != null) { 685 | print(node.child(n), level + 1, out); 686 | } 687 | out.println(); 688 | } 689 | 690 | private static String indent(int level) { 691 | StringBuilder b = new StringBuilder(); 692 | for (int i = 0; i < level; i++) { 693 | b.append(" "); 694 | } 695 | return b.toString(); 696 | } 697 | 698 | Node root() { 699 | return root; 700 | } 701 | 702 | Factory factory() { 703 | return factory; 704 | } 705 | 706 | @Override 707 | public void close() throws Exception { 708 | factory.close(); 709 | } 710 | 711 | public void commit() { 712 | factory.commit(); 713 | } 714 | 715 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/Entry.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | public final class Entry { 4 | 5 | private final K key; 6 | private final V value; 7 | 8 | private Entry(K key, V value) { 9 | this.key = key; 10 | this.value = value; 11 | } 12 | 13 | public static Entry create(K key, V value) { 14 | return new Entry(key, value); 15 | } 16 | 17 | public K key() { 18 | return key; 19 | } 20 | 21 | public V value() { 22 | return value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/LargeByteBuffer.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | /** 7 | * Similar to {@link ByteBuffer} but supports {@code long} positions instead of 8 | * {@code int} positions. Does not include those methods in ByteBuffer that 9 | * include the position as a parameter (for simplicity). 10 | * 11 | *

12 | * Also includes the notion of commit which forces flushing of memory buffers to 13 | * disk. 14 | * 15 | */ 16 | public interface LargeByteBuffer { 17 | 18 | long position(); 19 | 20 | void position(long newPosition); 21 | 22 | byte get(); 23 | 24 | void put(byte b); 25 | 26 | void get(byte[] dst); 27 | 28 | void put(byte[] src); 29 | 30 | int getInt(); 31 | 32 | void putInt(int value); 33 | 34 | short getShort(); 35 | 36 | void putShort(short value); 37 | 38 | long getLong(); 39 | 40 | void putLong(long value); 41 | 42 | double getDouble(); 43 | 44 | void putDouble(double value); 45 | 46 | double getFloat(); 47 | 48 | void putFloat(float value); 49 | 50 | void commit(); 51 | 52 | default String getString() { 53 | int length = getVarint(); 54 | byte[] bytes = new byte[length]; 55 | get(bytes); 56 | return new String(bytes, StandardCharsets.UTF_8); 57 | } 58 | 59 | default void putString(String value) { 60 | byte[] bytes = value.getBytes(StandardCharsets.UTF_8); 61 | putVarint(bytes.length); 62 | put(bytes); 63 | } 64 | 65 | /** 66 | * Stores an integer in a variable number of bytes (up to 5). A varint is an 67 | * alternative storage method for an integer that may take up as little as one 68 | * byte for small values. 69 | * 70 | *

71 | * Algorithm used is from ProtocolBuffers. 72 | * 73 | * @param value integer value to store 74 | */ 75 | default void putVarint(int value) { 76 | while (true) { 77 | if ((value & ~0x7F) == 0) { 78 | put((byte) value); 79 | break; 80 | } else { 81 | put((byte) ((value & 0x7F) | 0x80)); 82 | value >>>= 7; 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Returns an integer that was stored in a variable number of bytes (up to 5). A 89 | * varint is an alternative storage method for an integer that may take up as 90 | * little as one byte for small values. 91 | * 92 | *

93 | * Algorithm used is from ProtocolBuffers. 94 | * @return the decoded integer 95 | */ 96 | default int getVarint() { 97 | // Adapated from ProtocolBuffers CodedInputStream 98 | int x; 99 | long pos = position(); 100 | if ((x = get()) >= 0) { 101 | return x; 102 | } else if ((x ^= (get() << 7)) < 0) { 103 | x ^= (~0 << 7); 104 | } else if ((x ^= (get() << 14)) >= 0) { 105 | x ^= (~0 << 7) ^ (~0 << 14); 106 | } else if ((x ^= (get() << 21)) < 0) { 107 | x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21); 108 | } else { 109 | position(pos); 110 | // get the value the slow way but can handle when integer needed more than 4 111 | // bytes to represent 112 | return (int) getVarlong(); 113 | } 114 | return x; 115 | } 116 | 117 | /** 118 | * Stores a long in a variable number of bytes (up to 9). A varlong is an 119 | * alternative storage method for a long that may take up as little as one byte 120 | * for small values. 121 | * 122 | *

123 | * Algorithm used is from ProtocolBuffers. 124 | * 125 | * @param value long value to store 126 | */ 127 | default void putVarlong(long value) { 128 | while (true) { 129 | if ((value & ~0x7FL) == 0) { 130 | put((byte) value); 131 | return; 132 | } else { 133 | put((byte) (((int) value & 0x7F) | 0x80)); 134 | value >>>= 7; 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Returns a long that was stored in a variable number of bytes (up to 9). A 141 | * varlong is an alternative storage method for a long that may take up as 142 | * little as one byte for small values. 143 | * 144 | *

145 | * Algorithm used is from ProtocolBuffers. 146 | * @return the decoded long 147 | */ 148 | default long getVarlong() { 149 | long result = 0; 150 | for (int shift = 0; shift < 64; shift += 7) { 151 | final byte b = get(); 152 | result |= (long) (b & 0x7F) << shift; 153 | if ((b & 0x80) == 0) { 154 | return result; 155 | } 156 | } 157 | throw new IllegalStateException("malformed varlong"); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.nio.charset.Charset; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | public interface Serializer { 7 | 8 | T read(LargeByteBuffer bb); 9 | 10 | void write(LargeByteBuffer bb, T t); 11 | 12 | /** 13 | * Returns the maximum size in bytes of a serialized item. Returns 0 when there 14 | * is no maximum. 15 | * 16 | * @return the maximum size in bytes of a serialized item. Returns 0 when there 17 | * is no maximum. 18 | */ 19 | int maxSize(); 20 | 21 | public static Serializer SHORT = new Serializer() { 22 | 23 | @Override 24 | public Short read(LargeByteBuffer bb) { 25 | return bb.getShort(); 26 | } 27 | 28 | @Override 29 | public void write(LargeByteBuffer bb, Short t) { 30 | bb.putShort(t); 31 | } 32 | 33 | @Override 34 | public int maxSize() { 35 | return Short.BYTES; 36 | } 37 | }; 38 | 39 | public static Serializer INTEGER = new Serializer() { 40 | 41 | @Override 42 | public Integer read(LargeByteBuffer bb) { 43 | return bb.getInt(); 44 | } 45 | 46 | @Override 47 | public void write(LargeByteBuffer bb, Integer t) { 48 | bb.putInt(t); 49 | } 50 | 51 | @Override 52 | public int maxSize() { 53 | return Integer.BYTES; 54 | } 55 | }; 56 | 57 | public static Serializer LONG = new Serializer() { 58 | 59 | @Override 60 | public Long read(LargeByteBuffer bb) { 61 | return bb.getLong(); 62 | } 63 | 64 | @Override 65 | public void write(LargeByteBuffer bb, Long t) { 66 | bb.putLong(t); 67 | } 68 | 69 | @Override 70 | public int maxSize() { 71 | return Long.BYTES; 72 | } 73 | }; 74 | 75 | public static Serializer FLOAT = new Serializer() { 76 | 77 | @Override 78 | public Float read(LargeByteBuffer bb) { 79 | return ((Double) bb.getFloat()).floatValue(); 80 | } 81 | 82 | @Override 83 | public void write(LargeByteBuffer bb, Float t) { 84 | bb.putFloat(t); 85 | } 86 | 87 | @Override 88 | public int maxSize() { 89 | return Float.BYTES; 90 | } 91 | }; 92 | 93 | public static Serializer DOUBLE = new Serializer() { 94 | 95 | @Override 96 | public Double read(LargeByteBuffer bb) { 97 | return bb.getDouble(); 98 | } 99 | 100 | @Override 101 | public void write(LargeByteBuffer bb, Double t) { 102 | bb.putDouble(t); 103 | } 104 | 105 | @Override 106 | public int maxSize() { 107 | return Double.BYTES; 108 | } 109 | }; 110 | 111 | public static Serializer utf8() { 112 | return utf8(0); 113 | } 114 | 115 | public static Serializer utf8(int maxSize) { 116 | return string(StandardCharsets.UTF_8, maxSize); 117 | } 118 | 119 | public static Serializer string(Charset charset, int maxSize) { 120 | return new Serializer() { 121 | 122 | @Override 123 | public String read(LargeByteBuffer bb) { 124 | return bb.getString(); 125 | } 126 | 127 | @Override 128 | public void write(LargeByteBuffer bb, String s) { 129 | bb.putString(s); 130 | } 131 | 132 | @Override 133 | public int maxSize() { 134 | return maxSize; 135 | } 136 | }; 137 | } 138 | 139 | public static Serializer bytes(int maxSize) { 140 | return new Serializer() { 141 | 142 | @Override 143 | public byte[] read(LargeByteBuffer bb) { 144 | int size = bb.getInt(); 145 | byte[] bytes = new byte[size]; 146 | bb.get(bytes); 147 | return bytes; 148 | } 149 | 150 | @Override 151 | public void write(LargeByteBuffer bb, byte[] bytes) { 152 | bb.putInt(bytes.length); 153 | bb.put(bytes); 154 | } 155 | 156 | @Override 157 | public int maxSize() { 158 | return maxSize; 159 | } 160 | 161 | }; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Factory.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | public interface Factory extends AutoCloseable { 4 | 5 | Leaf createLeaf(); 6 | 7 | NonLeaf createNonLeaf(); 8 | 9 | void commit(); 10 | 11 | /** 12 | * Called when the root node of the BPlusTree is initialized or changes. 13 | * 14 | * @param node new root node 15 | */ 16 | void root(Node node); 17 | 18 | Node loadOrCreateRoot(); 19 | 20 | Options options(); 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/FactoryProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | public interface FactoryProvider { 4 | 5 | Factory createFactory(Options options); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/LargeMappedByteBuffer.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.io.UncheckedIOException; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | import java.nio.ByteBuffer; 11 | import java.nio.MappedByteBuffer; 12 | import java.nio.channels.FileChannel; 13 | import java.nio.channels.FileChannel.MapMode; 14 | import java.nio.file.Files; 15 | import java.nio.file.StandardOpenOption; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.TreeMap; 19 | 20 | import com.github.davidmoten.bplustree.Entry; 21 | import com.github.davidmoten.bplustree.LargeByteBuffer; 22 | import com.github.davidmoten.guavamini.annotations.VisibleForTesting; 23 | 24 | public final class LargeMappedByteBuffer implements AutoCloseable, LargeByteBuffer { 25 | 26 | private final int segmentSizeBytes; 27 | 28 | private final TreeMap map = new TreeMap<>(); 29 | private final List> list = new ArrayList<>(); 30 | private final File directory; 31 | private final String segmentNamePrefix; 32 | 33 | private byte[] temp2Bytes = new byte[2]; 34 | private byte[] temp4Bytes = new byte[4]; 35 | private byte[] temp8Bytes = new byte[8]; 36 | 37 | public LargeMappedByteBuffer(File directory, int segmentSizeBytes, String segmentNamePrefix) { 38 | this.directory = directory; 39 | this.segmentSizeBytes = segmentSizeBytes; 40 | this.segmentNamePrefix = segmentNamePrefix; 41 | } 42 | 43 | private long position; 44 | 45 | private MappedByteBuffer bb(long position) { 46 | // TODO close segments when map gets too many entries 47 | 48 | long num = segmentNumber(position); 49 | Segment segment = getSegment(num); 50 | if (segment == null) { 51 | segment = createSegment(num); 52 | } 53 | segment.bb.position((int) (position % segmentSizeBytes)); 54 | return segment.bb; 55 | } 56 | 57 | private Segment getSegment(long num) { 58 | for (int i = 0; i < list.size(); i++) { 59 | Entry entry = list.get(i); 60 | if (entry.key() == num) { 61 | return entry.value(); 62 | } 63 | } 64 | return null; 65 | } 66 | 67 | private void putSegment(long num, Segment segment) { 68 | // map.put(num, segment); 69 | list.add(Entry.create(num, segment)); 70 | } 71 | 72 | private Segment createSegment(long num) { 73 | File file = new File(directory, segmentNamePrefix + num); 74 | Segment segment = map(file, segmentSizeBytes); 75 | putSegment(num, segment); 76 | return segment; 77 | } 78 | 79 | private static Segment map(File file, int segmentSizeBytes) { 80 | try { 81 | checkFile(file, segmentSizeBytes); 82 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 83 | raf.setLength(segmentSizeBytes); 84 | } 85 | FileChannel channel = (FileChannel) Files // 86 | .newByteChannel(// 87 | file.toPath(), // 88 | StandardOpenOption.CREATE, // 89 | StandardOpenOption.READ, // 90 | StandardOpenOption.WRITE); 91 | 92 | // map the whole file 93 | MappedByteBuffer bb = channel.map(MapMode.READ_WRITE, 0, segmentSizeBytes); 94 | return new Segment(channel, bb); 95 | } catch (IOException e) { 96 | throw new UncheckedIOException(e); 97 | } 98 | } 99 | 100 | @VisibleForTesting 101 | static void checkFile(File file, int segmentSizeBytes) { 102 | if (file.exists() && file.length() != segmentSizeBytes) { 103 | throw new IllegalStateException("segment file " + file + " should be of size " + segmentSizeBytes 104 | + " but was of size " + file.length()); 105 | } 106 | } 107 | 108 | @Override 109 | public void position(long newPosition) { 110 | this.position = newPosition; 111 | } 112 | 113 | @Override 114 | public byte get() { 115 | return bb(position++).get(); 116 | } 117 | 118 | @Override 119 | public void put(byte b) { 120 | bb(position++).put(b); 121 | } 122 | 123 | @Override 124 | public void get(byte[] dst) { 125 | long p = position; 126 | if (segmentNumber(p) == segmentNumber(p + dst.length)) { 127 | bb(p).get(dst); 128 | } else { 129 | int i = 0; 130 | while (true) { 131 | long p2 = Math.min(segmentPosition(segmentNumber(p) + 1), position + dst.length); 132 | int length = (int) (p2 - p); 133 | if (length == 0) { 134 | break; 135 | } 136 | bb(p).get(dst, i, length); 137 | i += length; 138 | p = p2; 139 | } 140 | } 141 | position += dst.length; 142 | } 143 | 144 | @Override 145 | public void put(byte[] src) { 146 | long p = position; 147 | if (segmentNumber(p) == segmentNumber(p + src.length)) { 148 | bb(p).put(src); 149 | } else { 150 | int i = 0; 151 | while (true) { 152 | long p2 = Math.min(segmentPosition(segmentNumber(p) + 1), position + src.length); 153 | int length = (int) (p2 - p); 154 | if (length == 0) { 155 | break; 156 | } 157 | bb(p).put(src, i, length); 158 | i += length; 159 | p = p2; 160 | } 161 | } 162 | position += src.length; 163 | } 164 | 165 | @Override 166 | public int getInt() { 167 | long p = position; 168 | if (segmentNumber(p) == segmentNumber(p + Integer.BYTES)) { 169 | position += Integer.BYTES; 170 | return bb(p).getInt(); 171 | } else { 172 | get(temp4Bytes); 173 | return toInt(temp4Bytes); 174 | } 175 | } 176 | 177 | @Override 178 | public void putInt(int value) { 179 | long p = position; 180 | if (segmentNumber(p) == segmentNumber(p + Integer.BYTES)) { 181 | bb(p).putInt(value); 182 | position += Integer.BYTES; 183 | } else { 184 | put(toBytes(value)); 185 | } 186 | } 187 | 188 | @Override 189 | public long getLong() { 190 | long p = position; 191 | if (segmentNumber(p) == segmentNumber(p + Long.BYTES)) { 192 | position += Long.BYTES; 193 | return bb(p).getLong(); 194 | } else { 195 | get(temp8Bytes); 196 | return toLong(temp8Bytes); 197 | } 198 | } 199 | 200 | @Override 201 | public void putLong(long value) { 202 | long p = position; 203 | if (segmentNumber(p) == segmentNumber(p + Long.BYTES)) { 204 | position += Long.BYTES; 205 | bb(p).putLong(value); 206 | } else { 207 | put(toBytes(value)); 208 | } 209 | } 210 | 211 | @Override 212 | public long position() { 213 | return position; 214 | } 215 | 216 | @Override 217 | public short getShort() { 218 | long p = position; 219 | if (segmentNumber(p) == segmentNumber(p + Short.BYTES)) { 220 | position += Short.BYTES; 221 | return bb(p).getShort(); 222 | } else { 223 | get(temp2Bytes); 224 | return toShort(temp2Bytes); 225 | } 226 | } 227 | 228 | @Override 229 | public void putShort(short value) { 230 | long p = position; 231 | if (segmentNumber(p) == segmentNumber(p + Short.BYTES)) { 232 | bb(p).putShort(value); 233 | position += Short.BYTES; 234 | } else { 235 | put(toBytes(value)); 236 | } 237 | } 238 | 239 | private long segmentNumber(long position) { 240 | return position / segmentSizeBytes; 241 | } 242 | 243 | private long segmentPosition(long segmentNumber) { 244 | return segmentSizeBytes * segmentNumber; 245 | } 246 | 247 | private static byte[] toBytes(short n) { 248 | return ByteBuffer.allocate(Short.BYTES).putShort(n).array(); 249 | } 250 | 251 | private static byte[] toBytes(int n) { 252 | return ByteBuffer.allocate(Integer.BYTES).putInt(n).array(); 253 | } 254 | 255 | private static byte[] toBytes(long n) { 256 | return ByteBuffer.allocate(Long.BYTES).putLong(n).array(); 257 | } 258 | 259 | private static byte[] toBytes(double n) { 260 | return ByteBuffer.allocate(Double.BYTES).putDouble(n).array(); 261 | } 262 | 263 | private static byte[] toBytes(float n) { 264 | return ByteBuffer.allocate(Float.BYTES).putFloat(n).array(); 265 | } 266 | 267 | private short toShort(byte[] bytes) { 268 | short ret = 0; 269 | for (int i = 0; i < 2; i++) { 270 | ret <<= 8; 271 | ret |= bytes[i] & 0xFF; 272 | } 273 | return ret; 274 | } 275 | 276 | private static int toInt(byte[] bytes) { 277 | int ret = 0; 278 | for (int i = 0; i < 4; i++) { 279 | ret <<= 8; 280 | ret |= bytes[i] & 0xFF; 281 | } 282 | return ret; 283 | } 284 | 285 | private static long toLong(byte[] b) { 286 | long result = 0; 287 | for (int i = 0; i < 8; i++) { 288 | result <<= 8; 289 | result |= (b[i] & 0xFF); 290 | } 291 | return result; 292 | } 293 | 294 | private static double toDouble(byte[] b) { 295 | return ByteBuffer.wrap(b).getDouble(); 296 | } 297 | 298 | private static double toFloat(byte[] b) { 299 | return ByteBuffer.wrap(b).getFloat(); 300 | } 301 | 302 | @Override 303 | public void commit() { 304 | for (Segment segment : map.values()) { 305 | segment.bb.force(); 306 | } 307 | } 308 | 309 | @Override 310 | public void close() throws IOException { 311 | for (Entry entry : list) { 312 | entry.value().close(); 313 | } 314 | list.clear(); 315 | } 316 | 317 | private static final class Segment { 318 | private final FileChannel channel; 319 | final MappedByteBuffer bb; 320 | 321 | Segment(FileChannel channel, MappedByteBuffer bb) { 322 | this.channel = channel; 323 | this.bb = bb; 324 | } 325 | 326 | public void close() throws IOException { 327 | // Note that System.gc() seems to do the job as well 328 | // as closeDirectBuffer but of course may cause overall 329 | // system pauses which may not be desirable for everyone 330 | closeDirectBuffer(bb); 331 | channel.close(); 332 | } 333 | 334 | } 335 | 336 | private static void closeDirectBuffer(ByteBuffer cb) { 337 | if (cb == null || !cb.isDirect()) 338 | return; 339 | // we could use this type cast and call functions without reflection code, 340 | // but static import from sun.* package is risky for non-SUN virtual machine. 341 | // try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) 342 | // { } 343 | 344 | // JavaSpecVer: 1.6, 1.7, 1.8, 9, 10 345 | boolean isOldJDK = System.getProperty("java.specification.version", "99").startsWith("1."); 346 | try { 347 | if (isOldJDK) { 348 | Method cleaner = cb.getClass().getMethod("cleaner"); 349 | cleaner.setAccessible(true); 350 | Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean"); 351 | clean.setAccessible(true); 352 | clean.invoke(cleaner.invoke(cb)); 353 | } else { 354 | Class unsafeClass; 355 | try { 356 | unsafeClass = Class.forName("sun.misc.Unsafe"); 357 | } catch (Exception ex) { 358 | // jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method, 359 | // but that method should be added if sun.misc.Unsafe is removed. 360 | unsafeClass = Class.forName("jdk.internal.misc.Unsafe"); 361 | } 362 | Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class); 363 | clean.setAccessible(true); 364 | Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe"); 365 | theUnsafeField.setAccessible(true); 366 | Object theUnsafe = theUnsafeField.get(null); 367 | clean.invoke(theUnsafe, cb); 368 | } 369 | } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchFieldException 370 | | SecurityException | NoSuchMethodException | ClassNotFoundException e) { 371 | e.printStackTrace(); 372 | } 373 | cb = null; 374 | } 375 | 376 | @Override 377 | public double getDouble() { 378 | get(temp8Bytes); 379 | return toDouble(temp8Bytes); 380 | } 381 | 382 | @Override 383 | public void putDouble(double value) { 384 | long p = position; 385 | if (segmentNumber(p) == segmentNumber(p + Double.BYTES)) { 386 | bb(p).putDouble(value); 387 | position += Double.BYTES; 388 | } else { 389 | put(toBytes(value)); 390 | } 391 | } 392 | 393 | @Override 394 | public double getFloat() { 395 | get(temp4Bytes); 396 | return toFloat(temp4Bytes); 397 | } 398 | 399 | @Override 400 | public void putFloat(float value) { 401 | long p = position; 402 | if (segmentNumber(p) == segmentNumber(p + Float.BYTES)) { 403 | bb(p).putFloat(value); 404 | position += Float.BYTES; 405 | } else { 406 | put(toBytes(value)); 407 | } 408 | } 409 | 410 | } 411 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Leaf.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | public interface Leaf extends Node { 4 | 5 | public static final int TYPE = 0; 6 | 7 | @Override 8 | Options options(); 9 | 10 | V value(int i); 11 | 12 | void setNumKeys(int numKeys); 13 | 14 | void setValue(int i, V value); 15 | 16 | /** 17 | * Inserts a key and value at the given index in the node and increments the 18 | * number of keys in the node. 19 | * 20 | * @param i which position to make the insertino at in the Leaf keys 21 | * @param key key to insert 22 | * @param value value to insert 23 | */ 24 | void insert(int i, K key, V value); 25 | 26 | /** 27 | * Copies length KeyValues from index start to the start of {@code newLeaf}, 28 | * sets the number of keys in the new Leaf to be {@code length}, sets the number 29 | * of keys in source Leaf to be {@code start}. 30 | * 31 | * @param start start index of Key Value pairs to copy in current Leaf 32 | * @param length number of Key Value pairs to copy 33 | * @param newLeaf a new empty Leaf 34 | */ 35 | void move(int start, int length, Leaf newLeaf); 36 | 37 | void setNext(Leaf sibling); 38 | 39 | Leaf next(); 40 | 41 | @Override 42 | default Split insert(K key, V value) { 43 | // Simple linear search 44 | int i = getLocation(key); 45 | int numKeys = numKeys(); 46 | if (numKeys == options().maxLeafKeys()) { 47 | // The node is full. We must split it 48 | // the first mid entries will be retained 49 | // and the rest moved to a new right sibling 50 | int mid = (options().maxLeafKeys() + 1) / 2; 51 | int len = numKeys - mid; 52 | Leaf sibling = factory().createLeaf(); 53 | move(mid, len, sibling); 54 | if (i < mid) { 55 | // Inserted element goes to left sibling 56 | Util.insertNonfull(this, key, value, i, mid); 57 | } else { 58 | // Inserted element goes to right sibling 59 | // TODO this probably brings about another array copy 60 | // (shift to the right) in sibling so perhaps should be combined with the 61 | // original move 62 | Util.insertNonfull(sibling, key, value, i - mid, len); 63 | } 64 | sibling.setNext(next()); 65 | setNext(sibling); 66 | // Notify the parent about the split 67 | return new Split<>(sibling.key(0), // make the right's key >= 68 | // result.key 69 | this, sibling); 70 | } else { 71 | // The node was not full 72 | Util.insertNonfull(this, key, value, i, numKeys); 73 | return null; 74 | } 75 | } 76 | 77 | /** 78 | * Returns the position where 'key' should be inserted in a leaf node that has 79 | * the given keys. The position returned will be the first key K for which 80 | * {@code key} <= K.. If no key satisfies that criterion then returns the 81 | * current number of keys. 82 | * 83 | * @param key key to insert 84 | * @return the position where key should be inserted 85 | */ 86 | @Override 87 | default int getLocation(K key) { 88 | return Util.getLocation(this, key, options().comparator(), true); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Node.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public interface Node { 7 | 8 | // returns null if no split, otherwise returns split info 9 | Split insert(K key, V value); 10 | 11 | K key(int i); 12 | 13 | int numKeys(); 14 | 15 | Options options(); 16 | 17 | Factory factory(); 18 | 19 | default List keys() { 20 | List list = new ArrayList(); 21 | for (int i = 0; i < numKeys(); i++) { 22 | list.add(key(i)); 23 | } 24 | return list; 25 | } 26 | 27 | /** 28 | * Returns the position where 'key' should be inserted in a leaf node that has 29 | * the given keys. 30 | * 31 | * @param key key to insert 32 | * @return the position where key should be inserted 33 | */ 34 | int getLocation(K key); 35 | 36 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/NonLeaf.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | public interface NonLeaf extends Node { 4 | 5 | public static final int TYPE = 1; 6 | 7 | void setNumKeys(int numKeys); 8 | 9 | @Override 10 | int numKeys(); 11 | 12 | void setChild(int i, Node node); 13 | 14 | Node child(int i); 15 | 16 | @Override 17 | K key(int i); 18 | 19 | void setKey(int i, K key); 20 | 21 | void move(int mid, NonLeaf other, int length); 22 | 23 | /** 24 | * Inserts the key at the given index and sets the left child of that key to be 25 | * {@code left}. Also increments the number of keys in the node. 26 | * 27 | * @param i index to insert at 28 | * @param key key to insert 29 | * @param left child to set of the new key 30 | */ 31 | void insert(int i, K key, Node left); 32 | 33 | @Override 34 | default Split insert(K key, V value) { 35 | if (numKeys() == options().maxNonLeafKeys()) { // Split 36 | int mid = options().maxNonLeafKeys() / 2 + 1; 37 | int len = options().maxNonLeafKeys() - mid; 38 | NonLeaf sibling = factory().createNonLeaf(); 39 | move(mid, sibling, len); 40 | 41 | // Set up the return variable 42 | Split result = new Split<>(key(mid - 1), this, sibling); 43 | 44 | // Now insert in the appropriate sibling 45 | if (options().comparator().compare(key, result.key) < 0) { 46 | Util.insertNonfull(this, key, value); 47 | } else { 48 | Util.insertNonfull(sibling, key, value); 49 | } 50 | return result; 51 | } else {// No split 52 | Util.insertNonfull(this, key, value); 53 | return null; 54 | } 55 | } 56 | 57 | /** 58 | * Returns the position where 'key' should be inserted in a non-leaf node that 59 | * has the given keys. The position returned will be the first key K for which 60 | * {@code key} < K.. If no key satisfies that criterion then returns the 61 | * current number of keys. 62 | * 63 | * @param key key to insert 64 | * @return the position where key should be inserted 65 | */ 66 | @Override 67 | default int getLocation(K key) { 68 | return Util.getLocation(this, key, options().comparator(), false); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Options.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import java.util.Comparator; 4 | 5 | import com.github.davidmoten.guavamini.Preconditions; 6 | 7 | public final class Options { 8 | 9 | /** the maximum number of keys in the leaf node, M must be > 0 */ 10 | private final int maxLeafKeys; 11 | 12 | /** 13 | * the maximum number of keys in inner node, the number of pointer is N+1, N 14 | * must be > 2 15 | */ 16 | private final int maxNonLeafKeys; 17 | private final Comparator comparator; 18 | private final boolean uniqueKeys; 19 | private final FactoryProvider factoryProvider; 20 | 21 | public Options(int maxLeafKeys, int maxNonLeafKeys, boolean uniqueKeys, 22 | Comparator comparator, FactoryProvider factoryProvider) { 23 | // only one byte used to store num keys so check values 24 | Preconditions.checkArgument(0 < maxLeafKeys && maxLeafKeys <= 255); 25 | Preconditions.checkArgument(0 < maxNonLeafKeys && maxNonLeafKeys <= 255); 26 | this.maxLeafKeys = maxLeafKeys; 27 | this.maxNonLeafKeys = maxNonLeafKeys; 28 | this.comparator = comparator; 29 | this.uniqueKeys = uniqueKeys; 30 | this.factoryProvider = factoryProvider; 31 | } 32 | 33 | public int maxLeafKeys() { 34 | return maxLeafKeys; 35 | } 36 | 37 | public int maxNonLeafKeys() { 38 | return maxNonLeafKeys; 39 | } 40 | 41 | public Comparator comparator() { 42 | return comparator; 43 | } 44 | 45 | public boolean uniqueKeys() { 46 | return uniqueKeys; 47 | } 48 | 49 | public FactoryProvider factoryProvider() { 50 | return factoryProvider; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Split.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | public final class Split { 4 | public final K key; 5 | public final Node left; 6 | public final Node right; 7 | 8 | public Split(K key, Node left, Node right) { 9 | this.key = key; 10 | this.left = left; 11 | this.right = right; 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import java.util.Comparator; 4 | 5 | final class Util { 6 | 7 | private Util() { 8 | // prevent instantiation 9 | } 10 | 11 | static void insertNonfull(Leaf leaf, K key, V value, int idx, int numKeys) { 12 | // numKeys == leaf.numKeys() but might be costly to call and may have already 13 | // been calculated by the calling methods so we use a pased value 14 | if (idx < numKeys && leaf.options().uniqueKeys() && leaf.key(idx).equals(key)) { 15 | // We are inserting a duplicate value, simply overwrite the old one 16 | leaf.setValue(idx, value); 17 | } else { 18 | // The key we are inserting is unique 19 | leaf.insert(idx, key, value); 20 | } 21 | } 22 | 23 | static void insertNonfull(NonLeaf node, K key, V value) { 24 | // Simple linear search 25 | int index = node.getLocation(key); 26 | Node child = node.child(index); 27 | Split result = child.insert(key, value); 28 | 29 | if (result != null) { 30 | if (index == node.numKeys()) { 31 | // Insertion at the rightmost key 32 | node.setKey(index, result.key); 33 | node.setChild(index, result.left); 34 | node.setChild(index + 1, result.right); 35 | node.setNumKeys(node.numKeys() + 1); 36 | } else { 37 | // Insertion not at the rightmost key 38 | // shift i>idx to the right 39 | node.insert(index, result.key, result.left); 40 | node.setChild(index + 1, result.right); 41 | } 42 | } // else the current node is not affected 43 | } 44 | 45 | static int getLocation(Node node, K key, Comparator comparator, boolean acceptEquals) { 46 | int numKeys = node.numKeys(); 47 | if (numKeys == 0) { 48 | return 0; 49 | } 50 | int start = 0; 51 | int finish = numKeys - 1; 52 | while (true) { 53 | int mid = (start + finish) / 2; 54 | int c = comparator.compare(key, node.key(mid)); 55 | if (c < 0 || (acceptEquals && c == 0)) { 56 | finish = mid; 57 | if (start == finish) { 58 | return mid; 59 | } 60 | } else { 61 | if (start == finish) { 62 | return mid + 1; 63 | } 64 | start = mid + 1; 65 | } 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/file/FactoryFile.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.file; 2 | 3 | import java.io.File; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.IntStream; 7 | 8 | import com.github.davidmoten.bplustree.Serializer; 9 | import com.github.davidmoten.bplustree.internal.Factory; 10 | import com.github.davidmoten.bplustree.internal.LargeMappedByteBuffer; 11 | import com.github.davidmoten.bplustree.internal.Leaf; 12 | import com.github.davidmoten.bplustree.internal.Node; 13 | import com.github.davidmoten.bplustree.internal.NonLeaf; 14 | import com.github.davidmoten.bplustree.internal.Options; 15 | 16 | public final class FactoryFile implements Factory { 17 | 18 | private static final int NODE_TYPE_BYTES = 1; 19 | private static final int NUM_KEYS_BYTES = 1; 20 | private static final int NUM_NODES_BYTES = 4; 21 | private static final int POSITION_BYTES = 8; 22 | private static final long POSITION_NOT_PRESENT = -1; 23 | private final Options options; 24 | 25 | // a pool of LeafFile objects to use 26 | private final List> leavesPool; 27 | private int leavesIndex = 0; 28 | 29 | // position where next node will be written, first 8 bytes are for the position 30 | // of the root node 31 | private long index = POSITION_BYTES; 32 | 33 | private long valuesIndex = 0; // position where next value will be written 34 | private final Serializer keySerializer; 35 | private final Serializer valueSerializer; 36 | private final LargeMappedByteBuffer metadata; 37 | private final LargeMappedByteBuffer bb; 38 | private final LargeMappedByteBuffer values; 39 | private final Runnable onClose; 40 | 41 | public FactoryFile(Options options, File directory, Serializer keySerializer, 42 | Serializer valueSerializer, int segmentSizeBytes, Runnable onClose) { 43 | this.options = options; 44 | this.keySerializer = keySerializer; 45 | this.valueSerializer = valueSerializer; 46 | this.onClose = onClose; 47 | this.bb = new LargeMappedByteBuffer(directory, segmentSizeBytes, "index-"); 48 | this.values = new LargeMappedByteBuffer(directory, segmentSizeBytes, "value-"); 49 | this.metadata = new LargeMappedByteBuffer(directory, 8192, "metadata-"); // only needs 8 bytes right now 50 | this.leavesPool = createLeafPool(this, 10); 51 | } 52 | 53 | ////////////////////////////////////////////////// 54 | // Format of a Leaf 55 | // NODE_TYPE NUM_KEYS (KEY VALUE)* NEXT_LEAF_POSITION 56 | // where 57 | // NODE_TYPE is one byte (0 = Leaf, 1 = NonLeaf) 58 | // NUM_KEYS is one byte unsigned 59 | // KEY is a byte array of fixed size 60 | // VALUE is a byte array of fixed size 61 | // NEXT_LEAF_POSITION is 8 bytes signed long 62 | // Every Leaf has space allocated for maxLeafKeys key value pairs 63 | ////////////////////////////////////////////////// 64 | 65 | private static List> createLeafPool(FactoryFile factory, int size) { 66 | return IntStream // 67 | .rangeClosed(1, size) // 68 | .mapToObj(x -> new LeafFile(factory, POSITION_NOT_PRESENT)).collect(Collectors.toList()); 69 | } 70 | 71 | @Override 72 | public Leaf createLeaf() { 73 | return getLeaf(leafNextPosition()); 74 | } 75 | 76 | private Leaf getLeaf(long position) { 77 | // return new LeafFile(this, position); 78 | LeafFile leaf = leavesPool.get(leavesIndex); 79 | leavesIndex = (leavesIndex + 1) % leavesPool.size(); 80 | leaf.position(position); 81 | return leaf; 82 | } 83 | 84 | private int leafBytes() { 85 | return relativeLeafKeyPosition(options.maxLeafKeys()) // 86 | + POSITION_BYTES; // next leaf position 87 | } 88 | 89 | private long leafNextPosition() { 90 | long i = index; 91 | bb.position(index); 92 | bb.put((byte) Leaf.TYPE); 93 | bb.position(index + leafBytes() - POSITION_BYTES); 94 | bb.putLong(POSITION_NOT_PRESENT); 95 | // shift by max size of a leaf node: numKeys, keys, values, next leaf position 96 | // (b+tree pointer to next leaf node) 97 | index += leafBytes(); 98 | return i; 99 | } 100 | 101 | private int relativeLeafKeyPosition(int i) { 102 | return NODE_TYPE_BYTES + NUM_KEYS_BYTES + i * (keySerializer.maxSize() + POSITION_BYTES); 103 | } 104 | 105 | public K leafKey(long position, int i) { 106 | long p = position + relativeLeafKeyPosition(i); 107 | bb.position(p); 108 | return keySerializer.read(bb); 109 | } 110 | 111 | public int leafNumKeys(long position) { 112 | bb.position(position + NODE_TYPE_BYTES); 113 | return bb.get() & 0xFF; 114 | } 115 | 116 | public void leafSetNumKeys(long position, int numKeys) { 117 | bb.position(position + NODE_TYPE_BYTES); 118 | bb.put((byte) numKeys); 119 | } 120 | 121 | public V leafValue(long position, int i) { 122 | long p = position + relativeLeafKeyPosition(i) + keySerializer.maxSize(); 123 | bb.position(p); 124 | long valuePos = bb.getLong(); 125 | values.position(valuePos); 126 | return valueSerializer.read(values); 127 | } 128 | 129 | public void leafSetValue(long position, int i, V value) { 130 | long p = position + relativeLeafKeyPosition(i) + keySerializer.maxSize(); 131 | bb.position(p); 132 | bb.putLong(valuesIndex); 133 | values.position(valuesIndex); 134 | valueSerializer.write(values, value); 135 | valuesIndex = values.position(); 136 | } 137 | 138 | public void leafInsert(long position, int i, K key, V value) { 139 | int relativeStart = relativeLeafKeyPosition(i); 140 | int relativeFinish = relativeLeafKeyPosition(leafNumKeys(position)); 141 | 142 | bb.position(position + relativeStart); 143 | byte[] bytes = new byte[relativeFinish - relativeStart]; 144 | bb.get(bytes); 145 | 146 | // copy bytes across one key 147 | bb.position(position + relativeLeafKeyPosition(i + 1)); 148 | bb.put(bytes); 149 | 150 | // write inserted key and value 151 | long p = position + relativeStart; 152 | bb.position(p); 153 | keySerializer.write(bb, key); 154 | bb.position(p + keySerializer.maxSize()); 155 | bb.putLong(valuesIndex); 156 | values.position(valuesIndex); 157 | valueSerializer.write(values, value); 158 | valuesIndex = values.position(); 159 | // increment number of keys in leaf node 160 | leafSetNumKeys(position, leafNumKeys(position) + 1); 161 | } 162 | 163 | public void leafMove(long position, int start, int length, LeafFile other) { 164 | int relativeStart = relativeLeafKeyPosition(start); 165 | int relativeEnd = relativeLeafKeyPosition(start + length); 166 | byte[] bytes = new byte[relativeEnd - relativeStart]; 167 | bb.position(position + relativeStart); 168 | bb.get(bytes); 169 | long p = other.position() + relativeLeafKeyPosition(0); 170 | bb.position(p); 171 | bb.put(bytes); 172 | // set the number of keys in source node to be `start` 173 | leafSetNumKeys(position, start); 174 | leafSetNumKeys(other.position(), length); 175 | } 176 | 177 | public void leafSetNext(long position, LeafFile sibling) { 178 | long p = position + relativeLeafKeyPosition(options.maxLeafKeys()); 179 | long v; 180 | if (sibling == null) { 181 | v = POSITION_NOT_PRESENT; 182 | } else { 183 | v = sibling.position(); 184 | } 185 | bb.position(p); 186 | bb.putLong(v); 187 | } 188 | 189 | public LeafFile leafNext(long position) { 190 | bb.position(position + relativeLeafKeyPosition(options.maxLeafKeys())); 191 | long p = bb.getLong(); 192 | if (p == POSITION_NOT_PRESENT) { 193 | return null; 194 | } else { 195 | return new LeafFile(this, p); 196 | } 197 | } 198 | 199 | ////////////////////////////////////////////////// 200 | // Format of a NonLeaf 201 | // NODE_TYPE NUM_KEYS (LEFT_CHILD_POSITION KEY)* RIGHT_CHILD_POSITION 202 | // where 203 | // NODE_TYPE is one byte (0 = Leaf, 1 = NonLeaf) 204 | // NUM_KEYS is 1 byte unsigned 205 | // LEFT_CHILD_POSITION is 8 bytes signed long 206 | // KEY is a fixed size byte array 207 | // RIGHT_CHILD_POSITION is 8 bytes signed long 208 | // Every NonLeaf has space allocated for maxNonLeafKeys keys 209 | ////////////////////////////////////////////////// 210 | 211 | @Override 212 | public NonLeaf createNonLeaf() { 213 | return new NonLeafFile(this, nextNonLeafPosition()); 214 | } 215 | 216 | private int nonLeafBytes() { 217 | // every key has a child node to the left and the final key has a child node to 218 | // the right as well as the left 219 | return NODE_TYPE_BYTES + NUM_NODES_BYTES + options.maxNonLeafKeys() * (POSITION_BYTES + keySerializer.maxSize()) 220 | + POSITION_BYTES; 221 | } 222 | 223 | private long nextNonLeafPosition() { 224 | long i = index; 225 | bb.position(index); 226 | bb.put((byte) NonLeaf.TYPE); 227 | index += nonLeafBytes(); 228 | return i; 229 | } 230 | 231 | public void nonLeafSetNumKeys(long position, int numKeys) { 232 | bb.position(position + NODE_TYPE_BYTES); 233 | bb.put((byte) numKeys); 234 | } 235 | 236 | public int nonLeafNumKeys(long position) { 237 | bb.position(position + NODE_TYPE_BYTES); 238 | return bb.get() & 0xFF; 239 | } 240 | 241 | public void nonLeafSetChild(long position, int i, NodeFile node) { 242 | long p = position + relativePositionNonLeafEntry(i); 243 | bb.position(p); 244 | bb.putLong(node.position()); 245 | } 246 | 247 | private int relativePositionNonLeafEntry(int i) { 248 | return NODE_TYPE_BYTES + NUM_KEYS_BYTES + i * (POSITION_BYTES + keySerializer.maxSize()); 249 | } 250 | 251 | public Node nonLeafChild(long position, int i) { 252 | bb.position(position + relativePositionNonLeafEntry(i)); 253 | long pos = bb.getLong(); 254 | return readNode(pos); 255 | } 256 | 257 | private Node readNode(long pos) { 258 | bb.position(pos); 259 | int type = bb.get(); 260 | if (type == Leaf.TYPE) { 261 | return new LeafFile(this, pos); 262 | // return getLeaf(pos); 263 | } else { 264 | return new NonLeafFile(this, pos); 265 | } 266 | } 267 | 268 | public K nonLeafKey(long position, int i) { 269 | bb.position(position + relativePositionNonLeafEntry(i) + POSITION_BYTES); 270 | return keySerializer.read(bb); 271 | } 272 | 273 | public void nonLeafSetKey(long position, int i, K key) { 274 | bb.position(position + relativePositionNonLeafEntry(i) + POSITION_BYTES); 275 | keySerializer.write(bb, key); 276 | } 277 | 278 | public void nonLeafMove(long position, int mid, int length, NonLeafFile other) { 279 | // read array corresponding to latter half of source node and put at beginning 280 | // of other node 281 | int relativeStart = relativePositionNonLeafEntry(mid); 282 | int size = relativePositionNonLeafEntry(mid + length + 1) - relativeStart; 283 | bb.position(position + relativeStart); 284 | byte[] bytes = new byte[size]; 285 | bb.get(bytes); 286 | long newPosition = other.position() + relativePositionNonLeafEntry(0); 287 | bb.position(newPosition); 288 | bb.put(bytes); 289 | nonLeafSetNumKeys(position, mid - 1); 290 | nonLeafSetNumKeys(other.position(), length); 291 | } 292 | 293 | public void nonLeafInsert(long position, int i, K key, NodeFile left) { 294 | int numKeys = nonLeafNumKeys(position); 295 | int relativeStart = relativePositionNonLeafEntry(i); 296 | int relativeEnd = relativePositionNonLeafEntry(numKeys) + POSITION_BYTES; 297 | bb.position(position + relativeStart); 298 | byte[] bytes = new byte[relativeEnd - relativeStart]; 299 | bb.get(bytes); 300 | bb.position(position + relativePositionNonLeafEntry(i + 1)); 301 | bb.put(bytes); 302 | bb.position(position + relativeStart); 303 | bb.putLong(left.position()); 304 | keySerializer.write(bb, key); 305 | nonLeafSetNumKeys(position, numKeys + 1); 306 | } 307 | 308 | @Override 309 | public void close() throws Exception { 310 | bb.close(); 311 | values.close(); 312 | if (onClose != null) { 313 | onClose.run(); 314 | } 315 | } 316 | 317 | @Override 318 | public void commit() { 319 | values.commit(); 320 | metadata.position(0); 321 | metadata.putLong(valuesIndex); 322 | metadata.commit(); 323 | bb.commit(); 324 | } 325 | 326 | @Override 327 | public void root(Node node) { 328 | bb.position(0); 329 | bb.putLong(((NodeFile) node).position()); 330 | } 331 | 332 | @Override 333 | public Node loadOrCreateRoot() { 334 | bb.position(0); 335 | long rootPosition = bb.getLong(); 336 | if (rootPosition == 0) { 337 | bb.position(0); 338 | bb.putLong(POSITION_BYTES); 339 | return createLeaf(); 340 | } else { 341 | metadata.position(0); 342 | valuesIndex = metadata.getLong(); 343 | return readNode(rootPosition); 344 | } 345 | } 346 | 347 | @Override 348 | public Options options() { 349 | return options; 350 | } 351 | 352 | } 353 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/file/LeafFile.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.file; 2 | 3 | import com.github.davidmoten.bplustree.internal.Leaf; 4 | import com.github.davidmoten.bplustree.internal.Options; 5 | 6 | public class LeafFile implements Leaf, NodeFile { 7 | 8 | private final FactoryFile factory; 9 | private long position; 10 | 11 | public LeafFile(FactoryFile factory, long position) { 12 | this.factory = factory; 13 | this.position = position; 14 | } 15 | 16 | @Override 17 | public K key(int i) { 18 | return factory.leafKey(position, i); 19 | } 20 | 21 | @Override 22 | public int numKeys() { 23 | return factory.leafNumKeys(position); 24 | } 25 | 26 | @Override 27 | public FactoryFile factory() { 28 | return factory; 29 | } 30 | 31 | @Override 32 | public Options options() { 33 | return factory.options(); 34 | } 35 | 36 | @Override 37 | public V value(int index) { 38 | return factory.leafValue(position, index); 39 | } 40 | 41 | @Override 42 | public void setNumKeys(int numKeys) { 43 | factory.leafSetNumKeys(position, numKeys); 44 | } 45 | 46 | @Override 47 | public void setValue(int idx, V value) { 48 | factory.leafSetValue(position, idx, value); 49 | } 50 | 51 | @Override 52 | public void insert(int idx, K key, V value) { 53 | factory.leafInsert(position, idx, key, value); 54 | } 55 | 56 | @Override 57 | public void move(int start, int length, Leaf other) { 58 | factory.leafMove(position, start, length, (LeafFile) other); 59 | } 60 | 61 | @Override 62 | public void setNext(Leaf sibling) { 63 | factory.leafSetNext(position, (LeafFile) sibling); 64 | } 65 | 66 | @Override 67 | public LeafFile next() { 68 | return factory.leafNext(position); 69 | } 70 | 71 | @Override 72 | public long position() { 73 | return position; 74 | } 75 | 76 | @Override 77 | public void position(long position) { 78 | this.position = position; 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | StringBuilder b = new StringBuilder(); 84 | b.append("LeafFile ["); 85 | b.append("position="); 86 | b.append(position); 87 | b.append(", numKeys="); 88 | b.append(numKeys()); 89 | b.append(", keyValues=["); 90 | StringBuilder b2 = new StringBuilder(); 91 | int n = numKeys(); 92 | for (int i = 0; i < n; i++) { 93 | if (b2.length() > 0) { 94 | b2.append(", "); 95 | } 96 | b2.append(key(i)); 97 | b2.append("->"); 98 | b2.append(value(i)); 99 | } 100 | b.append(b2.toString()); 101 | b.append("]"); 102 | b.append("]"); 103 | return b.toString(); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/file/NodeFile.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.file; 2 | 3 | public interface NodeFile { 4 | long position(); 5 | 6 | void position(long position); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/file/NonLeafFile.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.file; 2 | 3 | import com.github.davidmoten.bplustree.internal.Factory; 4 | import com.github.davidmoten.bplustree.internal.Node; 5 | import com.github.davidmoten.bplustree.internal.NonLeaf; 6 | import com.github.davidmoten.bplustree.internal.Options; 7 | 8 | public final class NonLeafFile implements NonLeaf, NodeFile { 9 | 10 | private final FactoryFile factory; 11 | private long position; 12 | 13 | public NonLeafFile(FactoryFile factory, long position) { 14 | this.factory = factory; 15 | this.position = position; 16 | } 17 | 18 | @Override 19 | public Options options() { 20 | return factory.options(); 21 | } 22 | 23 | @Override 24 | public Factory factory() { 25 | return factory; 26 | } 27 | 28 | @Override 29 | public void setNumKeys(int numKeys) { 30 | factory.nonLeafSetNumKeys(position, numKeys); 31 | } 32 | 33 | @Override 34 | public int numKeys() { 35 | return factory.nonLeafNumKeys(position); 36 | } 37 | 38 | @Override 39 | public void setChild(int index, Node node) { 40 | factory.nonLeafSetChild(position, index, (NodeFile) node); 41 | } 42 | 43 | @Override 44 | public Node child(int index) { 45 | return factory.nonLeafChild(position, index); 46 | } 47 | 48 | @Override 49 | public K key(int index) { 50 | return factory.nonLeafKey(position, index); 51 | } 52 | 53 | @Override 54 | public void setKey(int index, K key) { 55 | factory.nonLeafSetKey(position, index, key); 56 | } 57 | 58 | @Override 59 | public void move(int mid, NonLeaf other, int length) { 60 | factory.nonLeafMove(position, mid, length, (NonLeafFile) other); 61 | 62 | } 63 | 64 | @Override 65 | public void insert(int idx, K key, Node left) { 66 | factory.nonLeafInsert(position, idx, key, (NodeFile) left); 67 | } 68 | 69 | @Override 70 | public long position() { 71 | return position; 72 | } 73 | 74 | @Override 75 | public void position(long position) { 76 | this.position = position; 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | StringBuilder b = new StringBuilder(); 82 | b.append("NonLeafFile ["); 83 | b.append("position="); 84 | b.append(position); 85 | b.append(", numKeys="); 86 | b.append(numKeys()); 87 | b.append(", keys=["); 88 | StringBuilder b2 = new StringBuilder(); 89 | int n = numKeys(); 90 | for (int i = 0; i < n; i++) { 91 | if (b2.length() > 0) { 92 | b2.append(", "); 93 | } 94 | b2.append(key(i)); 95 | } 96 | b.append(b2.toString()); 97 | b.append("]"); 98 | b.append("]"); 99 | return b.toString(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/memory/FactoryMemory.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.memory; 2 | 3 | import com.github.davidmoten.bplustree.internal.Factory; 4 | import com.github.davidmoten.bplustree.internal.Leaf; 5 | import com.github.davidmoten.bplustree.internal.Node; 6 | import com.github.davidmoten.bplustree.internal.NonLeaf; 7 | import com.github.davidmoten.bplustree.internal.Options; 8 | 9 | public final class FactoryMemory implements Factory { 10 | 11 | private final Options options; 12 | 13 | public FactoryMemory(Options options) { 14 | this.options = options; 15 | } 16 | 17 | @Override 18 | public Leaf createLeaf() { 19 | return new LeafMemory(options, this); 20 | } 21 | 22 | @Override 23 | public NonLeaf createNonLeaf() { 24 | return new NonLeafMemory(options, this); 25 | } 26 | 27 | @Override 28 | public void close() throws Exception { 29 | // do nothing 30 | } 31 | 32 | @Override 33 | public void commit() { 34 | // do nothing 35 | } 36 | 37 | @Override 38 | public void root(Node node) { 39 | // do nothing 40 | } 41 | 42 | @Override 43 | public Node loadOrCreateRoot() { 44 | return createLeaf(); 45 | } 46 | 47 | @Override 48 | public Options options() { 49 | return options; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/memory/LeafMemory.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.memory; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.github.davidmoten.bplustree.internal.Factory; 6 | import com.github.davidmoten.bplustree.internal.Leaf; 7 | import com.github.davidmoten.bplustree.internal.Options; 8 | 9 | public final class LeafMemory implements Leaf { 10 | 11 | private final Options options; 12 | private final Factory factory; 13 | private final K[] keys; 14 | private final V[] values; 15 | private int numKeys; 16 | private Leaf next; 17 | 18 | @SuppressWarnings("unchecked") 19 | public LeafMemory(Options options, Factory factory) { 20 | this.options = options; 21 | keys = (K[]) new Object[options.maxLeafKeys()]; 22 | values = (V[]) new Object[options.maxLeafKeys()]; 23 | this.factory = factory; 24 | } 25 | 26 | @Override 27 | public V value(int index) { 28 | return values[index]; 29 | } 30 | 31 | @Override 32 | public K key(int index) { 33 | return keys[index]; 34 | } 35 | 36 | @Override 37 | public int numKeys() { 38 | return numKeys; 39 | } 40 | 41 | @Override 42 | public void move(int start, int length, Leaf other) { 43 | other.setNumKeys(length); 44 | System.arraycopy(keys, start, ((LeafMemory) other).keys, 0, length); 45 | System.arraycopy(values, start, ((LeafMemory) other).values, 0, length); 46 | numKeys = start; 47 | } 48 | 49 | @Override 50 | public void setNumKeys(int numKeys) { 51 | this.numKeys = numKeys; 52 | } 53 | 54 | @Override 55 | public void setValue(int idx, V value) { 56 | values[idx] = value; 57 | } 58 | 59 | @Override 60 | public void insert(int idx, K key, V value) { 61 | System.arraycopy(keys, idx, keys, idx + 1, numKeys - idx); 62 | System.arraycopy(values, idx, values, idx + 1, numKeys - idx); 63 | keys[idx] = key; 64 | values[idx] = value; 65 | numKeys++; 66 | } 67 | 68 | @Override 69 | public void setNext(Leaf next) { 70 | this.next = next; 71 | } 72 | 73 | @Override 74 | public Leaf next() { 75 | return next; 76 | } 77 | 78 | @Override 79 | public Options options() { 80 | return options; 81 | } 82 | 83 | @Override 84 | public Factory factory() { 85 | return factory; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | StringBuilder builder = new StringBuilder(); 91 | builder.append("LeafMemory ["); 92 | builder.append("numKeys="); 93 | builder.append(numKeys); 94 | builder.append(", keys="); 95 | builder.append(Arrays.toString(keys)); 96 | builder.append(", values="); 97 | builder.append(Arrays.toString(values)); 98 | builder.append("]"); 99 | return builder.toString(); 100 | } 101 | 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/memory/NonLeafMemory.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.memory; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.github.davidmoten.bplustree.internal.Factory; 6 | import com.github.davidmoten.bplustree.internal.Node; 7 | import com.github.davidmoten.bplustree.internal.NonLeaf; 8 | import com.github.davidmoten.bplustree.internal.Options; 9 | 10 | public final class NonLeafMemory implements NonLeaf { 11 | 12 | private final Options options; 13 | private final Factory factory; 14 | private final Node[] children; 15 | private final K[] keys; 16 | private int numKeys; // number of keys 17 | 18 | @SuppressWarnings("unchecked") 19 | NonLeafMemory(Options options, Factory factory) { 20 | this.options = options; 21 | this.factory = factory; 22 | this.children = new Node[options.maxNonLeafKeys() + 1]; 23 | this.keys = (K[]) new Object[options.maxLeafKeys()]; 24 | } 25 | 26 | @Override 27 | public void setNumKeys(int numKeys) { 28 | this.numKeys = numKeys; 29 | } 30 | 31 | @Override 32 | public int numKeys() { 33 | return numKeys; 34 | } 35 | 36 | @Override 37 | public void setChild(int index, Node node) { 38 | children[index] = node; 39 | } 40 | 41 | @Override 42 | public Node child(int index) { 43 | return children[index]; 44 | } 45 | 46 | @Override 47 | public K key(int index) { 48 | return keys[index]; 49 | } 50 | 51 | @Override 52 | public void setKey(int index, K key) { 53 | keys[index] = key; 54 | } 55 | 56 | @Override 57 | public void move(int mid, NonLeaf other, int length) { 58 | other.setNumKeys(length); 59 | System.arraycopy(this.keys, mid, ((NonLeafMemory) other).keys, 0, length); 60 | System.arraycopy(this.children, mid, ((NonLeafMemory) other).children, 0, length + 1); 61 | numKeys = mid - 1;// this is important, so the middle one elevates to next 62 | // depth(height), inner node's key don't repeat itself 63 | } 64 | 65 | @Override 66 | public void insert(int idx, K key, Node node) { 67 | System.arraycopy(keys, idx, keys, idx + 1, numKeys - idx); 68 | System.arraycopy(children, idx, children, idx + 1, numKeys - idx + 1); 69 | children[idx] = node; 70 | keys[idx] = key; 71 | numKeys+=1; 72 | } 73 | 74 | @Override 75 | public Options options() { 76 | return options; 77 | } 78 | 79 | @Override 80 | public Factory factory() { 81 | return factory; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | StringBuilder builder = new StringBuilder(); 87 | builder.append("NonLeafMemory ["); 88 | builder.append("numKeys=" + numKeys()); 89 | builder.append(", keys="); 90 | builder.append(Arrays.toString(keys)); 91 | builder.append("]"); 92 | return builder.toString(); 93 | } 94 | 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/util/LazyList.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Supplier; 6 | 7 | public final class LazyList { 8 | 9 | private final int size; 10 | private final Supplier factory; 11 | private final List list = new ArrayList(); 12 | 13 | public LazyList(int size, Supplier factory) { 14 | this.size = size; 15 | this.factory = factory; 16 | } 17 | 18 | public T get(int index) { 19 | if (index >= size) { 20 | throw new ArrayIndexOutOfBoundsException(); 21 | } else { 22 | if (index >= list.size()) { 23 | for (int j = list.size(); j <= index; j++) { 24 | list.add(null); 25 | } 26 | } 27 | T v = list.get(index); 28 | if (v == null) { 29 | v = factory.get(); 30 | list.set(index, v); 31 | } 32 | return v; 33 | } 34 | } 35 | 36 | public int size() { 37 | return size; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/davidmoten/bplustree/internal/util/Pool.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal.util; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Queue; 5 | import java.util.function.Supplier; 6 | 7 | import com.github.davidmoten.guavamini.Preconditions; 8 | 9 | public final class Pool { 10 | 11 | private final int maxSize; 12 | private final Supplier factory; 13 | 14 | private final Queue queue; 15 | private int checkedOut; 16 | 17 | public Pool(int maxSize, Supplier factory) { 18 | Preconditions.checkArgument(maxSize > 0); 19 | this.factory = factory; 20 | this.queue = new ArrayDeque<>(); 21 | this.checkedOut = 0; 22 | this.maxSize = maxSize; 23 | } 24 | 25 | public T get() { 26 | if (checkedOut == maxSize) { 27 | throw new IllegalStateException("cannot get item from pool because pool already at max size of " + maxSize); 28 | } 29 | T t = queue.poll(); 30 | if (t == null) { 31 | t = factory.get(); 32 | } 33 | checkedOut++; 34 | return t; 35 | } 36 | 37 | public void release(T t) { 38 | checkedOut--; 39 | queue.offer(t); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/BPlusTreeFileTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertNull; 6 | 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.NoSuchElementException; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | import org.davidmoten.kool.Stream; 17 | import org.junit.Test; 18 | 19 | import com.github.davidmoten.bplustree.internal.file.LeafFile; 20 | import com.github.davidmoten.guavamini.Lists; 21 | 22 | public final class BPlusTreeFileTest { 23 | 24 | private static BPlusTree create(int maxKeys) { 25 | return create(Testing.newDirectory(), maxKeys); 26 | } 27 | 28 | private static BPlusTree create(File dir, int maxKeys) { 29 | return BPlusTree.file() // 30 | .directory(dir) // 31 | .clearDirectory() // 32 | .maxKeys(maxKeys) // 33 | .segmentSizeMB(1) // 34 | .keySerializer(Serializer.INTEGER) // 35 | .valueSerializer(Serializer.INTEGER) // 36 | .naturalOrder(); 37 | } 38 | 39 | @Test 40 | public void testInsertOne() { 41 | BPlusTree tree = create(2); 42 | tree.insert(3, 10); 43 | tree.commit(); 44 | LeafFile leaf = (LeafFile) tree.root(); 45 | assertEquals(1, leaf.numKeys()); 46 | assertEquals(3, (int) leaf.key(0)); 47 | assertEquals(10, (int) leaf.value(0)); 48 | NodeWrapper t = NodeWrapper.root(tree); 49 | assertEquals(Arrays.asList(3), t.keys()); 50 | assertEquals(10, (int) tree.findFirst(3)); 51 | assertNull(tree.findFirst(4)); 52 | } 53 | 54 | @Test 55 | public void testInsertTwo() { 56 | BPlusTree tree = create(2); 57 | tree.insert(3, 10); 58 | tree.insert(5, 20); 59 | LeafFile leaf = (LeafFile) tree.root(); 60 | assertEquals(2, leaf.numKeys()); 61 | assertEquals(3, (int) leaf.key(0)); 62 | assertEquals(10, (int) leaf.value(0)); 63 | assertEquals(5, (int) leaf.key(1)); 64 | assertEquals(20, (int) leaf.value(1)); 65 | NodeWrapper t = NodeWrapper.root(tree); 66 | assertEquals(Arrays.asList(3, 5), t.keys()); 67 | assertEquals(10, (int) tree.findFirst(3)); 68 | assertEquals(20, (int) tree.findFirst(5)); 69 | } 70 | 71 | @Test 72 | public void testInsertTwoReverseOrderWhichTestsInsertMethodOnLeaf() { 73 | BPlusTree tree = create(2); 74 | tree.insert(5, 20); 75 | tree.insert(3, 10); 76 | LeafFile leaf = (LeafFile) tree.root(); 77 | assertEquals(2, leaf.numKeys()); 78 | assertEquals(3, (int) leaf.key(0)); 79 | assertEquals(10, (int) leaf.value(0)); 80 | assertEquals(5, (int) leaf.key(1)); 81 | assertEquals(20, (int) leaf.value(1)); 82 | NodeWrapper t = NodeWrapper.root(tree); 83 | assertEquals(Arrays.asList(3, 5), t.keys()); 84 | assertEquals(10, (int) tree.findFirst(3)); 85 | assertEquals(20, (int) tree.findFirst(5)); 86 | } 87 | 88 | @Test 89 | public void testInsertThreeWithMaxLeafKeysTwo() { 90 | BPlusTree tree = create(2); 91 | tree.insert(3, 10); 92 | tree.insert(5, 20); 93 | tree.insert(7, 30); 94 | NodeWrapper t = NodeWrapper.root(tree); 95 | assertEquals(Arrays.asList(5), t.keys()); 96 | List> children = t.children(); 97 | assertEquals(2, children.size()); 98 | assertEquals(Arrays.asList(3), children.get(0).keys()); 99 | assertEquals(Arrays.asList(5, 7), children.get(1).keys()); 100 | assertEquals(10, (int) tree.findFirst(3)); 101 | assertEquals(20, (int) tree.findFirst(5)); 102 | assertEquals(30, (int) tree.findFirst(7)); 103 | } 104 | 105 | @Test 106 | public void testInsertMany() { 107 | int numKeysPerNode = Integer.parseInt(System.getProperty("numKeys", "32")); 108 | int iterations = Integer.parseInt(System.getProperty("n", "1")); 109 | for (int j = 0; j < iterations; j++) { 110 | long t = System.currentTimeMillis(); 111 | int n = 1_000_000; 112 | { 113 | BPlusTree tree = create(numKeysPerNode); 114 | for (int i = 1; i <= n; i++) { 115 | int v = n - i + 1; 116 | tree.insert(v, v); 117 | } 118 | System.out.println("insert rate desc order= " 119 | + (n * 1000.0 / (System.currentTimeMillis() - t)) + " per second"); 120 | } 121 | { 122 | BPlusTree tree = create(numKeysPerNode); 123 | for (int i = 1; i <= n; i++) { 124 | tree.insert(i, i); 125 | } 126 | System.out.println("insert rate asc order = " 127 | + (n * 1000.0 / (System.currentTimeMillis() - t)) + " per second"); 128 | } 129 | } 130 | } 131 | 132 | @Test 133 | public void testRegexSpeed() { 134 | String s = "2019-11-06 23:13:00.427 DEBUG com.zaxxer.hikari.pool.HikariPool [HikariPool-2 housekeeper] - HikariPool-2 - Before cleanup stats (total=5, active=3, idle=2, waiting=0)"; 135 | Pattern p = Pattern.compile( 136 | "^.*com.zaxxer.hikari.pool.HikariPool.*Before cleanup stats.*, active=([0-9]+).*$"); 137 | long t = System.currentTimeMillis(); 138 | int n = 100000; 139 | for (int i = 0; i < n; i++) { 140 | Matcher m = p.matcher(s); 141 | if (m.find()) { 142 | if (m.group(1).equals("blah")) { 143 | System.out.println("hello"); 144 | } 145 | } 146 | } 147 | System.out.println("regex match rate = " + n * 1000.0 / (System.currentTimeMillis() - t) 148 | + " lines per second"); 149 | } 150 | 151 | @Test 152 | public void testWithBiggishInts() { 153 | BPlusTree tree = BPlusTree // 154 | .file() // 155 | .directory("target/bigints") // 156 | .clearDirectory() // 157 | .keySerializer(Serializer.INTEGER) // 158 | .valueSerializer(Serializer.INTEGER) // 159 | .comparator((a, b) -> Integer.compare(a, b)); 160 | tree.insert(-1220935264, 1); 161 | tree.insert(110327396, 2); 162 | tree.insert(99162322, 3); 163 | assertEquals(Arrays.asList(1, 3, 2), Stream.from(tree.findAll()).toList().get()); 164 | } 165 | 166 | @Test 167 | public void testInsert3201() throws Exception { 168 | try (BPlusTree tree = BPlusTree // 169 | .file() // 170 | .directory("target/insertSome") // 171 | .clearDirectory() // 172 | .maxKeys(2) // 173 | .keySerializer(Serializer.INTEGER) // l 174 | .valueSerializer(Serializer.INTEGER) // 175 | .comparator((a, b) -> Integer.compare(a, b))) { 176 | tree.insert(3, 300); 177 | tree.insert(2, 200); 178 | tree.insert(0, 0); 179 | tree.insert(1, 100); 180 | assertEquals(Lists.newArrayList(0, 100, 200, 300), 181 | Stream.from(tree.findAll()).toList().get()); 182 | } 183 | } 184 | 185 | @Test 186 | public void testFindAllNextWhenNone() { 187 | BPlusTree tree = BPlusTree // 188 | .file() // 189 | .directory("target/findall") // 190 | .clearDirectory() // 191 | .maxKeys(2) // 192 | .keySerializer(Serializer.INTEGER) // 193 | .valueSerializer(Serializer.INTEGER) // 194 | .comparator((a, b) -> Integer.compare(a, b)); 195 | Iterator it = tree.findAll().iterator(); 196 | assertFalse(it.hasNext()); 197 | try { 198 | it.next(); 199 | org.junit.Assert.fail(); 200 | } catch (NoSuchElementException e) { 201 | // ok 202 | } 203 | } 204 | 205 | 206 | @Test 207 | public void testCreateAndReopen() throws Exception { 208 | File dir = Testing.newDirectory(); 209 | int maxKeys = 4; 210 | BPlusTree tree = BPlusTree // 211 | .file() // 212 | .directory(dir) // 213 | .clearDirectory() // 214 | .maxKeys(maxKeys) // 215 | .segmentSizeMB(1) // 216 | .keySerializer(Serializer.INTEGER) // 217 | .valueSerializer(Serializer.INTEGER) // 218 | .naturalOrder(); 219 | tree.insert(1, 4); 220 | tree.commit(); 221 | assertEquals(Arrays.asList(4), toList(tree.findAll())); 222 | tree.close(); 223 | tree = BPlusTree // 224 | .file() // 225 | .directory(dir) // 226 | .maxKeys(maxKeys) // 227 | .segmentSizeMB(1) // 228 | .keySerializer(Serializer.INTEGER) // 229 | .valueSerializer(Serializer.INTEGER) // 230 | .naturalOrder(); 231 | assertEquals(Arrays.asList(4), toList(tree.findAll())); 232 | tree.insert(2, 5); 233 | assertEquals(Arrays.asList(4, 5), toList(tree.findAll())); 234 | tree.close(); 235 | } 236 | 237 | @Test 238 | public void testCreateAndReopen2() throws Exception { 239 | File dir = Testing.newDirectory(); 240 | int maxKeys = 4; 241 | BPlusTree tree = BPlusTree // 242 | .file() // 243 | .directory(dir) // 244 | .clearDirectory() // 245 | .maxKeys(maxKeys) // 246 | .segmentSizeMB(1) // 247 | .keySerializer(Serializer.INTEGER) // 248 | .valueSerializer(Serializer.INTEGER) // 249 | .naturalOrder(); 250 | tree.insert(0, 1); 251 | tree.insert(1, 2); 252 | tree.insert(2, 3); 253 | tree.commit(); 254 | assertEquals(Arrays.asList(1, 2, 3), toList(tree.findAll())); 255 | tree.close(); 256 | tree = BPlusTree // 257 | .file() // 258 | .directory(dir) // 259 | .maxKeys(maxKeys) // 260 | .segmentSizeMB(1) // 261 | .keySerializer(Serializer.INTEGER) // 262 | .valueSerializer(Serializer.INTEGER) // 263 | .naturalOrder(); 264 | assertEquals(Arrays.asList(1, 2, 3), toList(tree.findAll())); 265 | tree.insert(3, 4); 266 | assertEquals(Arrays.asList(1, 2, 3, 4), toList(tree.findAll())); 267 | tree.close(); 268 | } 269 | 270 | private static List toList(Iterable iterable) { 271 | Iterator it = iterable.iterator(); 272 | List list = new ArrayList<>(); 273 | while (it.hasNext()) { 274 | list.add(it.next()); 275 | } 276 | return list; 277 | } 278 | 279 | // public static void main(String[] args) { 280 | // BPlusTree tree = BPlusTree // 281 | // .file() // 282 | // .directory(Testing.newDirectory()) // 283 | // .maxKeys(8) // 284 | // .keySerializer(Serializer.LONG) // 285 | // .valueSerializer(Serializer.LONG) // 286 | // .naturalOrder(); 287 | // long i = 1; 288 | // long t = System.currentTimeMillis(); 289 | // while (true) { 290 | // tree.insert(i, i); 291 | // if (i % 1000000 == 0) { 292 | // long t2 = System.currentTimeMillis(); 293 | // System.out.println(i / 1000000 + "m, insertRate=" + 1000000 * 1000.0 / (t2 - t) 294 | // + " per second"); 295 | // t = t2; 296 | // } 297 | // i++; 298 | // } 299 | // } 300 | 301 | public static void main(String[] args) { 302 | BPlusTree tree; 303 | int choice = 1; 304 | if (choice == 1) { 305 | tree = BPlusTree.file().directory("").maxLeafKeys(255).maxNonLeafKeys(255).uniqueKeys(true).segmentSizeMB(4) 306 | .keySerializer(Serializer.LONG).valueSerializer(Serializer.utf8()).naturalOrder(); 307 | for (int i = 0; i < 100; i++) { 308 | tree.insert(i + 1L, "value" + i); 309 | } 310 | } else if (choice == 2) { 311 | tree = BPlusTree.file().directory("").maxLeafKeys(255).maxNonLeafKeys(255).uniqueKeys(true).segmentSizeMB(4) 312 | .keySerializer(Serializer.LONG).valueSerializer(Serializer.utf8()).naturalOrder(); 313 | for (int i = 100; i < 200; i++) { 314 | tree.insert(i + 1L, "value" + i); 315 | } 316 | } 317 | } 318 | 319 | } 320 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/BPlusTreeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertNull; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.function.Function; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | 18 | import org.davidmoten.kool.Stream; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | import org.junit.runners.Parameterized; 22 | import org.junit.runners.Parameterized.Parameters; 23 | 24 | import com.github.davidmoten.guavamini.Lists; 25 | 26 | @RunWith(Parameterized.class) 27 | public class BPlusTreeTest { 28 | 29 | private static final Function> creatorFile = maxKeys -> { 30 | 31 | return BPlusTree.file() // 32 | .directory(Testing.newDirectory()) // 33 | .clearDirectory() // 34 | .deleteOnClose() // 35 | .maxKeys(maxKeys) // 36 | .uniqueKeys(false) // 37 | .keySerializer(Serializer.INTEGER) // 38 | .valueSerializer(Serializer.INTEGER) // 39 | .naturalOrder(); 40 | }; 41 | 42 | private static final Function> creatorMemory = maxKeys -> BPlusTree 43 | .memory().maxKeys(maxKeys).naturalOrder(); 44 | 45 | @Parameters 46 | public static Collection creators() { 47 | return Arrays.asList(new Object[][] { { creatorMemory }, { creatorFile } }); 48 | } 49 | 50 | private final Function> creator; 51 | 52 | public BPlusTreeTest(Function> creator) { 53 | this.creator = creator; 54 | } 55 | 56 | private BPlusTree create(int maxKeys) { 57 | return creator.apply(maxKeys); 58 | } 59 | 60 | private static BPlusTree createWithStringValue(int maxKeys) { 61 | return BPlusTree.memory().maxKeys(maxKeys).naturalOrder(); 62 | } 63 | 64 | @Test 65 | public void testFindOnEmptyTree() throws Exception { 66 | try (BPlusTree t = createWithStringValue(4)) { 67 | assertNull(t.findFirst(1)); 68 | } 69 | } 70 | 71 | @Test 72 | public void testAddElementAndFind() throws Exception { 73 | try (BPlusTree t = createWithStringValue(4)) { 74 | t.insert(1, "boo"); 75 | assertEquals("boo", t.findFirst(1)); 76 | } 77 | } 78 | 79 | @Test 80 | public void testAddManyAndFind() throws Exception { 81 | for (int m = 4; m <= 10; m++) { 82 | try (BPlusTree t = createWithStringValue(m)) { 83 | for (int n = 1; n < 1000; n++) { 84 | for (int i = 0; i < n; i++) { 85 | t.insert(i, "a" + i); 86 | } 87 | for (int i = n - 1; i >= 0; i--) { 88 | assertEquals("a" + i, t.findFirst(i)); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | @Test 96 | public void testAddManyShuffledAndFind() throws Exception { 97 | for (int m = 4; m <= 10; m++) { 98 | try (BPlusTree t = createWithStringValue(m)) { 99 | for (int n = 1; n <= 1000; n++) { 100 | List list = IntStream.range(0, n).boxed().collect(Collectors.toList()); 101 | Collections.shuffle(list); 102 | for (int i = 0; i < n; i++) { 103 | t.insert(list.get(i), "a" + list.get(i)); 104 | } 105 | for (int i = n - 1; i >= 0; i--) { 106 | assertEquals("a" + i, t.findFirst(i)); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | @Test 114 | public void testSplitsCorrect3Entries() throws Exception { 115 | // verified with 116 | // https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 117 | try (BPlusTree t = create(2)) { 118 | for (int i = 1; i <= 3; i++) { 119 | t.insert(i, i); 120 | } 121 | NodeWrapper root = NodeWrapper.root(t); 122 | assertEquals(Arrays.asList(2), root.keys()); 123 | List> children = root.children(); 124 | assertEquals(2, children.size()); 125 | assertEquals(Arrays.asList(1), children.get(0).keys()); 126 | assertEquals(Arrays.asList(2, 3), children.get(1).keys()); 127 | } 128 | } 129 | 130 | @Test 131 | public void testSplitsCorrect4Entries() throws Exception { 132 | // verified with 133 | // https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 134 | try (BPlusTree t = create(2)) { 135 | for (int i = 1; i <= 4; i++) { 136 | t.insert(i, i); 137 | } 138 | NodeWrapper root = NodeWrapper.root(t); 139 | assertEquals(Arrays.asList(2, 3), root.keys()); 140 | List> children = root.children(); 141 | assertEquals(3, children.size()); 142 | assertEquals(Arrays.asList(1), children.get(0).keys()); 143 | assertEquals(Arrays.asList(2), children.get(1).keys()); 144 | assertEquals(Arrays.asList(3, 4), children.get(2).keys()); 145 | } 146 | } 147 | 148 | // @Test 149 | // public void testStructureWithRepeats() throws Exception { 150 | // try (BPlusTree t = create(4)) { 151 | // t.insert(1, 10); 152 | // t.insert(1, 20); 153 | // t.insert(1, 30); 154 | // t.insert(1, 40); 155 | // t.insert(1, 50); 156 | // t.insert(1, 60); 157 | // t.print(); 158 | // t.findOrderPreserving(0, 2).forEach(System.out::println); 159 | // } 160 | // } 161 | 162 | @Test 163 | public void testSplitsCorrect5Entries() throws Exception { 164 | // verified with 165 | // https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 166 | try (BPlusTree t = create(2)) { 167 | for (int i = 1; i <= 5; i++) { 168 | t.insert(i, i); 169 | } 170 | NodeWrapper root = NodeWrapper.root(t); 171 | assertEquals(Arrays.asList(3), root.keys()); 172 | List> children = root.children(); 173 | assertEquals(Arrays.asList(3), root.keys()); 174 | assertEquals(2, children.size()); 175 | assertEquals(Arrays.asList(2), children.get(0).keys()); 176 | assertEquals(Arrays.asList(4), children.get(1).keys()); 177 | List> a = children.get(0).children(); 178 | List> b = children.get(1).children(); 179 | assertEquals(2, a.size()); 180 | assertEquals(Arrays.asList(1), a.get(0).keys()); 181 | assertEquals(Arrays.asList(2), a.get(1).keys()); 182 | assertEquals(2, b.size()); 183 | assertEquals(Arrays.asList(3), b.get(0).keys()); 184 | assertEquals(Arrays.asList(4, 5), b.get(1).keys()); 185 | } 186 | } 187 | 188 | @Test 189 | public void testStructureCorrect3Entries() throws Exception { 190 | // verified with 191 | // https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 192 | try (BPlusTree t = create(2)) { 193 | for (int i = 1; i <= 3; i++) { 194 | t.insert(i, i); 195 | } 196 | NodeWrapper root = NodeWrapper.root(t); 197 | assertEquals(Arrays.asList(2), root.keys()); 198 | List> children = root.children(); 199 | assertEquals(2, children.size()); 200 | assertEquals(Arrays.asList(1), children.get(0).keys()); 201 | assertEquals(Arrays.asList(2, 3), children.get(1).keys()); 202 | } 203 | } 204 | 205 | @Test 206 | public void testStructureCorrect4Entries() throws Exception { 207 | // verified with 208 | // https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 209 | try (BPlusTree t = create(2)) { 210 | for (int i = 1; i <= 4; i++) { 211 | t.insert(i, i); 212 | } 213 | t.print(); 214 | NodeWrapper root = NodeWrapper.root(t); 215 | assertEquals(Arrays.asList(2, 3), root.keys()); 216 | List> children = root.children(); 217 | assertEquals(3, children.size()); 218 | assertEquals(Arrays.asList(1), children.get(0).keys()); 219 | assertEquals(Arrays.asList(2), children.get(1).keys()); 220 | assertEquals(Arrays.asList(3, 4), children.get(2).keys()); 221 | } 222 | } 223 | 224 | @Test 225 | public void testPrint() throws Exception { 226 | try (BPlusTree t = create(2)) { 227 | for (int i = 1; i <= 10; i++) { 228 | t.insert(i, i); 229 | } 230 | t.print(); 231 | } 232 | } 233 | 234 | @Test 235 | public void testNextWithFindOneAtStart() throws Exception { 236 | try (BPlusTree t = create(2)) { 237 | for (int i = 1; i <= 10; i++) { 238 | t.insert(i, i); 239 | } 240 | Iterator it = t.find(1, 2).iterator(); 241 | assertTrue(it.hasNext()); 242 | assertEquals(1, (int) it.next()); 243 | assertFalse(it.hasNext()); 244 | } 245 | } 246 | 247 | @Test 248 | public void testNextWithFindTwoAtStart() throws Exception { 249 | try (BPlusTree t = create(2)) { 250 | for (int i = 1; i <= 10; i++) { 251 | t.insert(i, i); 252 | } 253 | Iterator it = t.find(1, 3).iterator(); 254 | assertTrue(it.hasNext()); 255 | assertEquals(1, (int) it.next()); 256 | assertTrue(it.hasNext()); 257 | assertEquals(2, (int) it.next()); 258 | assertFalse(it.hasNext()); 259 | } 260 | } 261 | 262 | @Test 263 | public void testNextWithFindTwoOverlappingEnd() throws Exception { 264 | try (BPlusTree t = create(2)) { 265 | for (int i = 1; i <= 10; i++) { 266 | t.insert(i, i); 267 | } 268 | Iterator it = t.find(9, 11).iterator(); 269 | assertTrue(it.hasNext()); 270 | assertEquals(9, (int) it.next()); 271 | assertTrue(it.hasNext()); 272 | assertEquals(10, (int) it.next()); 273 | assertFalse(it.hasNext()); 274 | } 275 | } 276 | 277 | @Test 278 | public void testNextWithFind2() throws Exception { 279 | try (BPlusTree t = create(2)) { 280 | for (int i = 1; i <= 10; i++) { 281 | t.insert(i, i); 282 | } 283 | Iterator it = t.find(1, 3).iterator(); 284 | assertTrue(it.hasNext()); 285 | assertEquals(1, (int) it.next()); 286 | assertTrue(it.hasNext()); 287 | assertEquals(2, (int) it.next()); 288 | assertFalse(it.hasNext()); 289 | } 290 | } 291 | 292 | @Test 293 | public void testFindRange() throws Exception { 294 | try (BPlusTree t = create(2)) { 295 | for (int i = 1; i <= 10; i++) { 296 | t.insert(i, i); 297 | } 298 | assertEquals(Arrays.asList(1), toList(t.find(1, 2))); 299 | assertEquals(Arrays.asList(1, 2), toList(t.find(1, 3))); 300 | assertEquals(Arrays.asList(9, 10), toList(t.find(9, 11))); 301 | assertEquals(Arrays.asList(), toList(t.find(11, 20))); 302 | assertEquals(Arrays.asList(), toList(t.find(-3, -1))); 303 | } 304 | } 305 | 306 | private static List toList(Iterable iterable) { 307 | List list = new ArrayList<>(); 308 | iterable.forEach(list::add); 309 | return list; 310 | } 311 | 312 | @Test 313 | public void testDuplicateSupportedAndInReverseOrderOfInsert() throws Exception { 314 | try (BPlusTree t = create(2)) { 315 | t.insert(1, 2); 316 | t.insert(1, 3); 317 | assertEquals(Lists.newArrayList(3, 2), // 318 | toList(t.find(0, 4))); 319 | } 320 | } 321 | 322 | // @Test 323 | // public void testDuplicateSupportedAndOrderPreservedBySpecialFindMethodManyDifferentKeys() 324 | // throws Exception { 325 | // try (BPlusTree t = create(2)) { 326 | // t.insert(1, 12); 327 | // t.insert(1, 13); 328 | // t.insert(2, 21); 329 | // t.insert(2, 22); 330 | // t.insert(2, 23); 331 | // t.insert(3, 31); 332 | // assertEquals(Lists.newArrayList(12, 13, 21, 22, 23, 31), // 333 | // toList(t.findOrderPreserving(0, 4))); 334 | // } 335 | // } 336 | 337 | // @Test 338 | // public void testDuplicateSupportedAndOrderPreservedBySpecialFindMethod() throws Exception { 339 | // try (BPlusTree t = create(2)) { 340 | // t.insert(1, 12); 341 | // t.insert(1, 13); 342 | // t.insert(2, 21); 343 | // t.insert(2, 22); 344 | // t.insert(2, 23); 345 | // t.insert(3, 31); 346 | // assertEquals(Lists.newArrayList(12, 13, 21, 22, 23), // 347 | // toList(t.findOrderPreserving(0, 3))); 348 | // } 349 | // } 350 | 351 | // @Test 352 | // public void testDuplicateSupportedAndOrderPreservedBySpecialFindMethod2() throws Exception { 353 | // try (BPlusTree t = create(2)) { 354 | // t.insert(1, 10); 355 | // t.insert(1, 11); 356 | // t.insert(1, 12); 357 | // t.insert(1, 13); 358 | // t.insert(1, 14); 359 | // t.insert(1, 15); 360 | // assertEquals(Lists.newArrayList(10, 11, 12, 13, 14, 15), // 361 | // toList(t.findOrderPreserving(0, 3))); 362 | // } 363 | // } 364 | 365 | // @Test 366 | // public void testDuplicateSupportedAndOrderPreservedBySpecialFindMethodAllKeysSame() 367 | // throws Exception { 368 | // try (BPlusTree t = create(2)) { 369 | // t.insert(1, 2); 370 | // t.insert(1, 3); 371 | // assertEquals(Lists.newArrayList(2, 3), toList(t.findOrderPreserving(0, 4))); 372 | // } 373 | // } 374 | 375 | @Test 376 | public void testDuplicateNotSupportedWhenUniqueKeysSetToTrue() throws Exception { 377 | try (BPlusTree t = BPlusTree.memory().maxKeys(2).uniqueKeys() 378 | .naturalOrder()) { 379 | t.insert(1, 2); 380 | t.insert(1, 3); 381 | assertEquals(Lists.newArrayList(3), toList(t.find(0, 4))); 382 | } 383 | } 384 | 385 | @Test 386 | public void testClearOversize() { 387 | List list = Lists.newArrayList(1, 2, 3); 388 | List list2 = BPlusTree.clear(list, 2); 389 | assertTrue(list2.isEmpty()); 390 | assertTrue(list != list2); 391 | } 392 | 393 | @Test 394 | public void testClearUndersize() { 395 | List list = Lists.newArrayList(1, 2, 3); 396 | List list2 = BPlusTree.clear(list, 3); 397 | assertTrue(list2.isEmpty()); 398 | assertTrue(list == list2); 399 | } 400 | 401 | @Test 402 | public void testInsertt3201() throws Exception { 403 | try (BPlusTree tree = create(2)) { 404 | tree.insert(3, 300); 405 | tree.insert(2, 200); 406 | tree.insert(0, 0); 407 | tree.insert(1, 100); 408 | assertEquals(Lists.newArrayList(0, 100, 200, 300), 409 | Stream.from(tree.findAll()).toList().get()); 410 | } 411 | } 412 | 413 | @Test 414 | public void testInsert43210() throws Exception { 415 | try (BPlusTree tree = create(2)) { 416 | tree.insert(4, 400); 417 | tree.insert(3, 300); 418 | tree.insert(2, 200); 419 | tree.insert(1, 100); 420 | tree.insert(0, 7); 421 | assertEquals(Lists.newArrayList(7, 100, 200, 300, 400), 422 | Stream.from(tree.findAll()).toList().get()); 423 | } 424 | } 425 | 426 | @Test 427 | public void testInsert31024() throws Exception { 428 | try (BPlusTree tree = create(2)) { 429 | tree.insert(3, 300); 430 | tree.insert(1, 100); 431 | tree.insert(0, 7); 432 | tree.insert(2, 200); 433 | tree.insert(4, 400); 434 | assertEquals(Lists.newArrayList(7, 100, 200, 300, 400), 435 | Stream.from(tree.findAll()).toList().get()); 436 | } 437 | } 438 | 439 | @Test 440 | public void testTreeOnAllPermutationsOfNonRepeatedInput() throws Exception { 441 | for (int maxKeys = 2; maxKeys <= 5; maxKeys++) { 442 | int mk = maxKeys; 443 | for (int i = 5; i <= 5; i++) { 444 | List expected = new ArrayList<>(); 445 | for (int j = 0; j < i; j++) { 446 | expected.add(j); 447 | } 448 | Stream.permutations(i).doOnNext(list -> { 449 | try (BPlusTree tree = create(mk)) { 450 | for (int v : list) { 451 | tree.insert(v, v); 452 | } 453 | // assertEquals(0, (int) tree.firstLeaf(tree.root()).key(0)); 454 | assertEquals(expected, Stream.from(tree.findAll()).toList().get()); 455 | } 456 | }).forEach(); 457 | } 458 | } 459 | } 460 | 461 | @Test 462 | public void testLeafToString() throws Exception { 463 | try (BPlusTree tree = create(2)) { 464 | tree.insert(1, 1); 465 | assertTrue(tree.root().toString().startsWith("Leaf")); 466 | } 467 | } 468 | 469 | @Test 470 | public void testNonLeafToString() throws Exception { 471 | try (BPlusTree tree = create(2)) { 472 | tree.insert(1, 1); 473 | tree.insert(2, 2); 474 | tree.insert(3, 3); 475 | assertTrue(tree.root().toString().startsWith("NonLeaf")); 476 | } 477 | } 478 | 479 | @Test 480 | public void testFindOneKey() throws Exception { 481 | try (BPlusTree tree = create(2)) { 482 | tree.insert(1, 10); 483 | tree.insert(2, 20); 484 | Iterator it = tree.find(1).iterator(); 485 | assertEquals(10, (int) it.next()); 486 | assertFalse(it.hasNext()); 487 | } 488 | } 489 | 490 | @Test 491 | public void testFindOneKeyNotFound() throws Exception { 492 | try (BPlusTree tree = create(2)) { 493 | tree.insert(1, 10); 494 | tree.insert(2, 20); 495 | Iterator it = tree.find(3).iterator(); 496 | assertFalse(it.hasNext()); 497 | } 498 | } 499 | 500 | @Test 501 | public void testFindEntries() throws Exception { 502 | try (BPlusTree tree = create(2)) { 503 | tree.insert(1, 10); 504 | tree.insert(2, 20); 505 | Iterator> it = tree.findEntries(1, 2, true).iterator(); 506 | Entry entry = it.next(); 507 | assertEquals(1, (int) entry.key()); 508 | assertEquals(10, (int) entry.value()); 509 | entry = it.next(); 510 | assertEquals(2, (int) entry.key()); 511 | assertEquals(20, (int) entry.value()); 512 | assertFalse(it.hasNext()); 513 | } 514 | } 515 | 516 | @Test 517 | public void testFindEntriesExclusive() throws Exception { 518 | try (BPlusTree tree = create(2)) { 519 | tree.insert(1, 10); 520 | tree.insert(2, 20); 521 | Iterator> it = tree.findEntries(1, 2).iterator(); 522 | Entry entry = it.next(); 523 | assertEquals(1, (int) entry.key()); 524 | assertEquals(10, (int) entry.value()); 525 | assertFalse(it.hasNext()); 526 | } 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/Benchmarks.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.io.File; 4 | import java.util.Iterator; 5 | 6 | import org.mapdb.BTreeMap; 7 | import org.mapdb.DB; 8 | import org.mapdb.DBMaker; 9 | import org.openjdk.jmh.annotations.Benchmark; 10 | import org.openjdk.jmh.annotations.BenchmarkMode; 11 | import org.openjdk.jmh.annotations.Level; 12 | import org.openjdk.jmh.annotations.Measurement; 13 | import org.openjdk.jmh.annotations.Mode; 14 | import org.openjdk.jmh.annotations.Scope; 15 | import org.openjdk.jmh.annotations.Setup; 16 | import org.openjdk.jmh.annotations.State; 17 | import org.openjdk.jmh.annotations.TearDown; 18 | import org.openjdk.jmh.annotations.Warmup; 19 | 20 | public class Benchmarks { 21 | 22 | private static final LargeByteBuffer a = createLong(); 23 | private static final LargeByteBuffer b = createVarlong(); 24 | 25 | private static LargeByteBufferDelegating createLong() { 26 | LargeByteBufferDelegating b = new LargeByteBufferDelegating(); 27 | b.putLong(1234567); 28 | return b; 29 | } 30 | 31 | private static LargeByteBufferDelegating createVarlong() { 32 | LargeByteBufferDelegating b = new LargeByteBufferDelegating(); 33 | b.putVarlong(1234567); 34 | return b; 35 | } 36 | 37 | private static final int MAX_KEYS = 8; 38 | private static final int MANY = 2000000; 39 | private static final int NON_EMPTY_COUNT = 1000000; 40 | private static final int TIME_SECONDS = 10; 41 | private static final int ITERATIONS = 5; 42 | private static final int WARMUP_ITERATIONS = 5; 43 | 44 | @State(Scope.Thread) 45 | public static class EmptyTree { 46 | 47 | BPlusTree tree; 48 | 49 | @Setup(Level.Trial) 50 | public void doSetup() { 51 | tree = BPlusTree // 52 | .file() // 53 | .directory("target/bench") // 54 | .clearDirectory() // 55 | .deleteOnClose() // 56 | .maxLeafKeys(MAX_KEYS) // 57 | .segmentSizeMB(10) // 58 | .keySerializer(Serializer.INTEGER) // 59 | .valueSerializer(Serializer.INTEGER) // 60 | .naturalOrder(); 61 | } 62 | 63 | @TearDown(Level.Trial) 64 | public void doTearDown() { 65 | try { 66 | tree.close(); 67 | } catch (Exception e) { 68 | throw new RuntimeException(e); 69 | } 70 | } 71 | } 72 | 73 | @State(Scope.Thread) 74 | public static class EmptyTreeMapDb { 75 | 76 | private DB db; 77 | private BTreeMap tree; 78 | 79 | @Setup(Level.Trial) 80 | public void doSetup() { 81 | db = DBMaker // 82 | .fileDB(new File("target/mapdb")) // 83 | .concurrencyDisable() // 84 | .fileDeleteAfterClose() // 85 | .fileMmapEnableIfSupported() // 86 | .make(); 87 | tree = db.treeMap("tree") // 88 | .keySerializer(org.mapdb.Serializer.INTEGER) // 89 | .valueSerializer(org.mapdb.Serializer.INTEGER) // 90 | .createOrOpen(); 91 | } 92 | 93 | @TearDown(Level.Trial) 94 | public void doTearDown() { 95 | try { 96 | tree.close(); 97 | db.close(); 98 | } catch (Exception e) { 99 | throw new RuntimeException(e); 100 | } 101 | } 102 | } 103 | 104 | @State(Scope.Thread) 105 | public static class NonEmptyTree { 106 | 107 | BPlusTree tree; 108 | 109 | @Setup(Level.Trial) 110 | public void doSetup() { 111 | tree = BPlusTree // 112 | .file() // 113 | .directory("target/bench") // 114 | .clearDirectory() // 115 | .deleteOnClose() // 116 | .maxLeafKeys(MAX_KEYS) // 117 | .segmentSizeMB(10) // 118 | .keySerializer(Serializer.INTEGER) // 119 | .valueSerializer(Serializer.INTEGER) // 120 | .naturalOrder(); 121 | for (int i = 0; i < NON_EMPTY_COUNT; i++) { 122 | tree.insert(i, i); 123 | } 124 | } 125 | 126 | @TearDown(Level.Trial) 127 | public void doTearDown() { 128 | try { 129 | tree.close(); 130 | } catch (Exception e) { 131 | throw new RuntimeException(e); 132 | } 133 | } 134 | } 135 | 136 | @State(Scope.Thread) 137 | public static class NonEmptyTreeMapDb { 138 | 139 | private DB db; 140 | private BTreeMap tree; 141 | 142 | @Setup(Level.Trial) 143 | public void doSetup() { 144 | db = DBMaker.fileDB(new File("target/mapdb")) // 145 | .concurrencyDisable() // 146 | .fileDeleteAfterClose() // 147 | .fileMmapEnableIfSupported() // 148 | .make(); 149 | tree = db.treeMap("tree") // 150 | .keySerializer(org.mapdb.Serializer.INTEGER) // 151 | .valueSerializer(org.mapdb.Serializer.INTEGER) // 152 | .createOrOpen(); 153 | for (int i = 0; i < NON_EMPTY_COUNT; i++) { 154 | tree.put(i, i); 155 | } 156 | } 157 | 158 | @TearDown(Level.Trial) 159 | public void doTearDown() { 160 | try { 161 | tree.close(); 162 | db.close(); 163 | } catch (Exception e) { 164 | throw new RuntimeException(e); 165 | } 166 | } 167 | } 168 | 169 | @Benchmark 170 | @BenchmarkMode(Mode.SingleShotTime) 171 | @Warmup(iterations = WARMUP_ITERATIONS, time = TIME_SECONDS) 172 | @Measurement(iterations = ITERATIONS, time = TIME_SECONDS) 173 | public void storeManyIntsBPlusTree(EmptyTree state) throws Exception { 174 | for (int i = 0; i < MANY; i++) { 175 | state.tree.insert(i, i); 176 | } 177 | } 178 | 179 | @Benchmark 180 | @BenchmarkMode(Mode.SingleShotTime) 181 | @Warmup(iterations = WARMUP_ITERATIONS, time = TIME_SECONDS) 182 | @Measurement(iterations = ITERATIONS, time = TIME_SECONDS) 183 | public void storeManyIntsMapDb(EmptyTreeMapDb state) { 184 | for (int i = 0; i < MANY; i++) { 185 | state.tree.put(i, i); 186 | } 187 | } 188 | 189 | @Benchmark 190 | @BenchmarkMode(Mode.Throughput) 191 | @Warmup(iterations = WARMUP_ITERATIONS, time = TIME_SECONDS) 192 | @Measurement(iterations = ITERATIONS, time = TIME_SECONDS) 193 | public long rangeSearchManyIntsBPlusTree(NonEmptyTree state) { 194 | return count(state.tree.find(100000, 100000, true).iterator()); 195 | } 196 | 197 | @Benchmark 198 | @BenchmarkMode(Mode.Throughput) 199 | @Warmup(iterations = WARMUP_ITERATIONS, time = TIME_SECONDS) 200 | @Measurement(iterations = ITERATIONS, time = TIME_SECONDS) 201 | public long rangeSearchManyIntsMapDb(NonEmptyTreeMapDb state) { 202 | return count(state.tree.valueIterator(100000, true, 100000, true)); 203 | } 204 | 205 | private static long count(Iterator it) { 206 | long count = 0; 207 | while (it.hasNext()) { 208 | it.next(); 209 | count++; 210 | } 211 | return count; 212 | } 213 | 214 | // @Benchmark 215 | public long getLong() { 216 | a.position(0); 217 | return a.getLong(); 218 | } 219 | 220 | // @Benchmark 221 | public long getVarlong() { 222 | b.position(0); 223 | return b.getVarlong(); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/FactoryFileTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | import com.github.davidmoten.bplustree.internal.Factory; 8 | import com.github.davidmoten.bplustree.internal.file.LeafFile; 9 | import com.github.davidmoten.bplustree.internal.file.NonLeafFile; 10 | 11 | public class FactoryFileTest { 12 | 13 | @Test 14 | public void testLeafFile() throws Exception { 15 | try (BPlusTree t = create()) { 16 | Factory factory = t.factory(); 17 | LeafFile leaf = (LeafFile) factory.createLeaf(); 18 | LeafFile leaf2 = (LeafFile) factory.createLeaf(); 19 | leaf.setNumKeys(2); 20 | assertEquals(2, leaf.numKeys()); 21 | leaf.setNumKeys(0); 22 | assertEquals(0, leaf.numKeys()); 23 | leaf.setNext(leaf2); 24 | assertEquals(leaf2.position(), leaf.next().position()); 25 | leaf.insert(3, Long.MAX_VALUE); 26 | assertEquals(1, leaf.numKeys()); 27 | assertEquals(3, (int) leaf.key(0)); 28 | leaf.setValue(0, 1234567890L); 29 | assertEquals(1234567890L, (long) leaf.value(0)); 30 | } 31 | } 32 | 33 | @Test 34 | public void testNonLeafFile() throws Exception { 35 | try (BPlusTree t = create()) { 36 | Factory factory = t.factory(); 37 | NonLeafFile n = (NonLeafFile) factory.createNonLeaf(); 38 | n.setNumKeys(2); 39 | assertEquals(2, n.numKeys()); 40 | n.setNumKeys(0); 41 | assertEquals(0, n.numKeys()); 42 | } 43 | } 44 | 45 | private BPlusTree create() { 46 | return BPlusTree // 47 | .file() // 48 | .directory("target/facfile") // 49 | .clearDirectory() // 50 | .maxKeys(3) // 51 | .keySerializer(Serializer.INTEGER) // 52 | .valueSerializer(Serializer.LONG) // 53 | .naturalOrder(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/LargeByteBufferDelegating.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class LargeByteBufferDelegating implements LargeByteBuffer { 6 | 7 | private final ByteBuffer bb = ByteBuffer.allocate(1024); 8 | 9 | 10 | @Override 11 | public long position() { 12 | return bb.position(); 13 | } 14 | 15 | @Override 16 | public void position(long newPosition) { 17 | bb.position((int) newPosition); 18 | } 19 | 20 | @Override 21 | public byte get() { 22 | return bb.get(); 23 | } 24 | 25 | @Override 26 | public void put(byte b) { 27 | bb.put(b); 28 | } 29 | 30 | @Override 31 | public void get(byte[] dst) { 32 | bb.get(dst); 33 | } 34 | 35 | @Override 36 | public void put(byte[] src) { 37 | bb.put(src); 38 | } 39 | 40 | @Override 41 | public int getInt() { 42 | return bb.getInt(); 43 | } 44 | 45 | @Override 46 | public void putInt(int value) { 47 | bb.putInt(value); 48 | } 49 | 50 | @Override 51 | public short getShort() { 52 | return bb.getShort(); 53 | } 54 | 55 | @Override 56 | public void putShort(short value) { 57 | bb.putShort(value); 58 | } 59 | 60 | @Override 61 | public long getLong() { 62 | return bb.getLong(); 63 | } 64 | 65 | @Override 66 | public void putLong(long value) { 67 | bb.putLong(value); 68 | } 69 | 70 | @Override 71 | public double getDouble() { 72 | return bb.getDouble(); 73 | } 74 | 75 | @Override 76 | public void putDouble(double value) { 77 | bb.putDouble(value); 78 | } 79 | 80 | @Override 81 | public double getFloat() { 82 | return bb.getFloat(); 83 | } 84 | 85 | @Override 86 | public void putFloat(float value) { 87 | bb.putFloat(value); 88 | } 89 | 90 | @Override 91 | public void commit() { 92 | // do nothing 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/NodeWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.github.davidmoten.bplustree.internal.Node; 7 | import com.github.davidmoten.bplustree.internal.NonLeaf; 8 | 9 | public final class NodeWrapper { 10 | 11 | private final Node node; 12 | 13 | public NodeWrapper(Node node) { 14 | this.node = node; 15 | } 16 | 17 | public static NodeWrapper root(BPlusTree tree) { 18 | return new NodeWrapper(tree.root()); 19 | } 20 | 21 | public List keys() { 22 | return node.keys(); 23 | } 24 | 25 | public List> children() { 26 | NonLeaf nd = (NonLeaf) node; 27 | List> list = new ArrayList<>(); 28 | for (int i = 0; i < nd.numKeys() + 1; i++) { 29 | if (nd.child(i) != null) { 30 | list.add(new NodeWrapper(nd.child(i))); 31 | } 32 | } 33 | return list; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/SerializerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | import org.junit.Test; 9 | 10 | import com.github.davidmoten.bplustree.internal.LargeMappedByteBuffer; 11 | 12 | public class SerializerTest { 13 | 14 | @Test 15 | public void testLong() throws IOException { 16 | assertEquals(8, Serializer.LONG.maxSize()); 17 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 18 | "test-")) { 19 | b.position(0); 20 | Serializer.LONG.write(b, Long.MAX_VALUE); 21 | b.position(0); 22 | assertEquals(Long.MAX_VALUE, (long) Serializer.LONG.read(b)); 23 | } 24 | } 25 | 26 | @Test 27 | public void testShort() throws IOException { 28 | assertEquals(2, Serializer.SHORT.maxSize()); 29 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 30 | "test-")) { 31 | b.position(0); 32 | Serializer.SHORT.write(b, Short.MAX_VALUE); 33 | b.position(0); 34 | assertEquals(Short.MAX_VALUE, (long) Serializer.SHORT.read(b)); 35 | } 36 | } 37 | 38 | @Test 39 | public void testInteger() throws IOException { 40 | assertEquals(4, Serializer.INTEGER.maxSize()); 41 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 42 | "test-")) { 43 | b.position(0); 44 | Serializer.INTEGER.write(b, Integer.MAX_VALUE); 45 | b.position(0); 46 | assertEquals(Integer.MAX_VALUE, (long) Serializer.INTEGER.read(b)); 47 | } 48 | } 49 | 50 | @Test 51 | public void testUtf8() throws IOException { 52 | Serializer ser = Serializer.utf8(16); 53 | assertEquals(16, ser.maxSize()); 54 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 55 | "test-")) { 56 | b.position(0); 57 | ser.write(b, "hello"); 58 | b.position(0); 59 | assertEquals("hello", ser.read(b)); 60 | } 61 | } 62 | 63 | @Test 64 | public void testUtf8NoMaxSize() throws IOException { 65 | Serializer ser = Serializer.utf8(); 66 | assertEquals(0, ser.maxSize()); 67 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 68 | "test-")) { 69 | b.position(0); 70 | ser.write(b, "hello"); 71 | b.position(0); 72 | assertEquals("hello", ser.read(b)); 73 | } 74 | } 75 | 76 | @Test 77 | public void testBytes() throws IOException { 78 | Serializer ser = Serializer.bytes(16); 79 | assertEquals(16, ser.maxSize()); 80 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 81 | "test-")) { 82 | b.position(0); 83 | ser.write(b, "hello".getBytes(StandardCharsets.UTF_8)); 84 | b.position(0); 85 | assertEquals("hello", new String(ser.read(b), StandardCharsets.UTF_8)); 86 | } 87 | } 88 | 89 | @Test 90 | public void testFloat() throws IOException { 91 | Serializer ser = Serializer.FLOAT; 92 | assertEquals(Float.BYTES, ser.maxSize()); 93 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 94 | "test-")) { 95 | b.position(0); 96 | ser.write(b, 123.4f); 97 | b.position(0); 98 | assertEquals(123.4f, ser.read(b), 0.0001); 99 | } 100 | } 101 | 102 | @Test 103 | public void testDouble() throws IOException { 104 | Serializer ser = Serializer.DOUBLE; 105 | assertEquals(Double.BYTES, ser.maxSize()); 106 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, 107 | "test-")) { 108 | b.position(0); 109 | ser.write(b, 123.4); 110 | b.position(0); 111 | assertEquals(123.4, ser.read(b), 0.0001); 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/Testing.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree; 2 | 3 | import java.io.File; 4 | import java.util.UUID; 5 | 6 | public class Testing { 7 | 8 | public static File newDirectory() { 9 | String directoryName = "target/" + UUID.randomUUID().toString().substring(0, 6); 10 | File file = new File(directoryName); 11 | file.mkdirs(); 12 | return file; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/internal/LargeMappedByteBufferTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import static org.junit.Assert.assertArrayEquals; 4 | import static org.junit.Assert.assertEquals; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | 9 | import org.junit.Test; 10 | 11 | import com.github.davidmoten.bplustree.Testing; 12 | 13 | public class LargeMappedByteBufferTest { 14 | 15 | @Test 16 | public void testWriteAndReadIntValuesAcrossSegments() throws IOException { 17 | for (int size = 1; size <= 3 * Integer.BYTES + 1; size++) { 18 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), size, "index-")) { 19 | b.putInt(10); 20 | assertEquals(4, b.position()); 21 | b.putInt(11); 22 | assertEquals(8, b.position()); 23 | b.putInt(12); 24 | assertEquals(12, b.position()); 25 | // now read what we've just written 26 | b.position(0); 27 | assertEquals(10, b.getInt()); 28 | assertEquals(11, b.getInt()); 29 | assertEquals(12, b.getInt()); 30 | } 31 | } 32 | } 33 | 34 | @Test 35 | public void testWriteAndReadBytes() throws IOException { 36 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 2, "index-")) { 37 | b.put((byte) 1); 38 | b.put((byte) 2); 39 | b.put((byte) 3); 40 | b.position(0); 41 | assertEquals(1, b.get()); 42 | assertEquals(2, b.get()); 43 | assertEquals(3, b.get()); 44 | } 45 | } 46 | 47 | @Test 48 | public void testReadAndWriteVarints() throws IOException { 49 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 2, "index-")) { 50 | b.putVarint(1234567); 51 | b.putVarint(Integer.MIN_VALUE); 52 | b.putVarint(Integer.MAX_VALUE); 53 | b.putVarint(-56); 54 | for (int i = 0; i < 10000; i++) { 55 | b.putVarint(i); 56 | } 57 | for (int i = 1; i <= 16; i++) { 58 | b.putVarint(123 << i); 59 | } 60 | b.position(0); 61 | assertEquals(1234567, b.getVarint()); 62 | assertEquals(Integer.MIN_VALUE, b.getVarint()); 63 | assertEquals(Integer.MAX_VALUE, b.getVarint()); 64 | assertEquals(-56, b.getVarint()); 65 | for (int i = 0; i < 10000; i++) { 66 | assertEquals(i, b.getVarint()); 67 | } 68 | for (int i = 1; i <= 16; i++) { 69 | assertEquals(123 << i, b.getVarint()); 70 | } 71 | } 72 | } 73 | 74 | @Test 75 | public void testReadAndWriteVarlongs() throws IOException { 76 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 2, "index-")) { 77 | b.putVarlong(1234567890123L); 78 | b.putVarlong(-56); 79 | long maxLong = Long.MAX_VALUE; 80 | b.putVarlong(maxLong); 81 | for (int i = 0; i < 10000; i++) { 82 | b.putVarlong(i * 123); 83 | } 84 | for (int i = 0; i <= 62; i++) { 85 | b.putVarlong(maxLong >> i); 86 | } 87 | b.position(0); 88 | assertEquals(1234567890123L, b.getVarlong()); 89 | assertEquals(-56, b.getVarlong()); 90 | assertEquals(maxLong, b.getVarlong()); 91 | for (int i = 0; i < 10000; i++) { 92 | assertEquals(i * 123, b.getVarlong()); 93 | } 94 | for (int i = 0; i <= 62; i++) { 95 | assertEquals(maxLong >> i, b.getVarlong()); 96 | } 97 | } 98 | } 99 | 100 | @Test(expected = IllegalStateException.class) 101 | public void testReadAndWriteIllegalVarlong() throws IOException { 102 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 2, "index-")) { 103 | b.put((byte) -1); 104 | b.put((byte) -1); 105 | b.put((byte) -1); 106 | b.put((byte) -1); 107 | b.put((byte) -1); 108 | b.put((byte) -1); 109 | b.put((byte) -1); 110 | b.put((byte) -1); 111 | b.put((byte) -1); 112 | b.put((byte) -1); 113 | b.position(0); 114 | b.getVarlong(); 115 | } 116 | } 117 | 118 | @Test 119 | public void testWriteAndReadArrayWithinSegment() throws IOException { 120 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), 100, "index-")) { 121 | byte[] bytes = new byte[] { 1, 2, 3, 4, 5, 6 }; 122 | b.put(bytes); 123 | b.put(bytes); 124 | 125 | // read 126 | b.position(0); 127 | byte[] a = new byte[bytes.length]; 128 | b.get(a); 129 | assertArrayEquals(bytes, a); 130 | a = new byte[bytes.length]; 131 | b.get(a); 132 | assertArrayEquals(bytes, a); 133 | } 134 | } 135 | 136 | @Test 137 | public void testWriteAndReadShortValuesAcrossSegments() throws IOException { 138 | for (int size = 1; size <= 3 * Short.BYTES + 1; size++) { 139 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), size, "index-")) { 140 | b.putShort((short) 10); 141 | b.putShort((short) 11); 142 | b.putShort((short) 12); 143 | // now read what we've just written 144 | b.position(0); 145 | assertEquals(10, b.getShort()); 146 | assertEquals(11, b.getShort()); 147 | assertEquals(12, b.getShort()); 148 | } 149 | } 150 | } 151 | 152 | @Test 153 | public void testWriteAndReadLongValuesAcrossSegments() throws IOException { 154 | for (int size = 1; size <= 3 * Long.BYTES + 1; size++) { 155 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), size, "index-")) { 156 | b.putLong(10); 157 | b.putLong(11); 158 | b.putLong(12); 159 | // now read what we've just written 160 | b.position(0); 161 | assertEquals(10, b.getLong()); 162 | assertEquals(11, b.getLong()); 163 | assertEquals(12, b.getLong()); 164 | } 165 | } 166 | } 167 | 168 | @Test 169 | public void testWriteAndReadDoubleValuesAcrossSegments() throws IOException { 170 | for (int size = 1; size <= 3 * Double.BYTES + 1; size++) { 171 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), size, "index-")) { 172 | b.putDouble(10.01); 173 | b.putDouble(11.01); 174 | b.putDouble(12.01); 175 | // now read what we've just written 176 | b.position(0); 177 | assertEquals(10.01, b.getDouble(), 0.00001); 178 | assertEquals(11.01, b.getDouble(), 0.00001); 179 | assertEquals(12.01, b.getDouble(), 0.00001); 180 | } 181 | } 182 | } 183 | 184 | @Test 185 | public void testWriteAndReadFloatValuesAcrossSegments() throws IOException { 186 | for (int size = 1; size <= 3 * Float.BYTES + 1; size++) { 187 | try (LargeMappedByteBuffer b = new LargeMappedByteBuffer(Testing.newDirectory(), size, "index-")) { 188 | b.putFloat(10.01f); 189 | b.putFloat(11.01f); 190 | b.putFloat(12.01f); 191 | // now read what we've just written 192 | b.position(0); 193 | assertEquals(10.01f, b.getFloat(), 0.00001); 194 | assertEquals(11.01f, b.getFloat(), 0.00001); 195 | assertEquals(12.01f, b.getFloat(), 0.00001); 196 | } 197 | } 198 | } 199 | 200 | @Test 201 | public void testCheckFileWhenFileDoesNotExistDoesNotThrow() throws IOException { 202 | File file = new File("target/doesNotExist"); 203 | LargeMappedByteBuffer.checkFile(file, 20); 204 | } 205 | 206 | @Test(expected = IllegalStateException.class) 207 | public void testCheckFileWhenFileExistsAndIsNotOfExpectedSize() throws IOException { 208 | File file = new File("target/zeroLengthFile"); 209 | file.createNewFile(); 210 | LargeMappedByteBuffer.checkFile(file, 20); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/test/java/com/github/davidmoten/bplustree/internal/NonLeafTest.java: -------------------------------------------------------------------------------- 1 | package com.github.davidmoten.bplustree.internal; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.Comparator; 6 | 7 | import org.junit.Test; 8 | 9 | public class NonLeafTest { 10 | 11 | @Test 12 | public void testGetLocationUniqueKeys() { 13 | Node n = create(1, 3, 5); 14 | assertEquals(0, getLocation(n, 0)); 15 | assertEquals(1, getLocation(n, 1)); 16 | assertEquals(1, getLocation(n, 2)); 17 | assertEquals(2, getLocation(n, 3)); 18 | assertEquals(2, getLocation(n, 4)); 19 | assertEquals(3, getLocation(n, 5)); 20 | assertEquals(3, getLocation(n, 6)); 21 | } 22 | 23 | private static int getLocation(Node n, int key) { 24 | return Util.getLocation(n, key, Comparator.naturalOrder(), false); 25 | } 26 | 27 | @Test 28 | public void testGetLocationNonUniqueKeys() { 29 | Node n = create(1, 1, 3, 3); 30 | assertEquals(0, getLocation(n, 0)); 31 | assertEquals(2, getLocation(n, 1)); 32 | assertEquals(2, getLocation(n, 2)); 33 | assertEquals(4, getLocation(n, 3)); 34 | assertEquals(4, getLocation(n, 4)); 35 | } 36 | 37 | private Node create(int... keys) { 38 | return new Node() { 39 | 40 | @Override 41 | public Options options() { 42 | return null; 43 | } 44 | 45 | @Override 46 | public Factory factory() { 47 | return null; 48 | } 49 | 50 | @Override 51 | public int numKeys() { 52 | return keys.length; 53 | } 54 | 55 | @Override 56 | public Integer key(int i) { 57 | return keys[i]; 58 | } 59 | 60 | @Override 61 | public int getLocation(Integer key) { 62 | return 0; 63 | } 64 | 65 | @Override 66 | public Split insert(Integer key, Integer value) { 67 | return null; 68 | } 69 | 70 | }; 71 | 72 | } 73 | 74 | } 75 | --------------------------------------------------------------------------------