diff --git a/README.md b/README.md index 0346f6a..a824d96 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,4 @@ This project is implemented to practice a B+Tree implementation to index data on - Prevent "too many open files" issue: since index chunks can grow, its safer to create a better pool for `SynchronisedFileChannel` used -currently- by `IndexFileManager` - Searching for keys, adding keys, or key values, are all done linearly. Alternatively, we could add/modify using binary search (works better in case of large key sizes) or hold a metadata in node with sorts -- Allocation may require a flag to set that part of storage as "reserved", so write and overwrite can be different. But would this be enough to prevent writing to a location a requester (a part of code that needed allocation) has allocated itself? Or, maybe this is completely wrong. If we make addIndex() sync, and only one thread can allocate space per table, there won't be an issue? Could still be wrong since some other table may have allocated space in same chunk, and that can ruin things (race condition) -- After allocation, table chunk offsets should be updated: any table after the table we allocated for needs to change (increase) their offset position. This is the only possible way to not break all nodes child pointers. +- Allocation may require a flag to set that part of storage as "reserved", so write and overwrite can be different. But would this be enough to prevent writing to a location a requester (a part of code that needed allocation) has allocated itself? Or, maybe this is completely wrong. If we make addIndex() sync, and only one thread can allocate space per table, there won't be an issue? Could still be wrong since some other table may have allocated space in same chunk, and that can ruin things (race condition) | **update**: this may be wrong as BTree operations on a single db should be sync diff --git a/src/main/java/com/github/sepgh/internal/storage/FileIndexStorageManager.java b/src/main/java/com/github/sepgh/internal/storage/FileIndexStorageManager.java index bed9b14..4f2d954 100644 --- a/src/main/java/com/github/sepgh/internal/storage/FileIndexStorageManager.java +++ b/src/main/java/com/github/sepgh/internal/storage/FileIndexStorageManager.java @@ -17,7 +17,6 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import static com.github.sepgh.internal.tree.node.BaseTreeNode.TYPE_INTERNAL_NODE_BIT; import static com.github.sepgh.internal.tree.node.BaseTreeNode.TYPE_LEAF_NODE_BIT; @@ -79,6 +78,38 @@ private AsynchronousFileChannel getAsynchronousFileChannel(int chunk) { return channel; } + @Override + public CompletableFuture fillRoot(int table, byte[] data){ + CompletableFuture output = new CompletableFuture<>(); + + Optional optionalTable = headerManager.getHeader().getTableOfId(table); + + Header.Table headerTable = optionalTable.get(); + if (optionalTable.isEmpty() || headerTable.getRoot() == null){ + output.completeExceptionally(new Exception("Root position is undetermined")); // Todo + return output; + } + + FileUtils.write(getAsynchronousFileChannel(optionalTable.get().getRoot().getChunk()), optionalTable.get().getRoot().getOffset(), data).whenComplete((size, throwable) -> { + if (throwable != null){ + output.completeExceptionally(throwable); + } + + output.complete( + new NodeData( + new Pointer( + Pointer.TYPE_NODE, + optionalTable.get().getRoot().getOffset(), + optionalTable.get().getRoot().getChunk() + ), + data + ) + ); + }); + + return output; + } + @Override public CompletableFuture> getRoot(int table) { CompletableFuture> output = new CompletableFuture<>(); @@ -101,6 +132,11 @@ public CompletableFuture> getRoot(int table) { return; } + if (bytes.length == 0 || bytes[0] == (byte) 0x00){ + output.complete(Optional.empty()); + return; + } + output.complete( Optional.of( new NodeData(new Pointer(Pointer.TYPE_NODE, root.getOffset(), root.getChunk()), bytes) @@ -151,7 +187,7 @@ public CompletableFuture writeNewNode(int table, byte[] data, boolean byte[] finalData1 = data; long offset = pointer.getPosition(); - // setting pointer position according to the offset. Reading table again since a new chunk may have been created + // setting pointer position according to the table offset. Reading table again since a new chunk may have been created pointer.setPosition(offset - headerManager.getHeader().getTableOfId(table).get().getIndexChunk(pointer.getChunk()).get().getOffset()); FileUtils.write(getAsynchronousFileChannel(pointer.getChunk()), offset, data).whenComplete((size, throwable) -> { if (throwable != null){ @@ -169,68 +205,110 @@ public CompletableFuture writeNewNode(int table, byte[] data, boolean return output; } - // Todo: as currently written in README, after allocating space, the chunk offset of tables after the tableId should be updated + private List getTablesIncludingChunk(int chunk){ + return headerManager.getHeader().getTables().stream().filter(table -> table.getIndexChunk(chunk).isPresent()).toList(); + } + + private int getIndexOfTable(List tables, int table){ + int index = -1; + for (int i = 0; i < tables.size(); i++) + if (tables.get(i).getId() == table){ + index = i; + break; + } + + return index; + } + + /** + * ## How it works: + * if the chunk is new for this table, then just allocate at the end of the file and add the chunk index to header + * and return. But if there isn't space left, try next chunk. + * if file size is not 0, + * see if there is any empty space in the file (allocated before but never written to) and return the + * pointer to that space if is available. + * if file size is equal or greater than maximum file size try next chunk + * allocate space at end of the file and return pointer if the table is at end of the file + * otherwise, allocate space right before the next table in this chunk begins and push next tables to the end + * also make sure to update possible roots and chunk indexes offset for next tables + * @param tableId table to allocate space in + * @param chunk chunk to allocate space in + * @return Pointer to the beginning of allocated location + */ private Pointer getAllocatedSpaceForNewNode(int tableId, int chunk) throws IOException, ExecutionException, InterruptedException { Header.Table table = headerManager.getHeader().getTableOfId(tableId).get(); Optional optional = table.getIndexChunk(chunk); - boolean newChunkCreated = optional.isEmpty(); + boolean newChunkCreatedForTable = optional.isEmpty(); AsynchronousFileChannel asynchronousFileChannel = this.getAsynchronousFileChannel(chunk); - int indexOfTableMetaData = headerManager.getHeader().indexOfTable(tableId); - - boolean isLastTable = indexOfTableMetaData == headerManager.getHeader().tablesCount() - 1; long fileSize = asynchronousFileChannel.size(); - long position = 0; - if (fileSize != 0){ - position = isLastTable ? - fileSize - engineConfig.indexGrowthAllocationSize() - : - headerManager.getHeader().getTableOfIndex(indexOfTableMetaData + 1).get().getIndexChunk(chunk).get().getOffset() - engineConfig.indexGrowthAllocationSize(); - - Future future = FileUtils.readBytes(asynchronousFileChannel, position, engineConfig.indexGrowthAllocationSize()); - byte[] bytes = new byte[0]; - try { - bytes = future.get(); - } catch (InterruptedException | ExecutionException e) { - throw new IOException(e); - } - Optional optionalAdditionalPosition = getPossibleAllocationLocation(bytes); - if (optionalAdditionalPosition.isPresent()){ - long finalPosition = position + optionalAdditionalPosition.get(); - return new Pointer(Pointer.TYPE_NODE, finalPosition, chunk); - } - - - /* - If there isn't an empty allocated location, we check if maximum size is reached. - If it is, we won't be allocating and just move on to next chunk - through recursion till we reach to a chunk where we can allocate space - */ + if (newChunkCreatedForTable){ if (fileSize >= engineConfig.getBTreeMaxFileSize()){ return getAllocatedSpaceForNewNode(tableId, chunk + 1); + } else { + Long position = FileUtils.allocate(asynchronousFileChannel, engineConfig.indexGrowthAllocationSize()).get(); + List newChunks = new ArrayList<>(table.getChunks()); + newChunks.add(new Header.IndexChunk(chunk, position)); + table.setChunks(newChunks); + headerManager.update(); + return new Pointer(Pointer.TYPE_NODE, position, chunk); } + } + List tablesIncludingChunk = getTablesIncludingChunk(chunk); + int indexOfTable = getIndexOfTable(tablesIncludingChunk, tableId); + boolean isLastTable = indexOfTable == tablesIncludingChunk.size() - 1; - } - Long finalPosition; - if (isLastTable || position == 0){ - finalPosition = FileUtils.allocate(asynchronousFileChannel, engineConfig.indexGrowthAllocationSize()).get(); - }else { - finalPosition = FileUtils.allocate(asynchronousFileChannel, position, engineConfig.indexGrowthAllocationSize()).get(); - } + if (fileSize > 0){ + long positionToCheck = + isLastTable ? + fileSize - engineConfig.indexGrowthAllocationSize() + : + tablesIncludingChunk.get(indexOfTable + 1).getIndexChunk(chunk).get().getOffset() - engineConfig.indexGrowthAllocationSize(); - if (newChunkCreated){ - List newChunks = new ArrayList<>(table.getChunks()); - newChunks.add(new Header.IndexChunk(chunk, finalPosition)); - table.setChunks(newChunks); - headerManager.update(); + if (positionToCheck > 0) { + byte[] bytes = FileUtils.readBytes(asynchronousFileChannel, positionToCheck, engineConfig.indexGrowthAllocationSize()).get(); + Optional optionalAdditionalPosition = getPossibleAllocationLocation(bytes); + if (optionalAdditionalPosition.isPresent()){ + long finalPosition = positionToCheck + optionalAdditionalPosition.get(); + return new Pointer(Pointer.TYPE_NODE, finalPosition, chunk); + } + } } - return new Pointer(Pointer.TYPE_NODE, finalPosition, chunk); + if (fileSize >= engineConfig.getBTreeMaxFileSize()) + return this.getAllocatedSpaceForNewNode(tableId, chunk + 1); + + + long allocatedOffset; + if (isLastTable){ + allocatedOffset = FileUtils.allocate(asynchronousFileChannel, engineConfig.indexGrowthAllocationSize()).get(); + } else { + allocatedOffset = FileUtils.allocate( + asynchronousFileChannel, + tablesIncludingChunk.get(indexOfTable + 1).getIndexChunk(chunk).get().getOffset(), + engineConfig.indexGrowthAllocationSize() + ).get(); + + for (int i = indexOfTable + 1; i < tablesIncludingChunk.size(); i++){ + Header.Table nextTable = tablesIncludingChunk.get(i); + if (nextTable.getRoot().getChunk() == chunk) { + nextTable.getRoot().setOffset( + nextTable.getRoot().getOffset() + engineConfig.indexGrowthAllocationSize() + ); + } + Header.IndexChunk indexChunk = nextTable.getIndexChunk(chunk).get(); + indexChunk.setOffset(indexChunk.getOffset() + engineConfig.indexGrowthAllocationSize()); + } + } + return new Pointer(Pointer.TYPE_NODE, allocatedOffset, chunk); } + /* + * Returns the empty position within byte[] passed to the method + */ private Optional getPossibleAllocationLocation(byte[] bytes){ for (int i = 0; i < engineConfig.getBTreeGrowthNodeAllocationCount(); i++){ int position = i * engineConfig.getPaddedSize(); diff --git a/src/main/java/com/github/sepgh/internal/storage/IndexStorageManager.java b/src/main/java/com/github/sepgh/internal/storage/IndexStorageManager.java index e383c09..ca23465 100644 --- a/src/main/java/com/github/sepgh/internal/storage/IndexStorageManager.java +++ b/src/main/java/com/github/sepgh/internal/storage/IndexStorageManager.java @@ -8,6 +8,8 @@ import java.util.concurrent.ExecutionException; public interface IndexStorageManager { + CompletableFuture fillRoot(int table, byte[] data); + CompletableFuture> getRoot(int table); byte[] getEmptyNode(); diff --git a/src/main/java/com/github/sepgh/internal/tree/BTreeIndexManager.java b/src/main/java/com/github/sepgh/internal/tree/BTreeIndexManager.java index c2ec9b8..98b3e6b 100644 --- a/src/main/java/com/github/sepgh/internal/tree/BTreeIndexManager.java +++ b/src/main/java/com/github/sepgh/internal/tree/BTreeIndexManager.java @@ -33,12 +33,7 @@ private BaseTreeNode getRoot(int table) throws ExecutionException, InterruptedEx LeafTreeNode leafTreeNode = (LeafTreeNode) BaseTreeNode.fromBytes(emptyNode, BaseTreeNode.NodeType.LEAF); leafTreeNode.setAsRoot(); - IndexStorageManager.NodeData nodeData = indexStorageManager.writeNewNode( - table, - leafTreeNode.getData(), - true - ).get(); - + IndexStorageManager.NodeData nodeData = indexStorageManager.fillRoot(table, leafTreeNode.getData()).get(); leafTreeNode.setNodePointer(nodeData.pointer()); return leafTreeNode; } diff --git a/src/test/java/com/github/sepgh/internal/tree/BTreeIndexManagerTestCase.java b/src/test/java/com/github/sepgh/internal/tree/BTreeIndexManagerTestCase.java index 4da772c..1681074 100644 --- a/src/test/java/com/github/sepgh/internal/tree/BTreeIndexManagerTestCase.java +++ b/src/test/java/com/github/sepgh/internal/tree/BTreeIndexManagerTestCase.java @@ -9,10 +9,7 @@ import com.github.sepgh.internal.tree.node.BaseTreeNode; import com.github.sepgh.internal.tree.node.InternalTreeNode; import com.github.sepgh.internal.tree.node.LeafTreeNode; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import java.io.IOException; import java.nio.file.Files; @@ -58,6 +55,12 @@ public void setUp() throws IOException { .build() ) ) + .root( + Header.IndexChunk.builder() + .chunk(0) + .offset(0) + .build() + ) .initialized(true) .build() ) @@ -80,6 +83,7 @@ public void destroy() throws IOException { @Test + @Timeout(value = 2) public void addIndex() throws IOException, ExecutionException, InterruptedException { HeaderManager headerManager = new InMemoryHeaderManager(header); FileIndexStorageManager fileIndexStorageManager = new FileIndexStorageManager(dbPath, headerManager, engineConfig); @@ -102,6 +106,7 @@ public void addIndex() throws IOException, ExecutionException, InterruptedExcept } @Test + @Timeout(value = 2) public void testSingleSplitAddIndex() throws IOException, ExecutionException, InterruptedException { Random random = new Random(); @@ -188,6 +193,7 @@ public void testSingleSplitAddIndex() throws IOException, ExecutionException, In * └── 012 */ @Test + @Timeout(value = 2) public void testMultiSplitAddIndex() throws IOException, ExecutionException, InterruptedException { List testIdentifiers = Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L); diff --git a/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerAllocationTestCase.java b/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerAllocationTestCase.java new file mode 100644 index 0000000..7c5348f --- /dev/null +++ b/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerAllocationTestCase.java @@ -0,0 +1,453 @@ +package com.github.sepgh.internal.tree; + +import com.github.sepgh.internal.EngineConfig; +import com.github.sepgh.internal.storage.FileIndexStorageManager; +import com.github.sepgh.internal.storage.InMemoryHeaderManager; +import com.github.sepgh.internal.storage.IndexStorageManager; +import com.github.sepgh.internal.storage.header.Header; +import com.github.sepgh.internal.storage.header.HeaderManager; +import com.github.sepgh.internal.tree.node.BaseTreeNode; +import com.github.sepgh.internal.tree.node.InternalTreeNode; +import com.github.sepgh.internal.tree.node.LeafTreeNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static com.github.sepgh.internal.storage.FileIndexStorageManager.INDEX_FILE_NAME; + +/* +* The purpose of this test case is to assure allocation wouldn't cause issue in multi-table environment +*/ +public class MultiTableBTreeIndexManagerAllocationTestCase { + private Path dbPath; + private EngineConfig engineConfig; + private Header header; + private int order = 3; + + @BeforeEach + public void setUp() throws IOException { + dbPath = Files.createTempDirectory("TEST_MultiTableBTreeIndexManagerAllocationTestCase"); + engineConfig = EngineConfig.builder() + .bTreeNodeMaxKey(order) + .bTreeGrowthNodeAllocationCount(2) + .build(); + engineConfig.setBTreeMaxFileSize(2 * 15L * engineConfig.getPaddedSize()); + + byte[] writingBytes = new byte[10 * engineConfig.getPaddedSize()]; + Path indexPath = Path.of(dbPath.toString(), String.format("%s.%d", INDEX_FILE_NAME, 0)); + Files.write(indexPath, writingBytes, StandardOpenOption.WRITE, StandardOpenOption.CREATE); + + header = Header.builder() + .database("sample") + .tables( + Arrays.asList( + Header.Table.builder() + .id(1) + .name("test") + .chunks( + Collections.singletonList( + Header.IndexChunk.builder() + .chunk(0) + .offset(0) + .build() + ) + ) + .root( + Header.IndexChunk.builder() + .chunk(0) + .offset(0) + .build() + ) + .initialized(true) + .build(), + Header.Table.builder() + .id(2) + .name("test2") + .chunks( + Collections.singletonList( + Header.IndexChunk.builder() + .chunk(0) + .offset(3L * engineConfig.getPaddedSize()) + .build() + ) + ) + .root( + Header.IndexChunk.builder() + .chunk(0) + .offset(3L * engineConfig.getPaddedSize()) + .build() + ) + .initialized(true) + .build() + ) + ) + .build(); + + Assertions.assertTrue(header.getTableOfId(1).isPresent()); + Assertions.assertTrue(header.getTableOfId(1).get().getIndexChunk(0).isPresent()); + Assertions.assertTrue(header.getTableOfId(2).isPresent()); + Assertions.assertTrue(header.getTableOfId(2).get().getIndexChunk(0).isPresent()); + } + + @AfterEach + public void destroy() throws IOException { + Path indexPath0 = Path.of(dbPath.toString(), String.format("%s.%d", INDEX_FILE_NAME, 0)); + Files.delete(indexPath0); + try { + Path indexPath1 = Path.of(dbPath.toString(), String.format("%s.%d", INDEX_FILE_NAME, 0)); + Files.delete(indexPath1); + } catch (NoSuchFileException ignored){} + } + + + /** + * + * The B+Tree in this test will include numbers from [1-12] added respectively + * The shape of the tree will be like below, and the test verifies that + * The test validates the tree for 2 tables in same database + * 007 + * ├── . + * │ ├── 001 [LEAF NODE 1] + * │ └── 002 [LEAF NODE 1] + * ├── 003 + * │ ├── 003 [LEAF NODE 2] + * │ └── 004 [LEAF NODE 2] + * ├── 005 + * │ ├── 005 [LEAF NODE 3] + * │ └── 006 [LEAF NODE 3] + * ├── . + * │ ├── 007 [LEAF NODE 4] + * │ └── 008 [LEAF NODE 4] + * ├── 009 + * │ ├── 009 [LEAF NODE 5] + * │ └── 010 [LEAF NODE 5] + * └── 0011 + * ├── 011 [LEAF NODE 6] + * └── 012 [LEAF NODE 6] + */ + @Test + public void testMultiSplitAddIndex() throws IOException, ExecutionException, InterruptedException { + + List testIdentifiers = Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L); + Pointer samplePointer = new Pointer(Pointer.TYPE_DATA, 100, 0); + + for (int tableId = 1; tableId <= 2; tableId++){ + HeaderManager headerManager = new InMemoryHeaderManager(header); + FileIndexStorageManager fileIndexStorageManager = new FileIndexStorageManager(dbPath, headerManager, engineConfig); + IndexManager indexManager = new BTreeIndexManager(order, fileIndexStorageManager); + + + for (long testIdentifier : testIdentifiers) { + indexManager.addIndex(tableId, testIdentifier, samplePointer); + } + + Optional optional = fileIndexStorageManager.getRoot(tableId).get(); + Assertions.assertTrue(optional.isPresent()); + + BaseTreeNode rootNode = BaseTreeNode.fromBytes(optional.get().bytes()); + Assertions.assertTrue(rootNode.isRoot()); + Assertions.assertFalse(rootNode.isLeaf()); + + Assertions.assertEquals(7, rootNode.keys().next()); + + // Checking root child at left + BaseTreeNode leftChildInternalNode = BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + ((InternalTreeNode) rootNode + ).getChildAtIndex(0).get()).get().bytes() + ); + List leftChildInternalNodeKeys = leftChildInternalNode.keyList(); + List leftChildInternalNodeChildren = ((InternalTreeNode)leftChildInternalNode).childrenList(); + Assertions.assertEquals(2, leftChildInternalNodeKeys.size()); + Assertions.assertEquals(3, leftChildInternalNodeKeys.get(0)); + Assertions.assertEquals(5, leftChildInternalNodeKeys.get(1)); + + // Far left leaf + LeafTreeNode currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(0) + ).get().bytes() + ); + List currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(1, currentLeafKeys.get(0)); + Assertions.assertEquals(2, currentLeafKeys.get(1)); + + // 2nd Leaf + Optional nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), leftChildInternalNodeChildren.get(1)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(1) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(3, currentLeafKeys.get(0)); + Assertions.assertEquals(4, currentLeafKeys.get(1)); + + //3rd leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), leftChildInternalNodeChildren.get(2)); // Todo + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(2) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(5, currentLeafKeys.get(0)); + Assertions.assertEquals(6, currentLeafKeys.get(1)); + + + // Checking root child at right + BaseTreeNode rightChildInternalNode = BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + ((InternalTreeNode) rootNode + ).getChildAtIndex(1).get()).get().bytes() + ); + List rightChildInternalNodeKeys = rightChildInternalNode.keyList(); + List rightChildInternalNodeChildren = ((InternalTreeNode)rightChildInternalNode).childrenList(); + Assertions.assertEquals(2, rightChildInternalNodeKeys.size()); + Assertions.assertEquals(9, rightChildInternalNodeKeys.get(0)); + Assertions.assertEquals(11, rightChildInternalNodeKeys.get(1)); + + // 4th leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), rightChildInternalNodeChildren.get(0)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + rightChildInternalNodeChildren.get(0) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(7, currentLeafKeys.get(0)); + Assertions.assertEquals(8, currentLeafKeys.get(1)); + + + // 5th leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), rightChildInternalNodeChildren.get(1)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + rightChildInternalNodeChildren.get(1) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(9, currentLeafKeys.get(0)); + Assertions.assertEquals(10, currentLeafKeys.get(1)); + + + // 6th node + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), rightChildInternalNodeChildren.get(2)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + rightChildInternalNodeChildren.get(2) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(11, currentLeafKeys.get(0)); + Assertions.assertEquals(12, currentLeafKeys.get(1)); + + } + + } + + + /** + * + * The B+Tree in this test will include numbers from [1-12] added respectively + * The shape of the tree will be like below, and the test verifies that + * The test validates the tree for 2 tables in same database + * 009 + * ├── . + * │ ├── 001 + * │ └── 002 + * ├── 003 + * │ ├── 003 + * │ ├── 004 + * │ └── 005 + * ├── 006 + * │ ├── 006 + * │ ├── 007 + * │ └── 008 + * ├── . + * │ ├── 009 + * │ └── 010 + * └── 011 + * ├── 011 + * └── 012 + */ + @Test + public void testMultiSplitAddIndex2() throws IOException, ExecutionException, InterruptedException { + + List testIdentifiers = Arrays.asList(1L, 4L, 9L, 6L, 10L, 8L, 3L, 2L, 11L, 5L, 7L, 12L); + Pointer samplePointer = new Pointer(Pointer.TYPE_DATA, 100, 0); + + for (int tableId = 1; tableId <= 2; tableId++){ + HeaderManager headerManager = new InMemoryHeaderManager(header); + FileIndexStorageManager fileIndexStorageManager = new FileIndexStorageManager(dbPath, headerManager, engineConfig); + IndexManager indexManager = new BTreeIndexManager(order, fileIndexStorageManager); + + + for (long testIdentifier : testIdentifiers) { + indexManager.addIndex(tableId, testIdentifier, samplePointer); + } + + Optional optional = fileIndexStorageManager.getRoot(tableId).get(); + Assertions.assertTrue(optional.isPresent()); + + BaseTreeNode rootNode = BaseTreeNode.fromBytes(optional.get().bytes()); + Assertions.assertTrue(rootNode.isRoot()); + Assertions.assertFalse(rootNode.isLeaf()); + + Assertions.assertEquals(9, rootNode.keys().next()); + + // Checking root child at left + BaseTreeNode leftChildInternalNode = BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + ((InternalTreeNode) rootNode + ).getChildAtIndex(0).get()).get().bytes() + ); + List leftChildInternalNodeKeys = leftChildInternalNode.keyList(); + List leftChildInternalNodeChildren = ((InternalTreeNode)leftChildInternalNode).childrenList(); + Assertions.assertEquals(2, leftChildInternalNodeKeys.size()); + Assertions.assertEquals(3, leftChildInternalNodeKeys.get(0)); + Assertions.assertEquals(6, leftChildInternalNodeKeys.get(1)); + + + // Far left leaf + LeafTreeNode currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(0) + ).get().bytes() + ); + List currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(1, currentLeafKeys.get(0)); + Assertions.assertEquals(2, currentLeafKeys.get(1)); + + + // 2nd Leaf + Optional nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), leftChildInternalNodeChildren.get(1)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(1) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(3, currentLeafKeys.size()); + Assertions.assertEquals(3, currentLeafKeys.get(0)); + Assertions.assertEquals(4, currentLeafKeys.get(1)); + Assertions.assertEquals(5, currentLeafKeys.get(2)); + + + //3rd leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), leftChildInternalNodeChildren.get(2)); // Todo + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + leftChildInternalNodeChildren.get(2) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(3, currentLeafKeys.size()); + Assertions.assertEquals(6, currentLeafKeys.get(0)); + Assertions.assertEquals(7, currentLeafKeys.get(1)); + Assertions.assertEquals(8, currentLeafKeys.get(2)); + + + // Checking root child at right + BaseTreeNode rightChildInternalNode = BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + ((InternalTreeNode) rootNode + ).getChildAtIndex(1).get()).get().bytes() + ); + List rightChildInternalNodeKeys = rightChildInternalNode.keyList(); + List rightChildInternalNodeChildren = ((InternalTreeNode) rightChildInternalNode).childrenList(); + Assertions.assertEquals(1, rightChildInternalNodeKeys.size()); + Assertions.assertEquals(11, rightChildInternalNodeKeys.get(0)); + + + // 4th leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); //Todo + Assertions.assertEquals(nextPointer.get(), rightChildInternalNodeChildren.get(0)); + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + rightChildInternalNodeChildren.get(0) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(9, currentLeafKeys.get(0)); + Assertions.assertEquals(10, currentLeafKeys.get(1)); + + + // 5th leaf + nextPointer = currentLeaf.getNext(); + Assertions.assertTrue(nextPointer.isPresent()); + Assertions.assertEquals(nextPointer.get(), rightChildInternalNodeChildren.get(1)); //Todo + + currentLeaf = (LeafTreeNode) BaseTreeNode.fromBytes( + fileIndexStorageManager.readNode( + tableId, + rightChildInternalNodeChildren.get(1) + ).get().bytes() + ); + currentLeafKeys = currentLeaf.keyList(); + Assertions.assertEquals(2, currentLeafKeys.size()); + Assertions.assertEquals(11, currentLeafKeys.get(0)); + Assertions.assertEquals(12, currentLeafKeys.get(1)); + + } + + } + +} diff --git a/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerTestCase.java b/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerTestCase.java index f0fa0e2..1a49b70 100644 --- a/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerTestCase.java +++ b/src/test/java/com/github/sepgh/internal/tree/MultiTableBTreeIndexManagerTestCase.java @@ -58,6 +58,12 @@ public void setUp() throws IOException { .build() ) ) + .root( + Header.IndexChunk.builder() + .chunk(0) + .offset(0) + .build() + ) .initialized(true) .build(), Header.Table.builder() @@ -71,6 +77,12 @@ public void setUp() throws IOException { .build() ) ) + .root( + Header.IndexChunk.builder() + .chunk(0) + .offset(12L * engineConfig.getPaddedSize()) + .build() + ) .initialized(true) .build() )