From fece38bf315218980cdcd02cc47befbcdfe3e06f Mon Sep 17 00:00:00 2001 From: Jack Case Date: Thu, 26 Mar 2026 14:16:58 -0400 Subject: [PATCH] bringing in source files from my first pass at this project --- rustseq/Cargo.lock | 111 ++++++++++++++++++++ rustseq/Cargo.toml | 9 ++ rustseq/src/db.rs | 140 +++++++++++++++++++++++++ rustseq/src/main.rs | 68 ++++++++++++ rustseq/src/page.rs | 244 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 572 insertions(+) create mode 100644 rustseq/Cargo.lock create mode 100644 rustseq/Cargo.toml create mode 100644 rustseq/src/db.rs create mode 100644 rustseq/src/main.rs create mode 100644 rustseq/src/page.rs diff --git a/rustseq/Cargo.lock b/rustseq/Cargo.lock new file mode 100644 index 0000000..8ef7ff5 --- /dev/null +++ b/rustseq/Cargo.lock @@ -0,0 +1,111 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "rusqlite" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustseq" +version = "0.1.0" +dependencies = [ + "rusqlite", + "streaming-iterator", + "tree_iterators_rs", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "tree_iterators_rs" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93243e2901d558d73622302dfb013dc698e291a707a423435cd5feb8805b966f" +dependencies = [ + "streaming-iterator", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" diff --git a/rustseq/Cargo.toml b/rustseq/Cargo.toml new file mode 100644 index 0000000..77ac933 --- /dev/null +++ b/rustseq/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rustseq" +version = "0.1.0" +edition = "2024" + +[dependencies] +rusqlite = "0.34.0" +streaming-iterator = "0.1.9" +tree_iterators_rs = "3.1.0" diff --git a/rustseq/src/db.rs b/rustseq/src/db.rs new file mode 100644 index 0000000..660b8a6 --- /dev/null +++ b/rustseq/src/db.rs @@ -0,0 +1,140 @@ +use std::result; + +use rusqlite; + +pub struct DB { + connection: rusqlite::Connection +} + +impl DB { + pub fn connect(path: &str) -> Self { + let connection = rusqlite::Connection::open(path).expect("failed to open DB"); + DB { + connection: connection + } + } + + pub fn create_tables(&self) { + self.connection.execute_batch( + "CREATE TABLE blocks ( + id INTEGER NOT NULL UNIQUE, + content TEXT, + parent INTEGER, + next_sibling INTEGER, + page INTEGER, + PRIMARY KEY(id AUTOINCREMENT), + FOREIGN KEY(next_sibling) REFERENCES blocks(id) ON DELETE SET NULL, + FOREIGN KEY(parent) REFERENCES blocks(id) ON DELETE SET NULL + FOREIGN KEY(page) REFERENCES pages(id) ON DELETE SET NULL); + + CREATE TABLE pages ( + id INTEGER NOT NULL UNIQUE, + title TEXT, + first_block INTEGER, + PRIMARY KEY(id AUTOINCREMENT)); + ").expect("failed to create table"); + } + + /// insert a new block into the database, and get back its ID + pub fn insert_block(&mut self, new_block: &mut BlockRow) -> Result { + + let transaction = self.connection.transaction()?; + + transaction.execute("INSERT INTO blocks (content, parent, next_sibling, page) VALUES (?1, ?2, ?3, ?4);", + (&new_block.content, &new_block.parent_id, &new_block.sibling_id, &new_block.page_id))?; + + new_block.id = Some(transaction.last_insert_rowid()); + transaction.commit()?; + + Ok(new_block.id.expect("no block ID")) + } + + pub fn update_block(&mut self, updated_block: &BlockRow) -> Result<(), rusqlite::Error> { + let transaction = self.connection.transaction()?; + + transaction.execute("UPDATE blocks SET content=?1, parent=?2, next_sibling=?3, page=?4 WHERE id=?5", + (&updated_block.content, updated_block.parent_id, updated_block.sibling_id, updated_block.page_id, updated_block.id))?; + + transaction.commit()?; + + return Ok(()); + } + + /// insert a new page into the database, and get back its ID + pub fn insert_page(&mut self, new_page: &mut PageRow) -> Result { + let transaction = self.connection.transaction()?; + + transaction.execute("INSERT INTO pages (title, first_block) VALUES (?1, ?2);", + (&new_page.title, &new_page.root_block_id))?; + + new_page.id = Some(transaction.last_insert_rowid()); + + transaction.commit()?; + Ok(new_page.id.expect("no page ID")) + } + + pub fn update_page(&mut self, updated_page: &PageRow) -> Result<(), rusqlite::Error> { + let transaction = self.connection.transaction()?; + + transaction.execute("UPDATE blocks SET title=?1, first_block=?2 WHERE id=?3", + (&updated_page.title, &updated_page.root_block_id))?; + + transaction.commit()?; + + return Ok(()); + } + + pub fn get_page_blocks(&mut self, page: &PageRow) -> Result, rusqlite::Error> { + let transaction = self.connection.transaction()?; + + let mut statement = transaction.prepare("SELECT * FROM blocks WHERE page=?1")?; + + let rows = statement.query_map((page.id.unwrap(),), |row| { + Ok(BlockRow{ + id: row.get(0)?, + content: row.get(1)?, + parent_id: row.get(2)?, + sibling_id: row.get(3)?, + page_id: row.get(4)? + }) + })?; + + let mut row_block_vector = Vec::new(); + + for row in rows { + row_block_vector.push(row.unwrap()); + } + + return Ok(row_block_vector); + + } +} + +/// a BlockRow is a direct translation of a block row from the +/// database. It will be used to create a tree of Block objects +/// in memory. +#[derive(Debug)] +pub struct BlockRow { + pub id: Option, + pub content: String, + + pub parent_id: Option, + pub sibling_id: Option, + + pub page_id: Option +} + +impl BlockRow { + pub fn new(content: &str, parent_id: Option, sibling_id: Option, page_id: Option) -> Self { + BlockRow { id: None, content: String::from(content), parent_id: parent_id, sibling_id: sibling_id, page_id: page_id } + } +} + +#[derive(Debug)] +pub struct PageRow { + pub id: Option, + pub title: String, + + pub root_block_id: Option +} + diff --git a/rustseq/src/main.rs b/rustseq/src/main.rs new file mode 100644 index 0000000..dc95710 --- /dev/null +++ b/rustseq/src/main.rs @@ -0,0 +1,68 @@ +mod page; +mod db; + +use std::fs; + +use db::{DB, PageRow, BlockRow}; +use page::Page; + +const TEST_DB_PATH: &str = "test_db.sqlite"; + +fn init_test_db() -> DB { + // delete the existing test db + fs::remove_file(TEST_DB_PATH).unwrap_or_default(); + + // connect to the DB which creates the file + let database = DB::connect(TEST_DB_PATH); + + // run SQL to create the tables defined for rustseq + database.create_tables(); + + return database; +} + +fn main() { + let mut database = init_test_db(); + + // put a page in the database + let mut page_row = PageRow{id: None, title: String::from("test page"), root_block_id: None}; + database.insert_page(&mut page_row).unwrap(); + + // put some blocks in the database + let mut block1 = BlockRow::new("Hello World", None, None, page_row.id); + let mut block2 = BlockRow::new("This is a child block", None, None, page_row.id); + let mut block3 = BlockRow::new("this is a sibling", None, None, page_row.id); + let mut block4 = BlockRow::new("this is a sub-sibling", None, None, page_row.id); + + + database.insert_block(&mut block1).unwrap(); + database.insert_block(&mut block2).unwrap(); + database.insert_block(&mut block3).unwrap(); + database.insert_block(&mut block4).unwrap(); + + block2.parent_id = block1.id; + block3.parent_id = block1.id; + block4.parent_id = block3.id; + + // 1 + // / \ + // 2 3 + // / + // 4 + + database.update_block(&block2).unwrap(); + database.update_block(&block3).unwrap(); + database.update_block(&block4).unwrap(); + + // get the page's blocks from the database + let page_blocks = database.get_page_blocks(&page_row).unwrap(); + + println!("{:#?}", page_blocks); + + // put those blocks into the Page tree structure + let mut internal_page = Page::new(page_row, page_blocks); + internal_page.build_tree(); + // internal_page.set_root_block(internal_page.get_block_data_ref()[0].id); + internal_page.print_tree(); + +} diff --git a/rustseq/src/page.rs b/rustseq/src/page.rs new file mode 100644 index 0000000..b5d0585 --- /dev/null +++ b/rustseq/src/page.rs @@ -0,0 +1,244 @@ + +use std::{collections::HashMap}; + + +use tree_iterators_rs::prelude::*; +use streaming_iterator::StreamingIteratorMut; + +use crate::db::{BlockRow, PageRow}; + +/// A page is represented by a page object from the +/// database, and a tree of block objects with the data +/// for each node contained in the block_data `HashMap` +#[derive(Debug)] +pub struct Page { + page_data: PageRow, + block_data: HashMap, + block_tree: Tree, +} + +impl Page { + pub fn new(page_row: PageRow, block_data: Vec) -> Self { + + let mut block_map = HashMap::new(); + + for block in block_data.into_iter() { + block_map.insert(block.id.unwrap(), block); + } + + Page { + page_data: page_row, + block_data: block_map, + block_tree: Tree { + value: 0, + children: Vec::new() + } + } + } + + /// create a tree of `Tree` nodes representing the blocks of the page. + /// Each node contains the ID of a block and a vector of child `Tree` + /// nodes. + pub fn build_tree(&mut self) { + + let leaf_block_ids = self.get_leaves(); + + // key is tree parent ID, value is the subtree itself + let mut leaves: Vec> = Vec::new(); + + // each leaf block becomes a Tree node owned by the leaves vector + for block_id in leaf_block_ids { + leaves.push( + Tree { + value: block_id, + children: Vec::new() + } + ); + } + + // vector of complete subtrees + let mut subtrees: Vec> = Vec::new(); + + // store which subtree each block's node is in + // key: block id, value: subtree vector index + let mut known_nodes: HashMap = HashMap::new(); + + // OK, now try for each leaf getting to the root before going to another leaf + // subtrees from `subtrees` will be joined into new subtrees here + // attach subtrees to their parents up until they reach the root + + // Take the leaf from the vector + for (subtree_index, leaf) in leaves.into_iter().enumerate() { + // go from the leaf until reaching the root or until reaching an already visited node + let mut current_node_opt: Option> = Some(leaf); + + while let Some(ref current_node) = current_node_opt { + // build the subtree up until reaching the root or a known node + + // store the current node value in the known_nodes map + known_nodes.insert(current_node.value, subtree_index); + + // get the block data of the current node + let current_node_block_data = self.block_data.get(¤t_node.value).expect("the current node must refer to a block in block_data"); + + // the parent ID is a value, and if not it's a child of the root node + let parent_id_opt = current_node_block_data.parent_id; + + match parent_id_opt { + Some(parent_id) => { + // we are not yet at the root node + // is the parent node a known node? + if let Some(known_node_subtree_index) = known_nodes.get(&parent_id) { + // iterate the indicated subtree to find the node, append the current node + // to its children and break + let known_node_subtree = subtrees.get_mut(*known_node_subtree_index).expect("if it's a known node, the subtree must exist in subtrees"); + let mut known_node_iter = known_node_subtree.dfs_preorder_iter_mut().attach_context(); + + while let Some(context) = known_node_iter.next_mut() { + + // the context ancestors field includes the current node's value + let current_node_value = context.ancestors().last().unwrap(); + if **current_node_value != parent_id { continue; } + + let children = context.children_mut(); + children.push(current_node.clone()); + current_node_opt = None; + break; + } + + } else { + // create the parent node, update current_node and continue + let parent_node = Tree { + value: parent_id, + children: vec![current_node.clone()] + }; + + current_node_opt = Some(parent_node); + } + } + None => { + // the current node is a child of the root node + // store this subtree in subtrees + subtrees.push(current_node.clone()); + break; + } + + } + } + } + //make all final subtrees the children of a single 0-value node + let mut root_node = Tree{value: 0, children: Vec::with_capacity(subtrees.len())}; + for subtree in subtrees.into_iter() { + root_node.children.push(subtree); + } + let unsorted_tree = root_node; + // self.block_tree = self.sort_children(unsorted_tree); + self.block_tree = unsorted_tree; + } + + /// traverse the tree depth-first and print the value of each node along the way + pub fn print_tree(&self) { + let nodes: Vec<&i64> = self.block_tree.dfs_preorder_iter().collect(); + println!("{nodes:?}"); + } + + /// find the leaf nodes for the tree. + /// these are the blocks which no other block calls parent + fn get_leaves(&self) -> Vec { + // hash map containing child block ID vectors for each parent ID + let mut blocks_by_parent: HashMap> = HashMap::new(); + + for (block_id, block) in self.block_data.iter() { + let parent_id = block.parent_id.unwrap_or(0); + if let None = blocks_by_parent.get_mut(&parent_id) { + blocks_by_parent.insert(parent_id, Vec::new()); + } + blocks_by_parent.get_mut(&parent_id).unwrap().push(*block_id); + } + + let mut leaf_block_ids = Vec::new(); + + for (block_id, _block) in self.block_data.iter() { + // check that no block has this block as a parent + if let None = blocks_by_parent.get(block_id) { + leaf_block_ids.push(*block_id) + } + } + + return leaf_block_ids; + } + + /// iterate through the built tree and sort child nodes by their + /// next_sibling fields + /// return a properly sorted tree + fn sort_children(&self, mut built_tree: Tree) -> Tree { + let mut tree_iter = built_tree.dfs_preorder_iter_mut().attach_context(); + + // for each node in the tree, sort its children + while let Some(node) = tree_iter.next_mut() { + let children = node.children_mut(); + let mut sorted_children = Vec::new(); + // find the child with no next sibling + let last_sibling = children.iter().find(|node| { + self.block_data.get(&node.value).unwrap().sibling_id.is_none() + }); + sorted_children.push(last_sibling); + } + + Tree{value: 0, children:Vec::new()} + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + + fn setup_tree() -> Tree { + Tree{value: 1, children: vec![ + Tree{value: 2, children: vec![]}, + Tree{value: 3, children: vec![ + Tree{value: 4, children: vec![]} + ]} + ]} + } + + fn setup_blocks() -> Vec { + vec![ + BlockRow{id: Some(1), content: String::from("I'm block 1"), parent_id: None, sibling_id: None, page_id: None}, + BlockRow{id: Some(2), content: String::from("I'm block 2"), parent_id: Some(1), sibling_id: Some(3), page_id: None}, + BlockRow{id: Some(3), content: String::from("I'm block 3"), parent_id: Some(1), sibling_id: None, page_id: None}, + BlockRow{id: Some(4), content: String::from("I'm block 4"), parent_id: Some(3), sibling_id: None, page_id: None}, + BlockRow{id: Some(5), content: String::from("I'm block 5"), parent_id: None, sibling_id: Some(1), page_id: None} + ] + } + + fn setup_page() -> Page { + Page::new(PageRow{id:None, title:String::from(""), root_block_id: None}, setup_blocks()) + } + + fn iterate_tree(page: &Page) -> Vec { + let tree = page.block_tree.clone(); + tree.dfs_preorder().collect() + } + + #[test] + fn all_nodes_in_tree() { + let mut page = setup_page(); + page.build_tree(); + + // check the length of the iterated tree is equal to the number of blocks plus one for the root node + let iterated_tree = iterate_tree(&page); + assert_eq!(page.block_data.len() + 1, iterated_tree.len()) + } + + #[test] + fn tree_siblings_sorted() { + let mut page = setup_page(); + page.build_tree(); + + let iterated_tree = iterate_tree(&page); + assert_eq!(vec![0, 5, 1, 2, 3, 4], iterated_tree); + } +} \ No newline at end of file