diff --git a/btree_test b/btree_test index bd75cc1..8c470df 100755 Binary files a/btree_test and b/btree_test differ diff --git a/content_addressable_demo b/content_addressable_demo new file mode 100755 index 0000000..e0c15c3 Binary files /dev/null and b/content_addressable_demo differ diff --git a/content_hash_demo b/content_hash_demo index 43678d8..db0cbe5 100755 Binary files a/content_hash_demo and b/content_hash_demo differ diff --git a/deduplication_demo b/deduplication_demo new file mode 100755 index 0000000..82982c1 Binary files /dev/null and b/deduplication_demo differ diff --git a/include/btree.h b/include/btree.h index f7a37b9..49452f0 100644 --- a/include/btree.h +++ b/include/btree.h @@ -6,6 +6,7 @@ #include #include "fraction.h" #include "page_manager.h" +#include "content_storage.h" /* * BTree that stores the BTreeNodes, ensures it is balanced @@ -16,6 +17,8 @@ class BTree { private: Page* root; int maxKeysPerNode; // Maximum keys in each node + ContentStorage content_storage; + void insertNonFull(Page* root, const KeyType& key, const ValueType& value); void splitChild(Page* parent, int index, Page* child); @@ -25,10 +28,11 @@ class BTree { void mergeNodes(Page* parent, int index); public: - BTree(int maxKeys); // Constructor declaration + BTree(int maxKeys); void insert(const KeyType& key, const ValueType& value); void deleteKey(const KeyType& key); ValueType* search(const KeyType& key); // Public search method + void printStorageStats() const; Page findKey(Page* node, const KeyType& key); diff --git a/include/content_storage.h b/include/content_storage.h new file mode 100644 index 0000000..698f923 --- /dev/null +++ b/include/content_storage.h @@ -0,0 +1,100 @@ +#pragma once +#include +#include +#include +#include +#include +#include "page_manager.h" + +template +class ContentStorage { +private: + // Map content hash to actual page data + std::unordered_map>> content_map; + + // Map page ID to content hash for reverse lookup + std::unordered_map page_to_hash; + + // Next available page ID + uint16_t next_page_id = 1; + +public: + // Store a page and return its page ID + uint16_t storePage(const Page& page) { + // Update the page's content hash + Page page_copy = page; + page_copy.updateContentHash(); + std::string content_hash = page_copy.getContentHash(); + + // Check if we already have this content + auto it = content_map.find(content_hash); + if (it != content_map.end()) { + // Content already exists, return existing page ID + std::cout << "Deduplication: Found existing content with hash " << content_hash + << ", reusing page ID " << it->second->header.page_id << std::endl; + return it->second->header.page_id; + } + + // New content, assign a new page ID + page_copy.header.page_id = next_page_id++; + page_to_hash[page_copy.header.page_id] = content_hash; + content_map[content_hash] = std::make_shared>(page_copy); + + std::cout << "Stored new content with hash " << content_hash + << " as page ID " << page_copy.header.page_id << std::endl; + return page_copy.header.page_id; + } + + // Retrieve a page by its page ID + std::shared_ptr> getPage(uint16_t page_id) { + auto hash_it = page_to_hash.find(page_id); + if (hash_it == page_to_hash.end()) { + return nullptr; // Page not found + } + + auto content_it = content_map.find(hash_it->second); + if (content_it == content_map.end()) { + return nullptr; // Content not found (shouldn't happen) + } + + return content_it->second; + } + + // Get statistics about storage usage + void printStats() const { + std::cout << "\n=== Content Storage Statistics ===" << std::endl; + std::cout << "Total unique content blocks: " << content_map.size() << std::endl; + std::cout << "Total page IDs assigned: " << page_to_hash.size() << std::endl; + std::cout << "Next available page ID: " << next_page_id << std::endl; + + if (content_map.size() > 0) { + size_t total_keys = 0; + size_t total_data = 0; + for (const auto& pair : content_map) { + total_keys += pair.second->keys.size(); + total_data += pair.second->data.size(); + } + std::cout << "Total keys stored: " << total_keys << std::endl; + std::cout << "Total data bytes: " << total_data << std::endl; + } + std::cout << "===================================" << std::endl; + } + + // Check if a page with given content already exists + bool hasContent(const Page& page) { + Page page_copy = page; + page_copy.updateContentHash(); + return content_map.find(page_copy.getContentHash()) != content_map.end(); + } + + // Get the page ID for existing content + uint16_t getPageIdForContent(const Page& page) { + Page page_copy = page; + page_copy.updateContentHash(); + auto it = content_map.find(page_copy.getContentHash()); + if (it != content_map.end()) { + return it->second->header.page_id; + } + return 0; + } +}; diff --git a/makefile b/makefile index f2d874e..0be3c6a 100644 --- a/makefile +++ b/makefile @@ -11,12 +11,22 @@ OBJECTS = $(SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) DEMO_SOURCES = src/Btree.cpp src/content_hash_demo.cpp src/page_manager.cpp DEMO_OBJECTS = $(DEMO_SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) +# Content addressable demo +ADDRESSABLE_SOURCES = src/Btree.cpp src/content_addressable_demo.cpp src/page_manager.cpp +ADDRESSABLE_OBJECTS = $(ADDRESSABLE_SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) + +# Deduplication demo +DEDUP_SOURCES = src/Btree.cpp src/deduplication_demo.cpp src/page_manager.cpp +DEDUP_OBJECTS = $(DEDUP_SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) + # Target executables TARGET = btree_test DEMO_TARGET = content_hash_demo +ADDRESSABLE_TARGET = content_addressable_demo +DEDUP_TARGET = deduplication_demo # Default target -all: $(TARGET) $(DEMO_TARGET) +all: $(TARGET) $(DEMO_TARGET) $(ADDRESSABLE_TARGET) $(DEDUP_TARGET) # Create object directory if it doesn't exist $(OBJDIR): @@ -34,9 +44,17 @@ $(TARGET): $(OBJECTS) $(DEMO_TARGET): $(DEMO_OBJECTS) $(CXX) $(DEMO_OBJECTS) -o $(DEMO_TARGET) +# Link addressable demo executable +$(ADDRESSABLE_TARGET): $(ADDRESSABLE_OBJECTS) + $(CXX) $(ADDRESSABLE_OBJECTS) -o $(ADDRESSABLE_TARGET) + +# Link deduplication demo executable +$(DEDUP_TARGET): $(DEDUP_OBJECTS) + $(CXX) $(DEDUP_OBJECTS) -o $(DEDUP_TARGET) + # Clean build files clean: - rm -rf $(OBJDIR) $(TARGET) $(DEMO_TARGET) + rm -rf $(OBJDIR) $(TARGET) $(DEMO_TARGET) $(ADDRESSABLE_TARGET) $(DEDUP_TARGET) # Run the test run: $(TARGET) @@ -46,4 +64,12 @@ run: $(TARGET) demo: $(DEMO_TARGET) ./$(DEMO_TARGET) -.PHONY: all clean run demo +# Run the addressable demo +addressable: $(ADDRESSABLE_TARGET) + ./$(ADDRESSABLE_TARGET) + +# Run the deduplication demo +dedup: $(DEDUP_TARGET) + ./$(DEDUP_TARGET) + +.PHONY: all clean run demo addressable dedup diff --git a/src/Btree.cpp b/src/Btree.cpp index ad927ce..fe10880 100644 --- a/src/Btree.cpp +++ b/src/Btree.cpp @@ -9,7 +9,9 @@ template BTree::BTree(int maxKeys) : maxKeysPerNode(maxKeys) { // Initially, the tree is empty, so we create a root node // and mark it as a leaf (all data starts at the leaf level in B+ Trees) - root = new Page(createPage(true)); + Page root_page = createPage(true); + uint16_t root_id = content_storage.storePage(root_page); + root = new Page(*content_storage.getPage(root_id)); } /* @@ -18,12 +20,17 @@ BTree::BTree(int maxKeys) : maxKeysPerNode(maxKeys) { template void BTree::insert(const KeyType& key, const ValueType& value) { if (!root) { // If tree is empty, create a new root - root = new Page(createPage(true)); + Page root_page = createPage(true); + uint16_t root_id = content_storage.storePage(root_page); + root = new Page(*content_storage.getPage(root_id)); } else if (root->keys.size() == maxKeysPerNode) { // Check if the root is full - Page* newRoot = new Page(createPage(false)); - newRoot->children.push_back(0); // Page ID of the old root - splitChild(newRoot, 0, root); // Split child bc of overflow - root = newRoot; // Update the root to be the new node + Page new_root_page = createPage(false); + new_root_page.children.push_back(root->header.page_id); // Page ID of the old root + splitChild(&new_root_page, 0, root); // Split child bc of overflow + + // Store the new root in content storage + uint16_t new_root_id = content_storage.storePage(new_root_page); + root = new Page(*content_storage.getPage(new_root_id)); } // Now the root is guaranteed to not be empty insertNonFull(root, key, value); // Insert @@ -49,8 +56,13 @@ Page BTree::findKey(Page* node, const KeyT idx++; // move to child that might have key } if (idx < node->children.size()) { - // For now, we'll just return the current node since we don't have page loading - return *node; + // Load child page from content storage + auto child_page = content_storage.getPage(node->children[idx]); + if (child_page) { + return findKey(child_page.get(), key); + } else { + throw std::runtime_error("child page not found"); + } } else { throw std::runtime_error("key not found"); } @@ -88,8 +100,9 @@ void BTree::insertNonFull(Page* node, const KeyType } } - // Update content hash after modifying the page - node->updateContentHash(); + // Store the modified page in content storage + uint16_t new_page_id = content_storage.storePage(*node); + node->header.page_id = new_page_id; } else { // Find child to descend into @@ -97,12 +110,19 @@ void BTree::insertNonFull(Page* node, const KeyType i--; i++; - if (node->children[i] != 0 && node->children[i] < maxKeysPerNode) { // If the child is full, split it - splitChild(node, i, root); // For now, just use root as placeholder + // Load child page from content storage + auto child_page = content_storage.getPage(node->children[i]); + if (!child_page) { + throw std::runtime_error("child page not found"); + } + + if (child_page->keys.size() == maxKeysPerNode) { // If the child is full, split it + splitChild(node, i, child_page.get()); if (key > node->keys[i]) i++; // Check which child to go to after split } - insertNonFull(root, key, value); // Use recursion to insert in the child + // Recursively insert into child + insertNonFull(child_page.get(), key, value); } } @@ -111,29 +131,31 @@ template void BTree::splitChild(Page* parent, int index, Page* child) { int mid = maxKeysPerNode / 2; // Remember b+tree property - Page* newChild = new Page(createPage(child->is_leaf)); + Page new_child_page = createPage(child->is_leaf); // Copy second half of keys/values to the new node - newChild->keys.assign(child->keys.begin() + mid + 1, child->keys.end()); // Copy keys + new_child_page.keys.assign(child->keys.begin() + mid + 1, child->keys.end()); // Copy keys child->keys.resize(mid); // Keep the mid key in left for b+ tree if (child->is_leaf) { // If its a leaf, assign values size_t value_size = sizeof(ValueType); size_t start_offset = (mid + 1) * value_size; - newChild->data.assign(child->data.begin() + start_offset, child->data.end()); + new_child_page.data.assign(child->data.begin() + start_offset, child->data.end()); child->data.resize((mid + 1) * value_size); // Keep the mid key in left for b+ tree } else { // If not leaf, copy children - newChild->children.assign(child->children.begin() + mid + 1, child->children.end()); + new_child_page.children.assign(child->children.begin() + mid + 1, child->children.end()); child->children.resize(mid + 1); } - // Insert new child into parent - parent->children.insert(parent->children.begin() + index + 1, 1); // Insert new child page ID + // Store both modified pages in content storage + uint16_t child_id = content_storage.storePage(*child); + uint16_t new_child_id = content_storage.storePage(new_child_page); + + // Update parent + parent->children.insert(parent->children.begin() + index + 1, new_child_id); // Insert new child page ID parent->keys.insert(parent->keys.begin() + index, child->keys[mid]); // Insert the mid key into parent - child->updateContentHash(); - newChild->updateContentHash(); - parent->updateContentHash(); + child->header.page_id = child_id; } // Helper function to delete a key @@ -145,7 +167,12 @@ void BTree::deleteKey(const KeyType& key) { // If root is now empty and has a child, make child the new root if (!root->is_leaf && root->keys.empty()) { Page* oldRoot = root; // Store old root - root = new Page(createPage(true)); // For now, create new empty root + auto child_page = content_storage.getPage(root->children[0]); + if (child_page) { + root = new Page(*child_page); + } else { + root = new Page(createPage(true)); // For now, create new empty root + } delete oldRoot; // Free the old root } } @@ -168,7 +195,9 @@ void BTree::deleteFromNode(Page* node, const KeyTyp size_t start_offset = idx * value_size; node->data.erase(node->data.begin() + start_offset, node->data.begin() + start_offset + value_size); - node->updateContentHash(); + // Store the modified page in content storage + uint16_t new_page_id = content_storage.storePage(*node); + node->header.page_id = new_page_id; } else { // Key not found return; @@ -178,11 +207,16 @@ void BTree::deleteFromNode(Page* node, const KeyTyp idx++; // move to child that might have key } - // For now, just delete from root since we don't have proper child loading - deleteFromNode(root, key); // Delete from child + // Load child page from content storage + auto child_page = content_storage.getPage(node->children[idx]); + if (!child_page) { + throw std::runtime_error("child page not found"); + } + + deleteFromNode(child_page.get(), key); // Delete from child // Fix underflow (not enough keys in child) - if (root->keys.size() < (maxKeysPerNode + 1) / 2) { + if (child_page->keys.size() < (maxKeysPerNode + 1) / 2) { // For now, just leave as is since we don't have proper sibling handling } } @@ -192,8 +226,15 @@ void BTree::deleteFromNode(Page* node, const KeyTyp template void BTree::borrowFromLeft(Page* parent, int index) { - Page* child = parent->children[index] == 0 ? root : root; // For now, use root - Page* sibling = parent->children[index - 1] == 0 ? root : root; // For now, use root + auto child_page = content_storage.getPage(parent->children[index]); + auto sibling_page = content_storage.getPage(parent->children[index - 1]); + + if (!child_page || !sibling_page) { + throw std::runtime_error("child or sibling page not found"); + } + + Page* child = child_page.get(); + Page* sibling = sibling_page.get(); if (child->is_leaf) { // If leaf, just borrow the last key from sibling child->keys.insert(child->keys.begin(), sibling->keys.back()); // Insert at the beginning @@ -209,9 +250,11 @@ void BTree::borrowFromLeft(Page* parent, int index) sibling->data.resize(sibling->data.size() - value_size); // Remove the last value from sibling parent->keys[index - 1] = child->keys[0]; // Update the parent key - child->updateContentHash(); - sibling->updateContentHash(); - parent->updateContentHash(); + // Store modified pages in content storage + uint16_t child_id = content_storage.storePage(*child); + uint16_t sibling_id = content_storage.storePage(*sibling); + child->header.page_id = child_id; + sibling->header.page_id = sibling_id; } else { // If not leaf, borrow the last key and child pointer child->keys.insert(child->keys.begin(), parent->keys[index - 1]); parent->keys[index - 1] = sibling->keys.back(); // Update the parent key @@ -220,16 +263,25 @@ void BTree::borrowFromLeft(Page* parent, int index) child->children.insert(child->children.begin(), sibling->children.back()); sibling->children.pop_back(); - child->updateContentHash(); - sibling->updateContentHash(); - parent->updateContentHash(); + // Store modified pages in content storage + uint16_t child_id = content_storage.storePage(*child); + uint16_t sibling_id = content_storage.storePage(*sibling); + child->header.page_id = child_id; + sibling->header.page_id = sibling_id; } } template void BTree::borrowFromRight(Page* parent, int index) { - Page* child = parent->children[index] == 0 ? root : root; // For now, use root - Page* sibling = parent->children[index + 1] == 0 ? root : root; // For now, use root + auto child_page = content_storage.getPage(parent->children[index]); + auto sibling_page = content_storage.getPage(parent->children[index + 1]); + + if (!child_page || !sibling_page) { + throw std::runtime_error("child or sibling page not found"); + } + + Page* child = child_page.get(); + Page* sibling = sibling_page.get(); if (child->is_leaf) { // If leaf, just borrow the first key from sibling child->keys.push_back(sibling->keys.front()); @@ -244,9 +296,11 @@ void BTree::borrowFromRight(Page* parent, int index sibling->data.erase(sibling->data.begin(), sibling->data.begin() + value_size); parent->keys[index] = sibling->keys.front(); - child->updateContentHash(); - sibling->updateContentHash(); - parent->updateContentHash(); + // Store modified pages in content storage + uint16_t child_id = content_storage.storePage(*child); + uint16_t sibling_id = content_storage.storePage(*sibling); + child->header.page_id = child_id; + sibling->header.page_id = sibling_id; } else { // If not leaf, borrow the first key and child pointer child->keys.push_back(parent->keys[index]); parent->keys[index] = sibling->keys.front(); @@ -255,17 +309,26 @@ void BTree::borrowFromRight(Page* parent, int index child->children.push_back(sibling->children.front()); sibling->children.erase(sibling->children.begin()); - child->updateContentHash(); - sibling->updateContentHash(); - parent->updateContentHash(); + // Store modified pages in content storage + uint16_t child_id = content_storage.storePage(*child); + uint16_t sibling_id = content_storage.storePage(*sibling); + child->header.page_id = child_id; + sibling->header.page_id = sibling_id; } } // Merge two nodes template void BTree::mergeNodes(Page* parent, int index) { - Page* left = parent->children[index] == 0 ? root : root; // For now, use root - Page* right = parent->children[index + 1] == 0 ? root : root; // For now, use root + auto left_page = content_storage.getPage(parent->children[index]); + auto right_page = content_storage.getPage(parent->children[index + 1]); + + if (!left_page || !right_page) { + throw std::runtime_error("left or right page not found"); + } + + Page* left = left_page.get(); + Page* right = right_page.get(); if (!left->is_leaf) { // If not leaf, merge keys and children left->keys.push_back(parent->keys[index]); // Move the parent key down @@ -278,9 +341,10 @@ void BTree::mergeNodes(Page* parent, int index) { parent->keys.erase(parent->keys.begin() + index); // Remove the parent key parent->children.erase(parent->children.begin() + index + 1); // Remove the right child - - left->updateContentHash(); - parent->updateContentHash(); + + // Store modified pages in content storage + uint16_t left_id = content_storage.storePage(*left); + left->header.page_id = left_id; } // Public search method @@ -309,6 +373,12 @@ ValueType* BTree::search(const KeyType& key) { } } +// Print storage statistics +template +void BTree::printStorageStats() const { + content_storage.printStats(); +} + // Explicit template instantiations template class BTree; template class BTree; diff --git a/src/content_addressable_demo.cpp b/src/content_addressable_demo.cpp new file mode 100644 index 0000000..a522db4 --- /dev/null +++ b/src/content_addressable_demo.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include "btree.h" +#include "page_manager.h" + +int main() { + std::cout << "=== Content-Addressable Storage Deep Dive ===" << std::endl; + + // Create two pages with identical content + std::cout << "\n1. Creating two pages with identical content:" << std::endl; + + Page page1 = createPage(true); + Page page2 = createPage(true); + + // Add same keys to both pages + page1.keys = {1, 2, 3}; + page2.keys = {1, 2, 3}; + + // Add same data to both pages + std::string data1 = "apple"; + std::string data2 = "banana"; + std::string data3 = "cherry"; + + // Serialize the data + std::vector serialized_data; + for (char c : data1 + data2 + data3) { + serialized_data.push_back(static_cast(c)); + } + + page1.data = serialized_data; + page2.data = serialized_data; + + // Update content hashes + page1.updateContentHash(); + page2.updateContentHash(); + + std::cout << "Page 1 content hash: " << page1.getContentHash() << std::endl; + std::cout << "Page 2 content hash: " << page2.getContentHash() << std::endl; + std::cout << "Pages have same content: " << (page1.hasSameContent(page2) ? "YES" : "NO") << std::endl; + + // Create a third page with different content + std::cout << "\n2. Creating a third page with different content:" << std::endl; + + Page page3 = createPage(true); + page3.keys = {1, 2, 4}; // Different key + page3.data = serialized_data; + page3.updateContentHash(); + + std::cout << "Page 3 content hash: " << page3.getContentHash() << std::endl; + std::cout << "Page 1 and Page 3 have same content: " << (page1.hasSameContent(page3) ? "YES" : "NO") << std::endl; + + // Demonstrate storage efficiency + std::cout << "\n3. Storage Efficiency Benefits:" << std::endl; + std::cout << "Traditional storage would store:" << std::endl; + std::cout << " - Page 1: " << page1.keys.size() << " keys + " << page1.data.size() << " bytes" << std::endl; + std::cout << " - Page 2: " << page2.keys.size() << " keys + " << page2.data.size() << " bytes" << std::endl; + std::cout << " - Page 3: " << page3.keys.size() << " keys + " << page3.data.size() << " bytes" << std::endl; + std::cout << " Total: " << (page1.keys.size() + page2.keys.size() + page3.keys.size()) << " keys + " + << (page1.data.size() + page2.data.size() + page3.data.size()) << " bytes" << std::endl; + + std::cout << "\nContent-addressable storage would store:" << std::endl; + std::cout << " - Unique content 1 (hash: " << page1.getContentHash() << "): " + << page1.keys.size() << " keys + " << page1.data.size() << " bytes" << std::endl; + std::cout << " - Unique content 2 (hash: " << page3.getContentHash() << "): " + << page3.keys.size() << " keys + " << page3.data.size() << " bytes" << std::endl; + std::cout << " Total: " << (page1.keys.size() + page3.keys.size()) << " keys + " + << (page1.data.size() + page3.data.size()) << " bytes" << std::endl; + + std::cout << "\nSavings: " << page2.keys.size() << " keys + " << page2.data.size() << " bytes eliminated!" << std::endl; + + return 0; +} diff --git a/src/deduplication_demo.cpp b/src/deduplication_demo.cpp new file mode 100644 index 0000000..062935c --- /dev/null +++ b/src/deduplication_demo.cpp @@ -0,0 +1,54 @@ +#include +#include +#include "btree.h" + +int main() { + std::cout << "=== Content-Addressable Storage Deduplication Demo ===" << std::endl; + + // Create a B-tree with small node size to force splits + BTree tree(2); // Only 2 keys per node + + std::cout << "\n1. Inserting initial data..." << std::endl; + tree.insert(1, "apple"); + tree.insert(2, "banana"); + tree.printStorageStats(); + + std::cout << "\n2. Inserting more data to trigger splits..." << std::endl; + tree.insert(3, "cherry"); + tree.insert(4, "date"); + tree.printStorageStats(); + + std::cout << "\n3. Inserting duplicate data..." << std::endl; + tree.insert(1, "apple"); // Same key-value pair + tree.insert(2, "banana"); // Same key-value pair + tree.printStorageStats(); + + std::cout << "\n4. Inserting more unique data..." << std::endl; + tree.insert(5, "elderberry"); + tree.insert(6, "fig"); + tree.printStorageStats(); + + std::cout << "\n5. Testing search functionality..." << std::endl; + std::string* result1 = tree.search(1); + std::string* result5 = tree.search(5); + + if (result1) { + std::cout << "Found key 1: " << *result1 << std::endl; + delete result1; + } + if (result5) { + std::cout << "Found key 5: " << *result5 << std::endl; + delete result5; + } + + std::cout << "\n6. Final storage statistics:" << std::endl; + tree.printStorageStats(); + + std::cout << "\n=== Deduplication Benefits ===" << std::endl; + std::cout << "✓ Identical pages are stored only once" << std::endl; + std::cout << "✓ Storage requirements are minimized" << std::endl; + std::cout << "✓ Cache efficiency is improved" << std::endl; + std::cout << "✓ Data integrity is maintained" << std::endl; + + return 0; +} diff --git a/src/main.cpp b/src/main.cpp index e7556dd..165d4a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ int main() { std::cout << " delete - Delete a key" << std::endl; std::cout << " search - Search for a key" << std::endl; std::cout << " print - Print tree structure" << std::endl; + std::cout << " stats - Show storage statistics" << std::endl; std::cout << " quit - Exit" << std::endl; std::cout << "=====================================" << std::endl; @@ -74,12 +75,15 @@ int main() { std::cout << "Tree structure (simplified):" << std::endl; std::cout << "B-tree with max " << 3 << " keys per node" << std::endl; } + else if (cmd == "stats") { + tree.printStorageStats(); + } else if (cmd.empty()) { continue; } else { std::cout << "Unknown command: " << cmd << std::endl; - std::cout << "Available commands: insert, delete, search, print, quit" << std::endl; + std::cout << "Available commands: insert, delete, search, print, stats, quit" << std::endl; } }