diff --git a/Cargo.lock b/Cargo.lock index 3decc12..e424f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,47 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.10.0" @@ -12,6 +53,8 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" name = "btree" version = "0.1.0" dependencies = [ + "bincode", + "proptest", "rand", "tempfile", ] @@ -38,6 +81,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "getrandom" version = "0.3.4" @@ -62,6 +111,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -86,6 +144,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.42" @@ -130,6 +213,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rustix" version = "1.1.2" @@ -143,6 +241,47 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.111" @@ -167,12 +306,39 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" diff --git a/Cargo.toml b/Cargo.toml index e3d2c3f..9352c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +bincode = "2.0.1" +proptest = "1.9.0" rand = "0.9.2" tempfile = "3.23.0" diff --git a/src/btree.rs b/src/btree.rs index ade0036..059d16d 100644 --- a/src/btree.rs +++ b/src/btree.rs @@ -21,7 +21,7 @@ where } } - pub fn find(&self, key: i32) -> Option { + pub fn find(&mut self, key: i32) -> Option { let mut current_loc = self.root_loc; loop { let node = self.storage.read_node(current_loc)?; @@ -286,7 +286,7 @@ mod tests { #[test] fn test_non_existent_key() { let storage = InMemoryStorage::new(); - let tree = BPlusTree::open(storage); + let mut tree = BPlusTree::open(storage); assert_eq!(tree.find(999), None); // Assuming 999 is not in the tree } diff --git a/src/config.rs b/src/config.rs index 5bc575d..f5d26c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,2 +1,3 @@ pub const DEGREE: usize = 2; pub const MAX_KEYS: usize = 4; +pub const PAGE_SIZE: usize = 2048; diff --git a/src/file_storage.rs b/src/file_storage.rs new file mode 100644 index 0000000..4b5af4b --- /dev/null +++ b/src/file_storage.rs @@ -0,0 +1,309 @@ +use bincode::error::DecodeError; + +use crate::storage::Storage; +use crate::{config::PAGE_SIZE, node::Node}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; + +#[derive(Debug)] +pub struct FileStorage { + pub file: File, + pub page_reads: usize, + pub page_writes: usize, +} + +impl FileStorage { + pub fn new(path: &str) -> Self { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path) + .unwrap(); + + Self { + file, + page_reads: 0, + page_writes: 0, + } + } +} + +impl Storage for FileStorage { + fn read_node(&mut self, loc: usize) -> Option { + let offset = (loc * PAGE_SIZE) as u64; + self.file.seek(SeekFrom::Start(offset)).ok()?; + let mut block = [0u8; PAGE_SIZE]; + self.file.read_exact(&mut block).ok()?; + self.page_reads += 1; + FileStorage::deserialize_node(block) + } + + fn write_node(&mut self, loc: usize, input: &Node) { + let offset = (loc * PAGE_SIZE) as u64; + let block = FileStorage::serialize_node(input); + self.file.seek(SeekFrom::Start(offset)).unwrap(); + self.file.write_all(&block).unwrap(); + self.page_writes += 1; + } + fn total_nodes(&self) -> usize { + self.file.metadata().unwrap().len() as usize / PAGE_SIZE + } +} + +impl FileStorage { + fn serialize_node(input: &Node) -> [u8; PAGE_SIZE] { + let mut slice = [0u8; PAGE_SIZE]; + let _ = bincode::encode_into_slice(input, &mut slice, bincode::config::standard()); + return slice; + } + fn deserialize_node(input: [u8; PAGE_SIZE]) -> Option { + let result: Result<(Node, usize), DecodeError> = + bincode::decode_from_slice(&input, bincode::config::standard()); + match result { + Ok(value) => Some(value.0), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::{ + config::MAX_KEYS, + node::{InternalNode, LeafNode, Node}, + record::Record, + }; + use proptest::prelude::*; + use tempfile::NamedTempFile; + + fn temp_storage() -> FileStorage { + let file = NamedTempFile::new().unwrap(); + FileStorage::new(file.path().to_str().unwrap()) + } + + fn sample_leaf() -> Node { + Node::Leaf(LeafNode { + keys: vec![1, 2, 3], + values: vec![[1; 7], [2; 7], [3; 7]], + next: Some(42), + }) + } + + fn sample_internal() -> Node { + Node::Internal(InternalNode { + keys: vec![10, 20], + children: vec![1, 2, 3], + }) + } + + #[test] + fn write_and_read_leaf_node() { + let mut storage = temp_storage(); + let node = sample_leaf(); + + storage.write_node(0, &node); + let read_back = storage.read_node(0); + + assert_eq!(read_back.unwrap(), node); + assert_eq!(storage.page_reads, 1); + assert_eq!(storage.page_writes, 1); + } + + #[test] + fn write_and_read_internal_node() { + let mut storage = temp_storage(); + let node = sample_internal(); + + storage.write_node(5, &node); + let read_back = storage.read_node(5); + + assert_eq!(read_back.unwrap(), node); + } + + #[test] + fn read_empty_page_returns_none() { + let mut storage = temp_storage(); + + let result = storage.read_node(0); + + assert!(result.is_none()); + assert_eq!(storage.page_reads, 0); + } + + #[test] + fn total_nodes_is_correct() { + let mut storage = temp_storage(); + + storage.write_node(0, &sample_leaf()); + storage.write_node(1, &sample_internal()); + + assert_eq!(storage.total_nodes(), 2); + } + + #[test] + fn overwrite_existing_node() { + let mut storage = temp_storage(); + + let node1 = sample_leaf(); + let node2 = sample_internal(); + + storage.write_node(0, &node1); + storage.write_node(0, &node2); + + let read_back = storage.read_node(0).unwrap(); + assert_eq!(read_back, node2); + } + + fn arb_record() -> impl Strategy { + prop::array::uniform7(any::()) + } + + fn arb_node() -> impl Strategy { + let max = MAX_KEYS * 2 + 2; + + prop_oneof![ + ( + prop::collection::vec(any::(), 0..=max), + prop::collection::vec(arb_record(), 0..=max), + proptest::option::of(any::()), + ) + .prop_map(|(keys, values, next)| { Node::Leaf(LeafNode { keys, values, next }) }), + ( + prop::collection::vec(any::(), 0..=max), + prop::collection::vec(any::(), 0..=max + 1), + ) + .prop_map(|(keys, children)| { Node::Internal(InternalNode { keys, children }) }) + ] + } + + fn arb_nodes() -> impl Strategy> { + prop::collection::vec(arb_node(), 0..100) + } + + proptest! { + #[test] + fn node_roundtrip(node in arb_node()) { + let mut storage = temp_storage(); + + storage.write_node(0, &node); + let read_back = storage.read_node(0).unwrap(); + + prop_assert_eq!(node, read_back); + } + } + + proptest! { + #[test] + fn prop_multiple_nodes_roundtrip(nodes in arb_nodes()) { + let mut storage = temp_storage(); + + for (i, node) in nodes.iter().enumerate() { + storage.write_node(i, node); + } + + for (i, node) in nodes.iter().enumerate() { + let read_back = storage.read_node(i).unwrap(); + prop_assert_eq!(read_back, node.clone()); + } + } + } + + proptest! { + #[test] + fn prop_overwrite_last_wins(nodes in arb_nodes()) { + let mut storage = temp_storage(); + + for node in &nodes { + storage.write_node(0, node); + } + + if let Some(last) = nodes.last() { + let read_back = storage.read_node(0).unwrap(); + prop_assert_eq!(read_back, last.clone()); + } + } + } + + proptest! { + #[test] + fn prop_sparse_writes_are_isolated( + a in arb_node(), + b in arb_node(), + loc_a in 0usize..20, + loc_b in 0usize..20, + ) { + prop_assume!(loc_a != loc_b); + + let mut storage = temp_storage(); + + storage.write_node(loc_a, &a); + storage.write_node(loc_b, &b); + + let ra = storage.read_node(loc_a).unwrap(); + let rb = storage.read_node(loc_b).unwrap(); + + prop_assert_eq!(ra, a); + prop_assert_eq!(rb, b); + } + } + + proptest! { + #[test] + fn prop_total_nodes_matches_highest_write( + nodes in arb_nodes() + ) { + let mut storage = temp_storage(); + + for (i, node) in nodes.iter().enumerate() { + storage.write_node(i * 2, node); // deliberately sparse + } + + if !nodes.is_empty() { + let expected_min = (nodes.len() - 1) * 2 + 1; + prop_assert!(storage.total_nodes() >= expected_min); + } + } + } + + proptest! { + #[test] + fn prop_reads_do_not_mutate_data(nodes in arb_nodes()) { + let mut storage = temp_storage(); + + for (i, node) in nodes.iter().enumerate() { + storage.write_node(i, node); + } + + for i in 0..nodes.len() { + let _ = storage.read_node(i); + } + + for (i, node) in nodes.iter().enumerate() { + let read_back = storage.read_node(i).unwrap(); + prop_assert_eq!(read_back, node.clone()); + } + } + } + + proptest! { + #[test] + fn prop_unwritten_pages_return_none( + nodes in arb_nodes(), + extra in 1usize..20 + ) { + let mut storage = temp_storage(); + + for (i, node) in nodes.iter().enumerate() { + storage.write_node(i, node); + } + + let loc = nodes.len() + extra; + let result = storage.read_node(loc); + + prop_assert!(result.is_none()); + } + } +} diff --git a/src/main.rs b/src/main.rs index b32d835..0ef1c22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ mod btree; mod config; +mod file_storage; mod node; mod record; mod storage; -use crate::btree::BPlusTree; -use crate::storage::InMemoryStorage; +use crate::{btree::BPlusTree, file_storage::FileStorage}; fn main() { - let storage = InMemoryStorage::new(); + let storage = FileStorage::new("data.db"); let mut tree = BPlusTree::open(storage); // Insert some key-value pairs diff --git a/src/node.rs b/src/node.rs index e0b4881..d03bc5b 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,13 +1,15 @@ +use bincode::{Decode, Encode}; + use crate::config::DEGREE; use crate::record::Record; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] pub enum Node { Leaf(LeafNode), Internal(InternalNode), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] pub struct LeafNode { pub keys: Vec, pub values: Vec, @@ -24,7 +26,7 @@ impl LeafNode { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] pub struct InternalNode { pub keys: Vec, pub children: Vec, diff --git a/src/storage.rs b/src/storage.rs index 78862cf..8ba1961 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,7 +1,7 @@ use crate::node::Node; pub trait Storage { - fn read_node(&self, loc: usize) -> Option; + fn read_node(&mut self, loc: usize) -> Option; fn write_node(&mut self, loc: usize, node: &Node); fn total_nodes(&self) -> usize; } @@ -18,7 +18,7 @@ impl InMemoryStorage { } impl Storage for InMemoryStorage { - fn read_node(&self, loc: usize) -> Option { + fn read_node(&mut self, loc: usize) -> Option { self.nodes.get(loc)?.clone() }