From 5f533af1c8d9715a06032aa86100f5097cabfce1 Mon Sep 17 00:00:00 2001 From: bronku Date: Tue, 9 Dec 2025 11:33:33 +0100 Subject: [PATCH] storage? --- .gitignore | 2 + Cargo.lock | 122 ++++++++++++---------- Cargo.toml | 2 +- src/btree.rs | 33 +++++- src/consts.rs | 2 + src/main.rs | 29 +++++- src/node.rs | 30 ++++-- src/node_storage.rs | 242 ++++++++++++++++++++++++++++++++++++++++++++ src/record.rs | 20 +++- 9 files changed, 414 insertions(+), 68 deletions(-) create mode 100644 src/consts.rs create mode 100644 src/node_storage.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..25bb9a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +.DS_Store +btree.idx diff --git a/Cargo.lock b/Cargo.lock index e90a554..3decc12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,31 +3,17 @@ version = 4 [[package]] -name = "bincode" -version = "2.0.1" +name = "bitflags" +version = "2.10.0" 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", -] +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "btree" version = "0.1.0" dependencies = [ - "bincode", "rand", + "tempfile", ] [[package]] @@ -36,6 +22,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "getrandom" version = "0.3.4" @@ -54,6 +56,18 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -117,32 +131,16 @@ dependencies = [ ] [[package]] -name = "serde" -version = "1.0.228" +name = "rustix" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 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", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -156,24 +154,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[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 = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -183,6 +182,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 5cbed41..e3d2c3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -bincode = "2.0.1" rand = "0.9.2" +tempfile = "3.23.0" diff --git a/src/btree.rs b/src/btree.rs index f5ba0f2..6b6dfac 100644 --- a/src/btree.rs +++ b/src/btree.rs @@ -1,6 +1,36 @@ +use crate::Record; use crate::node::Node; -use crate::record::Record; +use crate::node_storage::NodeStorage; +#[derive(Debug)] +pub struct BTree { + storage: NodeStorage, + root: Node, +} + +impl BTree { + pub fn new(filename: &str) -> Self { + let mut storage = NodeStorage::open(filename); + let root = if storage.num_nodes() > 0 { + storage.read_node(0) + } else { + Node::new(true) + }; + + BTree { + storage: storage, + root: root, + } + } + + pub fn search(&self, key: i32) -> Option { + let mut current = 0; + let mut node = self.root; + + None + } +} +/* #[derive(Debug)] pub struct BTree { root: Option>, @@ -61,3 +91,4 @@ impl BTree { } } } +*/ diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..7977b7c --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,2 @@ +pub const MIN_DEGREE: usize = 3; // Example minimum degree +pub const MAX_KEYS: usize = 2 * MIN_DEGREE; // Max keys per node diff --git a/src/main.rs b/src/main.rs index 799f8db..0d5d7ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,35 @@ mod btree; +mod consts; mod node; +mod node_storage; mod record; -use btree::BTree; -use record::Record; +use crate::node::Node; +use crate::node_storage::NodeStorage; +use crate::record::Record; fn main() { + let mut storage = NodeStorage::open("btree.idx"); + + let mut node = storage.read_node(1); + node.insert_non_full(Record { + key: 1, + a: [69, 0, 0, 0, 0], + x: 0, + }); + + println!("{:?}", node); + + storage.write_node(1, &node); + + let node1 = storage.read_node(1); + + println!("{:?}", node1); + + println!("Record = {} bytes", std::mem::size_of::()); + println!("Node = {} bytes", std::mem::size_of::()); + + /* let mut btree = BTree::new(); let mut values: Vec = vec![]; @@ -24,4 +48,5 @@ fn main() { println!("Search for 6: {:?}", btree.search(values[0])); println!("Search for 15: {:?}", btree.search(values[4])); println!("Search for 21: {:?}", btree.search(100)); + */ } diff --git a/src/node.rs b/src/node.rs index ff4208e..aa1cba3 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,23 +1,36 @@ +use crate::consts::*; use crate::record::Record; -const MIN_DEGREE: usize = 3; // Minimum degree (minimum children per node) - -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct Node { - pub keys: Vec, - pub children: Vec>, + pub keys: [Option; MAX_KEYS], + pub children: [Option; MAX_KEYS + 1], + pub num_keys: usize, pub is_leaf: bool, } impl Node { pub fn new(is_leaf: bool) -> Self { - Node { - keys: Vec::new(), - children: Vec::new(), + Self { + keys: [(); MAX_KEYS].map(|_| None), + children: [(); MAX_KEYS + 1].map(|_| None), + num_keys: 0, is_leaf, } } + pub fn insert_non_full(&mut self, input: Record) -> bool { + let i = self.num_keys; + if i == MAX_KEYS { + return false; + } + self.keys[i] = Some(input); + self.num_keys += 1; + return true; + } + + /* + pub fn is_full(&self) -> bool { self.keys.len() >= 2 * MIN_DEGREE - 1 } @@ -82,4 +95,5 @@ impl Node { self.keys.insert(i, median); self.children.insert(i + 1, new_child); } + */ } diff --git a/src/node_storage.rs b/src/node_storage.rs new file mode 100644 index 0000000..bb67354 --- /dev/null +++ b/src/node_storage.rs @@ -0,0 +1,242 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; + +use crate::node::Node; +use crate::record::Record; + +pub const PAGE_SIZE: usize = 512; + +#[derive(Debug)] +pub struct NodeStorage { + file: File, + pub page_reads: usize, + pub page_writes: usize, +} + +impl NodeStorage { + pub fn open(path: &str) -> Self { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path) + .unwrap(); + + Self { + file, + page_reads: 0, + page_writes: 0, + } + } + fn serialize_node(node: &Node) -> [u8; PAGE_SIZE] { + let mut out = String::new(); + + // Format: + // L|numkeys|key0;key1;...|child0,child1,... + out.push(if node.is_leaf { 'L' } else { 'I' }); + out.push('|'); + out.push_str(&node.num_keys.to_string()); + out.push('|'); + + // Keys + for i in 0..node.num_keys { + let rec = node.keys[i].unwrap(); + out.push_str(&rec.to_text()); + if i + 1 < node.num_keys { + out.push(';'); + } + } + + out.push('|'); + + // Children (m = num_keys, m+1 children) + for i in 0..=node.num_keys { + match node.children[i] { + Some(idx) => out.push_str(&idx.to_string()), + None => out.push('.'), + } + + if i < node.num_keys { + out.push(','); + } + } + + // Convert to fixed-size block + let mut block = [b' '; PAGE_SIZE]; + let bytes = out.as_bytes(); + assert!(bytes.len() <= PAGE_SIZE, "Node too large to serialize"); + + block[..bytes.len()].copy_from_slice(bytes); + block + } + + fn deserialize_node(block: &[u8; PAGE_SIZE]) -> Node { + let text = std::str::from_utf8(block).unwrap().trim_end(); + let parts: Vec<&str> = text.split('|').collect(); + + let mut node = Node::new(parts[0] == "L"); + node.num_keys = parts[1].parse().unwrap(); + + if !parts[2].is_empty() { + for (i, ks) in parts[2].split(';').enumerate() { + let rec = Record::from_text(ks); + node.keys[i] = Some(rec); + } + } + + if !parts[3].is_empty() { + for (i, cs) in parts[3].split(',').enumerate() { + node.children[i] = if cs == "." { + None + } else { + Some(cs.parse().unwrap()) + } + } + } + + node + } + + pub fn read_node(&mut self, index: usize) -> Node { + let offset = (index * PAGE_SIZE) as u64; + + self.file.seek(SeekFrom::Start(offset)).unwrap(); + let mut block = [0u8; PAGE_SIZE]; + self.file.read_exact(&mut block).unwrap(); + + self.page_reads += 1; + + Self::deserialize_node(&block) + } + + pub fn write_node(&mut self, index: usize, node: &Node) { + let offset = (index * PAGE_SIZE) as u64; + + let block = Self::serialize_node(node); + + self.file.seek(SeekFrom::Start(offset)).unwrap(); + self.file.write_all(&block).unwrap(); + + self.page_writes += 1; + } + + pub fn append_node(&mut self, node: &Node) -> usize { + let index = self.num_nodes(); + self.write_node(index, node); + index + } + + pub fn num_nodes(&self) -> usize { + let len = self.file.metadata().unwrap().len() as usize; + len / PAGE_SIZE + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::Node; + use crate::record::Record; + use rand::Rng; + use tempfile::tempfile; + + fn random_node() -> Node { + let mut node = Node::new(true); + let mut rng = rand::rng(); + let n = rng.random_range(1..=crate::consts::MAX_KEYS); + node.num_keys = n; + for i in 0..n { + node.keys[i] = Some(Record::random()); + } + for i in 0..=n { + node.children[i] = Some(rng.random_range(0..100)); + } + + node + } + + #[test] + fn test_append_and_read_single_node() { + let file = tempfile().unwrap(); + let mut storage = NodeStorage { + file, + page_reads: 0, + page_writes: 0, + }; + + let node = random_node(); + let index = storage.append_node(&node); + + assert_eq!(index, 0); + assert_eq!(storage.page_writes, 1); + assert_eq!(storage.num_nodes(), 1); + + let read_node = storage.read_node(0); + assert_eq!(read_node.num_keys, node.num_keys); + + for i in 0..node.num_keys { + assert_eq!(read_node.keys[i].unwrap().key, node.keys[i].unwrap().key); + } + assert_eq!(storage.page_reads, 1); + } + + #[test] + fn test_overwrite_node() { + let file = tempfile().unwrap(); + let mut storage = NodeStorage { + file, + page_reads: 0, + page_writes: 0, + }; + + let node1 = random_node(); + let node2 = random_node(); + + let index = storage.append_node(&node1); + storage.write_node(index, &node2); + + let read_node = storage.read_node(index); + assert_eq!(read_node.num_keys, node2.num_keys); + + for i in 0..node2.num_keys { + assert_eq!(read_node.keys[i].unwrap().key, node2.keys[i].unwrap().key); + } + + assert_eq!(storage.page_writes, 2); + assert_eq!(storage.page_reads, 1); + } + + #[test] + fn test_multiple_nodes() { + let file = tempfile().unwrap(); + let mut storage = NodeStorage { + file, + page_reads: 0, + page_writes: 0, + }; + + let mut nodes = vec![]; + + // append 10 random nodes + for _ in 0..10 { + let node = random_node(); + storage.append_node(&node); + nodes.push(node); + } + + assert_eq!(storage.num_nodes(), 10); + assert_eq!(storage.page_writes, 10); + + // read them back and verify + for i in 0..10 { + let read_node = storage.read_node(i); + let orig = &nodes[i]; + assert_eq!(read_node.num_keys, orig.num_keys); + for j in 0..orig.num_keys { + assert_eq!(read_node.keys[j].unwrap().key, orig.keys[j].unwrap().key); + } + } + + assert_eq!(storage.page_reads, 10); + } +} diff --git a/src/record.rs b/src/record.rs index bfb8cc2..fd3d396 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,7 +1,6 @@ -use bincode::{Decode, Encode}; use rand::Rng; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Encode, Decode)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub struct Record { pub key: i32, pub a: [i32; 5], @@ -27,4 +26,21 @@ impl Record { let x = rng.random::(); Self::new(a, x) } + + pub fn to_text(&self) -> String { + format!( + "{},{},{},{},{},{},{}", + self.key, self.a[0], self.a[1], self.a[2], self.a[3], self.a[4], self.x + ) + } + + pub fn from_text(s: &str) -> Self { + let parts: Vec = s.split(',').map(|p| p.parse().unwrap()).collect(); + + Self { + key: parts[0], + a: [parts[1], parts[2], parts[3], parts[4], parts[5]], + x: parts[6], + } + } }