diff --git a/Cargo.lock b/Cargo.lock index e2d1bde95..4e2937414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,6 +733,8 @@ name = "brk_oracle" version = "0.11.2" dependencies = [ "brk_indexer", + "brk_reader", + "brk_rpc", "brk_types", "serde_json", "vecdb", diff --git a/crates/brk_cli/src/main.rs b/crates/brk_cli/src/main.rs index e2bf0fc12..40280a5f1 100644 --- a/crates/brk_cli/src/main.rs +++ b/crates/brk_cli/src/main.rs @@ -36,23 +36,23 @@ pub fn main() -> anyhow::Result<()> { let reader = Reader::new(config.blocksdir(), &client); - let mut indexer = Indexer::forced_import(&config.brkdir())?; + let mut indexer = Indexer::import(&config.brkdir(), &reader)?; #[cfg(not(debug_assertions))] { // Pre-run indexer if too far behind, then drop and reimport to reduce memory let chain_height = client.get_last_height()?; - let indexed_height = indexer.vecs.next_height(); + let indexed_height = indexer.vecs().next_height(); let blocks_behind = chain_height.saturating_sub(*indexed_height); if blocks_behind > 10_000 { info!("---"); info!("Indexing {blocks_behind} blocks before starting server..."); info!("---"); sleep(Duration::from_secs(10)); - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; drop(indexer); Mimalloc::collect(); - indexer = Indexer::forced_import(&config.brkdir())?; + indexer = Indexer::import(&config.brkdir(), &reader)?; } } @@ -60,7 +60,7 @@ pub fn main() -> anyhow::Result<()> { let mempool = Mempool::new(&client); - let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool.clone())); + let query = AsyncQuery::build(&indexer, &computer, Some(mempool.clone())); let mempool_clone = mempool.clone(); let resolver = query.sync(|q| q.indexer_prevout_resolver()); @@ -104,9 +104,9 @@ pub fn main() -> anyhow::Result<()> { let total_start = Instant::now(); if cfg!(debug_assertions) { - indexer.checked_index(&reader, &client, &exit)?; + indexer.checked_index(&exit)?; } else { - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; } Mimalloc::collect(); diff --git a/crates/brk_cohort/src/age_range.rs b/crates/brk_cohort/src/age_range.rs index ba4c2b056..f6267f0df 100644 --- a/crates/brk_cohort/src/age_range.rs +++ b/crates/brk_cohort/src/age_range.rs @@ -4,6 +4,7 @@ use brk_traversable::Traversable; use brk_types::Age; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde::Serialize; +use vecdb::{ColumnId, VecValue, Version}; use super::{CohortName, Filter, TimeFilter}; @@ -33,6 +34,135 @@ pub const HOURS_12Y: usize = 24 * 12 * 365; pub const HOURS_15Y: usize = 24 * 15 * 365; pub const AGE_RANGE_COUNT: usize = 23; +pub const STH_AGE_RANGE_COUNT: usize = 8; +pub const LTH_AGE_RANGE_COUNT: usize = AGE_RANGE_COUNT - STH_AGE_RANGE_COUNT; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum AgeRangeId { + Under1H, + From1HTo1D, + From1DTo1W, + From1WTo1M, + From1MTo2M, + From2MTo3M, + From3MTo4M, + From4MTo5M, + From5MTo6M, + From6MTo9M, + From9MTo1Y, + From1YTo18M, + From18MTo2Y, + From2YTo3Y, + From3YTo4Y, + From4YTo5Y, + From5YTo6Y, + From6YTo7Y, + From7YTo8Y, + From8YTo10Y, + From10YTo12Y, + From12YTo15Y, + Over15Y, +} + +pub const AGE_RANGE_IDS: [AgeRangeId; AGE_RANGE_COUNT] = [ + AgeRangeId::Under1H, + AgeRangeId::From1HTo1D, + AgeRangeId::From1DTo1W, + AgeRangeId::From1WTo1M, + AgeRangeId::From1MTo2M, + AgeRangeId::From2MTo3M, + AgeRangeId::From3MTo4M, + AgeRangeId::From4MTo5M, + AgeRangeId::From5MTo6M, + AgeRangeId::From6MTo9M, + AgeRangeId::From9MTo1Y, + AgeRangeId::From1YTo18M, + AgeRangeId::From18MTo2Y, + AgeRangeId::From2YTo3Y, + AgeRangeId::From3YTo4Y, + AgeRangeId::From4YTo5Y, + AgeRangeId::From5YTo6Y, + AgeRangeId::From6YTo7Y, + AgeRangeId::From7YTo8Y, + AgeRangeId::From8YTo10Y, + AgeRangeId::From10YTo12Y, + AgeRangeId::From12YTo15Y, + AgeRangeId::Over15Y, +]; + +pub const STH_AGE_RANGE_IDS: [AgeRangeId; STH_AGE_RANGE_COUNT] = [ + AgeRangeId::Under1H, + AgeRangeId::From1HTo1D, + AgeRangeId::From1DTo1W, + AgeRangeId::From1WTo1M, + AgeRangeId::From1MTo2M, + AgeRangeId::From2MTo3M, + AgeRangeId::From3MTo4M, + AgeRangeId::From4MTo5M, +]; + +pub const LTH_AGE_RANGE_IDS: [AgeRangeId; LTH_AGE_RANGE_COUNT] = [ + AgeRangeId::From5MTo6M, + AgeRangeId::From6MTo9M, + AgeRangeId::From9MTo1Y, + AgeRangeId::From1YTo18M, + AgeRangeId::From18MTo2Y, + AgeRangeId::From2YTo3Y, + AgeRangeId::From3YTo4Y, + AgeRangeId::From4YTo5Y, + AgeRangeId::From5YTo6Y, + AgeRangeId::From6YTo7Y, + AgeRangeId::From7YTo8Y, + AgeRangeId::From8YTo10Y, + AgeRangeId::From10YTo12Y, + AgeRangeId::From12YTo15Y, + AgeRangeId::Over15Y, +]; + +impl ColumnId for AgeRangeId { + type Row + = [T; AGE_RANGE_COUNT] + where + T: VecValue; + + const VERSION: Version = Version::ONE; + const ALL: &'static [Self] = &AGE_RANGE_IDS; + + #[inline] + fn index(self) -> usize { + self as usize + } + + #[inline] + fn get(self, row: &Self::Row) -> &T { + &row[self as usize] + } + + #[inline] + fn get_mut(self, row: &mut Self::Row) -> &mut T { + &mut row[self as usize] + } + + #[inline] + fn from_fn(mut f: F) -> Self::Row + where + T: VecValue, + F: FnMut(Self) -> T, + { + std::array::from_fn(|index| f(AGE_RANGE_IDS[index])) + } + + #[inline] + fn map(row: Self::Row, f: F) -> Self::Row + where + T: VecValue, + U: VecValue, + F: FnMut(T) -> U, + { + row.map(f) + } +} /// Age boundaries in hours. Defines the cohort ranges: /// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞) @@ -444,6 +574,31 @@ mod tests { } } + #[test] + fn column_ids_match_storage_order_and_term_split() { + assert_eq!(AgeRangeId::ALL, &AGE_RANGE_IDS); + assert_eq!( + STH_AGE_RANGE_IDS.len() + LTH_AGE_RANGE_IDS.len(), + AGE_RANGE_COUNT + ); + assert_eq!( + STH_AGE_RANGE_IDS.as_slice(), + &AGE_RANGE_IDS[..STH_AGE_RANGE_COUNT] + ); + assert_eq!( + LTH_AGE_RANGE_IDS.as_slice(), + &AGE_RANGE_IDS[STH_AGE_RANGE_COUNT..] + ); + assert_eq!(STH_AGE_RANGE_IDS.last(), Some(&AgeRangeId::From4MTo5M)); + assert_eq!(LTH_AGE_RANGE_IDS.first(), Some(&AgeRangeId::From5MTo6M)); + + let row = AgeRangeId::from_fn(|column| column.index()); + for (index, &column) in AGE_RANGE_IDS.iter().enumerate() { + assert_eq!(column.index(), index); + assert_eq!(*column.get(&row), index); + } + } + #[test] fn split_range_names_and_boundaries_match() { assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M); diff --git a/crates/brk_computer/examples/computer.rs b/crates/brk_computer/examples/computer.rs index e1fdb84b9..ec482680c 100644 --- a/crates/brk_computer/examples/computer.rs +++ b/crates/brk_computer/examples/computer.rs @@ -30,26 +30,26 @@ pub fn main() -> color_eyre::Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; let exit = Exit::new(); exit.set_ctrlc_handler(); // Pre-run indexer if too far behind, then drop and reimport to reduce memory let chain_height = client.get_last_height()?; - let indexed_height = indexer.vecs.next_height(); + let indexed_height = indexer.vecs().next_height(); if u32::from(chain_height).saturating_sub(u32::from(indexed_height)) > 1000 { - indexer.checked_index(&reader, &client, &exit)?; + indexer.checked_index(&exit)?; drop(indexer); Mimalloc::collect(); - indexer = Indexer::forced_import(&outputs_dir)?; + indexer = Indexer::import(&outputs_dir, &reader)?; } let mut computer = Computer::forced_import(&outputs_dir, &indexer)?; loop { let i = Instant::now(); - indexer.checked_index(&reader, &client, &exit)?; + indexer.checked_index(&exit)?; Mimalloc::collect(); diff --git a/crates/brk_computer/examples/computer_bench.rs b/crates/brk_computer/examples/computer_bench.rs index a6dda94db..17942ed4c 100644 --- a/crates/brk_computer/examples/computer_bench.rs +++ b/crates/brk_computer/examples/computer_bench.rs @@ -27,7 +27,7 @@ pub fn main() -> Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; let mut computer = Computer::forced_import(&outputs_benches_dir, &indexer)?; @@ -44,7 +44,7 @@ pub fn main() -> Result<()> { }); let i = Instant::now(); - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; info!("Done in {:?}", i.elapsed()); Mimalloc::collect(); diff --git a/crates/brk_computer/examples/computer_read.rs b/crates/brk_computer/examples/computer_read.rs index 126333947..4180ec235 100644 --- a/crates/brk_computer/examples/computer_read.rs +++ b/crates/brk_computer/examples/computer_read.rs @@ -3,6 +3,8 @@ use std::{env, path::Path, time::Instant}; use brk_computer::Computer; use brk_error::Result; use brk_indexer::Indexer; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use vecdb::{AnySerializableVec, AnyVec}; pub fn main() -> Result<()> { @@ -10,7 +12,13 @@ pub fn main() -> Result<()> { let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); - let indexer = Indexer::forced_import(&outputs_dir)?; + let bitcoin_dir = Client::default_bitcoin_path(); + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + )?; + let reader = Reader::new(bitcoin_dir.join("blocks"), &client); + let indexer = Indexer::import(&outputs_dir, &reader)?; let computer = Computer::forced_import(&outputs_dir, &indexer)?; diff --git a/crates/brk_computer/examples/computer_tree.rs b/crates/brk_computer/examples/computer_tree.rs index f25278399..02c03c00c 100644 --- a/crates/brk_computer/examples/computer_tree.rs +++ b/crates/brk_computer/examples/computer_tree.rs @@ -2,6 +2,8 @@ use std::{env, fs, path::Path}; use brk_computer::Computer; use brk_indexer::Indexer; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use brk_traversable::{Traversable, TreeNode}; pub fn main() -> color_eyre::Result<()> { @@ -10,12 +12,14 @@ pub fn main() -> color_eyre::Result<()> { let tmp = env::temp_dir().join("brk_tree_gen"); fs::create_dir_all(&tmp)?; - let indexer = Indexer::forced_import(&tmp)?; + let client = Client::new("http://127.0.0.1:1", Auth::None)?; + let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client); + let indexer = Indexer::import(&tmp, &reader)?; let computer = Computer::forced_import(&tmp, &indexer)?; let tree = TreeNode::Branch( [ - ("indexed".to_string(), indexer.vecs.to_tree_node()), + ("indexed".to_string(), indexer.vecs().to_tree_node()), ("computed".to_string(), computer.to_tree_node()), ] .into_iter() diff --git a/crates/brk_computer/examples/full_bench.rs b/crates/brk_computer/examples/full_bench.rs index b75350c50..0e988ed5e 100644 --- a/crates/brk_computer/examples/full_bench.rs +++ b/crates/brk_computer/examples/full_bench.rs @@ -44,23 +44,23 @@ pub fn main() -> color_eyre::Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; // Pre-run indexer if too far behind, then drop and reimport to reduce memory let chain_height = client.get_last_height()?; - let indexed_height = indexer.vecs.next_height(); + let indexed_height = indexer.vecs().next_height(); if chain_height.saturating_sub(*indexed_height) > 1000 { - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; drop(indexer); Mimalloc::collect(); - indexer = Indexer::forced_import(&outputs_dir)?; + indexer = Indexer::import(&outputs_dir, &reader)?; } let mut computer = Computer::forced_import(&outputs_dir, &indexer)?; loop { let i = Instant::now(); - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; info!("Done in {:?}", i.elapsed()); Mimalloc::collect(); diff --git a/crates/brk_computer/src/blocks/count/import.rs b/crates/brk_computer/src/blocks/count/import.rs index 62fdd43ce..2db513784 100644 --- a/crates/brk_computer/src/blocks/count/import.rs +++ b/crates/brk_computer/src/blocks/count/import.rs @@ -47,7 +47,7 @@ impl Vecs { total: LazyPerBlockCumulativeRolling::from_indexed_source( "block_count", version + Version::ONE, - &indexer.vecs.blocks.weight, + &indexer.vecs().blocks.weight, cumulative_block_count, cached_starts, indexes, diff --git a/crates/brk_computer/src/blocks/difficulty/import.rs b/crates/brk_computer/src/blocks/difficulty/import.rs index e3c00ac3b..66b24072d 100644 --- a/crates/brk_computer/src/blocks/difficulty/import.rs +++ b/crates/brk_computer/src/blocks/difficulty/import.rs @@ -36,7 +36,7 @@ impl Vecs { let hashrate = LazyPerBlock::from_height_source::( "difficulty_hashrate", version, - indexer.vecs.blocks.difficulty.read_only_clone(), + indexer.vecs().blocks.difficulty.read_only_clone(), indexes, ); @@ -63,7 +63,7 @@ impl Vecs { Self { value: Resolutions::forced_import( "difficulty", - indexer.vecs.blocks.difficulty.read_only_clone(), + indexer.vecs().blocks.difficulty.read_only_clone(), version, indexes, ), @@ -71,7 +71,7 @@ impl Vecs { adjustment: LazyPercentPerBlock::from_lookback_source( "difficulty_adjustment", version + Version::ONE, - &indexer.vecs.blocks.difficulty, + &indexer.vecs().blocks.difficulty, DIFFICULTY_ADJUSTMENT_LOOKBACK, difficulty_adjustment, indexes, diff --git a/crates/brk_computer/src/blocks/interval/compute.rs b/crates/brk_computer/src/blocks/interval/compute.rs index e3123c208..8326a1ce9 100644 --- a/crates/brk_computer/src/blocks/interval/compute.rs +++ b/crates/brk_computer/src/blocks/interval/compute.rs @@ -11,12 +11,12 @@ impl Vecs { let mut prev_timestamp = None; self.0.compute_from( starting_height, - &indexer.vecs.blocks.timestamp, + &indexer.vecs().blocks.timestamp, |height, timestamp| { let interval = if let Some(previous_height) = height.decremented() { let previous = prev_timestamp.unwrap_or_else(|| { indexer - .vecs + .vecs() .blocks .timestamp .collect_one(previous_height) diff --git a/crates/brk_computer/src/blocks/size/compute.rs b/crates/brk_computer/src/blocks/size/compute.rs index d16029164..21e5124eb 100644 --- a/crates/brk_computer/src/blocks/size/compute.rs +++ b/crates/brk_computer/src/blocks/size/compute.rs @@ -20,7 +20,7 @@ impl Vecs { self.size.compute( starting_height, &window_starts, - &indexer.vecs.blocks.total, + &indexer.vecs().blocks.total, exit, )?; diff --git a/crates/brk_computer/src/blocks/size/import.rs b/crates/brk_computer/src/blocks/size/import.rs index 841e79a25..336810037 100644 --- a/crates/brk_computer/src/blocks/size/import.rs +++ b/crates/brk_computer/src/blocks/size/import.rs @@ -26,7 +26,7 @@ impl Vecs { db, "block_vbytes", version, - &indexer.vecs.blocks.weight, + &indexer.vecs().blocks.weight, block_vbytes, indexes, cached_starts, diff --git a/crates/brk_computer/src/blocks/weight/import.rs b/crates/brk_computer/src/blocks/weight/import.rs index c886b0498..33e99120a 100644 --- a/crates/brk_computer/src/blocks/weight/import.rs +++ b/crates/brk_computer/src/blocks/weight/import.rs @@ -34,7 +34,7 @@ impl Vecs { let fullness = LazyPercentVec::from_indexed_source( "block_fullness", version, - &indexer.vecs.blocks.weight, + &indexer.vecs().blocks.weight, block_fullness, ); diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index 1e29f020f..58a3454cd 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -63,9 +63,9 @@ pub(crate) fn process_blocks( return Ok(()); } - let height_to_first_tx_index = &indexer.vecs.transactions.first_tx_index; - let height_to_first_txout_index = &indexer.vecs.outputs.first_txout_index; - let height_to_first_txin_index = &indexer.vecs.inputs.first_txin_index; + let height_to_first_tx_index = &indexer.vecs().transactions.first_tx_index; + let height_to_first_txout_index = &indexer.vecs().outputs.first_txout_index; + let height_to_first_txin_index = &indexer.vecs().inputs.first_txin_index; let height_to_tx_count = &transactions.count.total.block; let height_to_output_count = &outputs.count.total.sum; let height_to_input_count = &inputs.count.sum; @@ -106,7 +106,7 @@ pub(crate) fn process_blocks( debug!("VecsReaders created"); // Extend tx_index_to_height RangeMap with new entries (incremental, O(new_blocks)) - let target_len = indexer.vecs.transactions.first_tx_index.len(); + let target_len = indexer.vecs().transactions.first_tx_index.len(); let current_len = tx_index_to_height.len(); if current_len < target_len { debug!( @@ -114,7 +114,7 @@ pub(crate) fn process_blocks( current_len, target_len ); let new_entries: Vec = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(current_len, target_len); @@ -141,49 +141,49 @@ pub(crate) fn process_blocks( // Pre-collect first address indexes per type for the block range let first_p2a_vec = indexer - .vecs + .vecs() .addrs .p2a .first_index .collect_range_at(start_usize, end_usize); let first_p2pk33_vec = indexer - .vecs + .vecs() .addrs .p2pk33 .first_index .collect_range_at(start_usize, end_usize); let first_p2pk65_vec = indexer - .vecs + .vecs() .addrs .p2pk65 .first_index .collect_range_at(start_usize, end_usize); let first_p2pkh_vec = indexer - .vecs + .vecs() .addrs .p2pkh .first_index .collect_range_at(start_usize, end_usize); let first_p2sh_vec = indexer - .vecs + .vecs() .addrs .p2sh .first_index .collect_range_at(start_usize, end_usize); let first_p2tr_vec = indexer - .vecs + .vecs() .addrs .p2tr .first_index .collect_range_at(start_usize, end_usize); let first_p2wpkh_vec = indexer - .vecs + .vecs() .addrs .p2wpkh .first_index .collect_range_at(start_usize, end_usize); let first_p2wsh_vec = indexer - .vecs + .vecs() .addrs .p2wsh .first_index diff --git a/crates/brk_computer/src/distribution/compute/readers.rs b/crates/brk_computer/src/distribution/compute/readers.rs index f17a8151b..325592236 100644 --- a/crates/brk_computer/src/distribution/compute/readers.rs +++ b/crates/brk_computer/src/distribution/compute/readers.rs @@ -46,21 +46,21 @@ impl<'a> TxOutReaders<'a> { output_count: usize, ) -> &[TxOutData] { let end = first_txout_index + output_count; - self.indexer.vecs.outputs.value.collect_range_into_at( + self.indexer.vecs().outputs.value.collect_range_into_at( first_txout_index, end, &mut self.values_buf, ); - self.indexer.vecs.outputs.output_type.collect_range_into_at( - first_txout_index, - end, - &mut self.output_types_buf, - ); - self.indexer.vecs.outputs.type_index.collect_range_into_at( - first_txout_index, - end, - &mut self.type_indexes_buf, - ); + self.indexer + .vecs() + .outputs + .output_type + .collect_range_into_at(first_txout_index, end, &mut self.output_types_buf); + self.indexer + .vecs() + .outputs + .type_index + .collect_range_into_at(first_txout_index, end, &mut self.type_indexes_buf); self.txout_data_buf.clear(); self.txout_data_buf.extend( @@ -118,17 +118,17 @@ impl<'a> TxInReaders<'a> { let end = first_txin_index + input_count; self.input_values .collect_range_into_at(first_txin_index, end, &mut self.values_buf); - self.indexer.vecs.inputs.outpoint.collect_range_into_at( + self.indexer.vecs().inputs.outpoint.collect_range_into_at( first_txin_index, end, &mut self.outpoints_buf, ); - self.indexer.vecs.inputs.output_type.collect_range_into_at( - first_txin_index, - end, - &mut self.output_types_buf, - ); - self.indexer.vecs.inputs.type_index.collect_range_into_at( + self.indexer + .vecs() + .inputs + .output_type + .collect_range_into_at(first_txin_index, end, &mut self.output_types_buf); + self.indexer.vecs().inputs.type_index.collect_range_into_at( first_txin_index, end, &mut self.type_indexes_buf, diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index da801421e..bac037ef3 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -383,29 +383,29 @@ impl Vecs { + [ prices.spot.cents.height.version(), indexes.timestamp.monotonic.version(), - indexer.vecs.transactions.first_tx_index.version(), - indexer.vecs.outputs.first_txout_index.version(), - indexer.vecs.inputs.first_txin_index.version(), + indexer.vecs().transactions.first_tx_index.version(), + indexer.vecs().outputs.first_txout_index.version(), + indexer.vecs().inputs.first_txin_index.version(), transactions.count.total.block.version(), outputs.count.total.sum.version(), inputs.count.sum.version(), indexes.tx_index.output_count.version(), indexes.tx_index.input_count.version(), - indexer.vecs.outputs.value.version(), - indexer.vecs.outputs.output_type.version(), - indexer.vecs.outputs.type_index.version(), + indexer.vecs().outputs.value.version(), + indexer.vecs().outputs.output_type.version(), + indexer.vecs().outputs.type_index.version(), inputs.value.version(), - indexer.vecs.inputs.outpoint.version(), - indexer.vecs.inputs.output_type.version(), - indexer.vecs.inputs.type_index.version(), - indexer.vecs.addrs.p2pk65.first_index.version(), - indexer.vecs.addrs.p2pk33.first_index.version(), - indexer.vecs.addrs.p2pkh.first_index.version(), - indexer.vecs.addrs.p2sh.first_index.version(), - indexer.vecs.addrs.p2wpkh.first_index.version(), - indexer.vecs.addrs.p2wsh.first_index.version(), - indexer.vecs.addrs.p2tr.first_index.version(), - indexer.vecs.addrs.p2a.first_index.version(), + indexer.vecs().inputs.outpoint.version(), + indexer.vecs().inputs.output_type.version(), + indexer.vecs().inputs.type_index.version(), + indexer.vecs().addrs.p2pk65.first_index.version(), + indexer.vecs().addrs.p2pk33.first_index.version(), + indexer.vecs().addrs.p2pkh.first_index.version(), + indexer.vecs().addrs.p2sh.first_index.version(), + indexer.vecs().addrs.p2wpkh.first_index.version(), + indexer.vecs().addrs.p2wsh.first_index.version(), + indexer.vecs().addrs.p2tr.first_index.version(), + indexer.vecs().addrs.p2a.first_index.version(), ] .into_iter() .sum::(); @@ -571,7 +571,7 @@ impl Vecs { }; // 3. Get last height from indexer - let last_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1)); + let last_height = Height::from(indexer.vecs().blocks.blockhash.len().saturating_sub(1)); debug!( "last_height={}, starting_height={}", last_height, starting_height diff --git a/crates/brk_computer/src/indexes/addr.rs b/crates/brk_computer/src/indexes/addr.rs index 2fbdc4019..174ee6a04 100644 --- a/crates/brk_computer/src/indexes/addr.rs +++ b/crates/brk_computer/src/indexes/addr.rs @@ -99,13 +99,13 @@ impl Vecs { identity: LazyVec::init( "p2pk33_addr_index", version, - indexer.vecs.addrs.p2pk33.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pk33.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2pk33_addr", version, - indexer.vecs.addrs.p2pk33.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pk33.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -113,13 +113,13 @@ impl Vecs { identity: LazyVec::init( "p2pk65_addr_index", version, - indexer.vecs.addrs.p2pk65.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pk65.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2pk65_addr", version, - indexer.vecs.addrs.p2pk65.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pk65.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -127,13 +127,13 @@ impl Vecs { identity: LazyVec::init( "p2pkh_addr_index", version, - indexer.vecs.addrs.p2pkh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pkh.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2pkh_addr", version, - indexer.vecs.addrs.p2pkh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2pkh.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -141,13 +141,13 @@ impl Vecs { identity: LazyVec::init( "p2sh_addr_index", version, - indexer.vecs.addrs.p2sh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2sh.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2sh_addr", version, - indexer.vecs.addrs.p2sh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2sh.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -155,13 +155,13 @@ impl Vecs { identity: LazyVec::init( "p2tr_addr_index", version, - indexer.vecs.addrs.p2tr.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2tr.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2tr_addr", version, - indexer.vecs.addrs.p2tr.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2tr.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -169,13 +169,13 @@ impl Vecs { identity: LazyVec::init( "p2wpkh_addr_index", version, - indexer.vecs.addrs.p2wpkh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2wpkh.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2wpkh_addr", version, - indexer.vecs.addrs.p2wpkh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2wpkh.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -183,13 +183,13 @@ impl Vecs { identity: LazyVec::init( "p2wsh_addr_index", version, - indexer.vecs.addrs.p2wsh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2wsh.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2wsh_addr", version, - indexer.vecs.addrs.p2wsh.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2wsh.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -197,13 +197,13 @@ impl Vecs { identity: LazyVec::init( "p2a_addr_index", version, - indexer.vecs.addrs.p2a.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2a.bytes.read_only_boxed_clone(), |index, _| index, ), addr: LazyVec::init( "p2a_addr", version, - indexer.vecs.addrs.p2a.bytes.read_only_boxed_clone(), + indexer.vecs().addrs.p2a.bytes.read_only_boxed_clone(), |_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(), ), }, @@ -212,7 +212,7 @@ impl Vecs { "p2ms_output_index", version, indexer - .vecs + .vecs() .scripts .p2ms .to_tx_index @@ -225,7 +225,7 @@ impl Vecs { "empty_output_index", version, indexer - .vecs + .vecs() .scripts .empty .to_tx_index @@ -238,7 +238,7 @@ impl Vecs { "unknown_output_index", version, indexer - .vecs + .vecs() .scripts .unknown .to_tx_index @@ -250,7 +250,7 @@ impl Vecs { identity: LazyVec::init( "op_return_index", version, - indexer.vecs.op_return.to_tx_index.read_only_boxed_clone(), + indexer.vecs().op_return.to_tx_index.read_only_boxed_clone(), |index, _| index, ), }, diff --git a/crates/brk_computer/src/indexes/chain_counts.rs b/crates/brk_computer/src/indexes/chain_counts.rs index 79ee848fb..b318198ad 100644 --- a/crates/brk_computer/src/indexes/chain_counts.rs +++ b/crates/brk_computer/src/indexes/chain_counts.rs @@ -20,27 +20,31 @@ impl CachedChainCounts { "tx_count_cumulative", version, indexer - .vecs + .vecs() .transactions .first_tx_index .read_only_boxed_clone(), - indexer.vecs.transactions.txid.read_only_boxed_clone(), + indexer.vecs().transactions.txid.read_only_boxed_clone(), )), input: CachedVec::wrap(LazyCumulativeIndexVec::new( "input_count_cumulative", version, - indexer.vecs.inputs.first_txin_index.read_only_boxed_clone(), - indexer.vecs.inputs.outpoint.read_only_boxed_clone(), + indexer + .vecs() + .inputs + .first_txin_index + .read_only_boxed_clone(), + indexer.vecs().inputs.outpoint.read_only_boxed_clone(), )), output: CachedVec::wrap(LazyCumulativeIndexVec::new( "output_count_cumulative", version, indexer - .vecs + .vecs() .outputs .first_txout_index .read_only_boxed_clone(), - indexer.vecs.outputs.value.read_only_boxed_clone(), + indexer.vecs().outputs.value.read_only_boxed_clone(), )), } } diff --git a/crates/brk_computer/src/indexes/mod.rs b/crates/brk_computer/src/indexes/mod.rs index 62cb3aef8..98a033090 100644 --- a/crates/brk_computer/src/indexes/mod.rs +++ b/crates/brk_computer/src/indexes/mod.rs @@ -139,7 +139,7 @@ impl Vecs { let timestamp = Timestamps::from_locals( version, monotonic, - indexer.vecs.blocks.timestamp.read_only_boxed_clone(), + indexer.vecs().blocks.timestamp.read_only_boxed_clone(), &minute10, &minute30, &hour1, @@ -192,7 +192,7 @@ impl Vecs { let starting_height = indexer.safe_lengths().height; self.tx_heights.update(indexer, starting_height); - if starting_height.to_usize() < indexer.vecs.transactions.first_tx_index.len() { + if starting_height.to_usize() < indexer.vecs().transactions.first_tx_index.len() { self.chain_counts.clear(); } diff --git a/crates/brk_computer/src/indexes/timestamp.rs b/crates/brk_computer/src/indexes/timestamp.rs index 79e64f4e4..1b5f465bd 100644 --- a/crates/brk_computer/src/indexes/timestamp.rs +++ b/crates/brk_computer/src/indexes/timestamp.rs @@ -125,7 +125,7 @@ impl Timestamps { let mut prev = None; self.monotonic.inner.compute_transform( starting_height, - &indexer.vecs.blocks.timestamp, + &indexer.vecs().blocks.timestamp, |(h, timestamp, this)| { if prev.is_none() && let Some(prev_h) = h.decremented() diff --git a/crates/brk_computer/src/indexes/tx_heights.rs b/crates/brk_computer/src/indexes/tx_heights.rs index 688c19961..a3f9d9557 100644 --- a/crates/brk_computer/src/indexes/tx_heights.rs +++ b/crates/brk_computer/src/indexes/tx_heights.rs @@ -19,10 +19,10 @@ pub struct TxHeights(Arc>>); impl TxHeights { /// Build from the full `first_tx_index` vec at startup. pub fn init(indexer: &Indexer) -> Self { - let len = indexer.vecs.transactions.first_tx_index.len(); + let len = indexer.vecs().transactions.first_tx_index.len(); let entries: Vec = if len > 0 { indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(0, len) @@ -39,11 +39,11 @@ impl TxHeights { if inner.len() > reorg_len { inner.truncate(reorg_len); } - let target_len = indexer.vecs.transactions.first_tx_index.len(); + let target_len = indexer.vecs().transactions.first_tx_index.len(); let current_len = inner.len(); if current_len < target_len { let new_entries: Vec = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(current_len, target_len); diff --git a/crates/brk_computer/src/indexes/tx_index.rs b/crates/brk_computer/src/indexes/tx_index.rs index 23b033c29..7a0058239 100644 --- a/crates/brk_computer/src/indexes/tx_index.rs +++ b/crates/brk_computer/src/indexes/tx_index.rs @@ -18,28 +18,28 @@ impl Vecs { identity: LazyVec::init( "tx_index", version, - indexer.vecs.transactions.txid.read_only_boxed_clone(), + indexer.vecs().transactions.txid.read_only_boxed_clone(), |index, _| index, ), input_count: LazyIndexCountVec::new( "input_count", version, indexer - .vecs + .vecs() .transactions .first_txin_index .read_only_boxed_clone(), - indexer.vecs.inputs.outpoint.read_only_boxed_clone(), + indexer.vecs().inputs.outpoint.read_only_boxed_clone(), ), output_count: LazyIndexCountVec::new( "output_count", version, indexer - .vecs + .vecs() .transactions .first_txout_index .read_only_boxed_clone(), - indexer.vecs.outputs.value.read_only_boxed_clone(), + indexer.vecs().outputs.value.read_only_boxed_clone(), ), } } diff --git a/crates/brk_computer/src/indexes/txin_index.rs b/crates/brk_computer/src/indexes/txin_index.rs index a89d970b7..b901ccce9 100644 --- a/crates/brk_computer/src/indexes/txin_index.rs +++ b/crates/brk_computer/src/indexes/txin_index.rs @@ -14,7 +14,7 @@ impl Vecs { identity: LazyVec::init( "txin_index", version, - indexer.vecs.inputs.outpoint.read_only_boxed_clone(), + indexer.vecs().inputs.outpoint.read_only_boxed_clone(), |index, _| index, ), } diff --git a/crates/brk_computer/src/indexes/txout_index.rs b/crates/brk_computer/src/indexes/txout_index.rs index a613cc797..a0bbdfb7b 100644 --- a/crates/brk_computer/src/indexes/txout_index.rs +++ b/crates/brk_computer/src/indexes/txout_index.rs @@ -14,7 +14,7 @@ impl Vecs { identity: LazyVec::init( "txout_index", version, - indexer.vecs.outputs.value.read_only_boxed_clone(), + indexer.vecs().outputs.value.read_only_boxed_clone(), |index, _| index, ), } diff --git a/crates/brk_computer/src/inputs/by_type/compute.rs b/crates/brk_computer/src/inputs/by_type/compute.rs index e0a104ca7..bdc30f39d 100644 --- a/crates/brk_computer/src/inputs/by_type/compute.rs +++ b/crates/brk_computer/src/inputs/by_type/compute.rs @@ -11,10 +11,10 @@ impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { let starting_lengths = indexer.safe_lengths(); - let dep_version = indexer.vecs.inputs.output_type.version() - + indexer.vecs.transactions.first_tx_index.version() - + indexer.vecs.transactions.first_txin_index.version() - + indexer.vecs.transactions.txid.version(); + let dep_version = indexer.vecs().inputs.output_type.version() + + indexer.vecs().transactions.first_tx_index.version() + + indexer.vecs().transactions.first_txin_index.version() + + indexer.vecs().transactions.txid.version(); self.input_count .validate_and_truncate(dep_version, starting_lengths.height)?; @@ -26,18 +26,18 @@ impl Vecs { .min_stateful_len() .min(self.tx_count.min_stateful_len()); - let first_tx_index = &indexer.vecs.transactions.first_tx_index; + let first_tx_index = &indexer.vecs().transactions.first_tx_index; let end = first_tx_index.len(); if skip < end { self.input_count.truncate_if_needed_at(skip)?; self.tx_count.truncate_if_needed_at(skip)?; let fi_batch = first_tx_index.collect_range_at(skip, end); - let txid_len = indexer.vecs.transactions.txid.len(); - let total_txin_len = indexer.vecs.inputs.output_type.len(); + let txid_len = indexer.vecs().transactions.txid.len(); + let total_txin_len = indexer.vecs().inputs.output_type.len(); - let mut itype_cursor = indexer.vecs.inputs.output_type.cursor(); - let mut fi_in_cursor = indexer.vecs.transactions.first_txin_index.cursor(); + let mut itype_cursor = indexer.vecs().inputs.output_type.cursor(); + let mut fi_in_cursor = indexer.vecs().transactions.first_txin_index.cursor(); let mut height = skip; walk_blocks( diff --git a/crates/brk_computer/src/inputs/value.rs b/crates/brk_computer/src/inputs/value.rs index a6269969b..6fa14ddbc 100644 --- a/crates/brk_computer/src/inputs/value.rs +++ b/crates/brk_computer/src/inputs/value.rs @@ -13,8 +13,8 @@ const BATCH_SIZE: usize = SORT_MEMORY_BUDGET / (size_of::() + size_of:: Result<()> { let starting_lengths = indexer.safe_lengths(); - let txout_indexes = &indexer.vecs.inputs.txout_index; - let dep_version = txout_indexes.version() + indexer.vecs.outputs.value.version(); + let txout_indexes = &indexer.vecs().inputs.txout_index; + let dep_version = txout_indexes.version() + indexer.vecs().outputs.value.version(); self.value.validate_computed_version_or_reset(dep_version)?; let target = txout_indexes.len(); @@ -24,7 +24,7 @@ impl Vecs { return Ok(()); } - let value_reader = indexer.vecs.outputs.value.reader(); + let value_reader = indexer.vecs().outputs.value.reader(); let mut entries = Vec::with_capacity((target - min).min(BATCH_SIZE)); let mut values = Vec::with_capacity((target - min).min(BATCH_SIZE)); diff --git a/crates/brk_computer/src/internal/per_block/computed/cached_count_cumulative_rolling.rs b/crates/brk_computer/src/internal/per_block/computed/cached_count_cumulative_rolling.rs index 47e8bd169..fec220150 100644 --- a/crates/brk_computer/src/internal/per_block/computed/cached_count_cumulative_rolling.rs +++ b/crates/brk_computer/src/internal/per_block/computed/cached_count_cumulative_rolling.rs @@ -118,6 +118,8 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use brk_indexer::Indexer; + use brk_reader::Reader; + use brk_rpc::{Auth, Client}; use brk_traversable::Traversable; use vecdb::{AnyVec, ReadableVec}; @@ -135,7 +137,9 @@ mod tests { std::process::id() )); - let indexer = Indexer::forced_import(&path).unwrap(); + let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap(); + let reader = Reader::new_without_rlimit(path.join("blocks"), &client); + let indexer = Indexer::import(&path, &reader).unwrap(); let indexes = indexes::Vecs::forced_import(&path, Version::ONE, &indexer).unwrap(); let lookback = LookbackVecs::new( Version::ONE, diff --git a/crates/brk_computer/src/internal/per_tx/derived.rs b/crates/brk_computer/src/internal/per_tx/derived.rs index 448d1a98a..6d75a4555 100644 --- a/crates/brk_computer/src/internal/per_tx/derived.rs +++ b/crates/brk_computer/src/internal/per_tx/derived.rs @@ -96,7 +96,7 @@ where self.block.compute_with_skip( starting_lengths.height, tx_index_source, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, exit, skip_count, @@ -105,7 +105,7 @@ where self.distribution._6b.compute_from_nblocks( starting_lengths.height, tx_index_source, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, 6, exit, @@ -135,7 +135,7 @@ where starting_lengths.height, tx_index_source, vsize_source, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, exit, skip_count, @@ -144,7 +144,7 @@ where self.distribution._6b.compute_from_nblocks( starting_lengths.height, tx_index_source, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, 6, exit, diff --git a/crates/brk_computer/src/mining/rewards/compute.rs b/crates/brk_computer/src/mining/rewards/compute.rs index c59e017db..03c2751d8 100644 --- a/crates/brk_computer/src/mining/rewards/compute.rs +++ b/crates/brk_computer/src/mining/rewards/compute.rs @@ -26,10 +26,10 @@ impl Vecs { self.coinbase.compute_from( starting_height, prices, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, |_, tx_index| { let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -44,7 +44,7 @@ impl Vecs { count_cursor.advance(ti - count_cursor.position()); let output_count: usize = count_cursor.next().unwrap().into(); - indexer.vecs.outputs.value.fold_range_at( + indexer.vecs().outputs.value.fold_range_at( first_txout_index, first_txout_index + output_count, Sats::ZERO, @@ -59,7 +59,7 @@ impl Vecs { starting_height, &window_starts, prices, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, &transactions.fees.fee.tx_index, exit, diff --git a/crates/brk_computer/src/mining/rewards/import.rs b/crates/brk_computer/src/mining/rewards/import.rs index c60462c3e..a8d2b90f5 100644 --- a/crates/brk_computer/src/mining/rewards/import.rs +++ b/crates/brk_computer/src/mining/rewards/import.rs @@ -22,9 +22,9 @@ impl Vecs { cached_starts: &Windows<&CachedWindowStartVec>, ) -> Result { let coinbase_version = version - + indexer.vecs.transactions.first_txout_index.version() + + indexer.vecs().transactions.first_txout_index.version() + indexes.tx_index.output_count.version() - + indexer.vecs.outputs.value.version(); + + indexer.vecs().outputs.value.version(); let coinbase = ValuePerBlockCumulativeRolling::forced_import( db, diff --git a/crates/brk_computer/src/op_return/compute.rs b/crates/brk_computer/src/op_return/compute.rs index 7846f7ed0..26b51e9fd 100644 --- a/crates/brk_computer/src/op_return/compute.rs +++ b/crates/brk_computer/src/op_return/compute.rs @@ -51,8 +51,8 @@ impl Vecs { self.db.sync_bg_tasks()?; let starting_lengths = indexer.safe_lengths(); - let raw = &indexer.vecs.op_return; - let txs = &indexer.vecs.transactions; + let raw = &indexer.vecs().op_return; + let txs = &indexer.vecs().transactions; let version = raw.first_index.version() + raw.to_tx_index.version() + raw.kind.version() diff --git a/crates/brk_computer/src/outputs/by_type/compute.rs b/crates/brk_computer/src/outputs/by_type/compute.rs index be6a34a55..d0f5456be 100644 --- a/crates/brk_computer/src/outputs/by_type/compute.rs +++ b/crates/brk_computer/src/outputs/by_type/compute.rs @@ -11,10 +11,10 @@ impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { let starting_lengths = indexer.safe_lengths(); - let dep_version = indexer.vecs.outputs.output_type.version() - + indexer.vecs.transactions.first_tx_index.version() - + indexer.vecs.transactions.first_txout_index.version() - + indexer.vecs.transactions.txid.version(); + let dep_version = indexer.vecs().outputs.output_type.version() + + indexer.vecs().transactions.first_tx_index.version() + + indexer.vecs().transactions.first_txout_index.version() + + indexer.vecs().transactions.txid.version(); self.output_count .validate_and_truncate(dep_version, starting_lengths.height)?; @@ -26,19 +26,19 @@ impl Vecs { .min_stateful_len() .min(self.tx_count.min_stateful_len()); - let first_tx_index = &indexer.vecs.transactions.first_tx_index; + let first_tx_index = &indexer.vecs().transactions.first_tx_index; let end = first_tx_index.len(); if skip < end { self.output_count.truncate_if_needed_at(skip)?; self.tx_count.truncate_if_needed_at(skip)?; let fi_batch = first_tx_index.collect_range_at(skip, end); - let txid_len = indexer.vecs.transactions.txid.len(); - let total_txout_len = indexer.vecs.outputs.output_type.len(); + let txid_len = indexer.vecs().transactions.txid.len(); + let total_txout_len = indexer.vecs().outputs.output_type.len(); - let mut otype_cursor = indexer.vecs.outputs.output_type.reader().cursor(); + let mut otype_cursor = indexer.vecs().outputs.output_type.reader().cursor(); let fo_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() diff --git a/crates/brk_computer/src/outputs/spent/compute.rs b/crates/brk_computer/src/outputs/spent/compute.rs index 4f8a77b5d..7983ea1ce 100644 --- a/crates/brk_computer/src/outputs/spent/compute.rs +++ b/crates/brk_computer/src/outputs/spent/compute.rs @@ -12,14 +12,14 @@ impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result { let starting_lengths = indexer.safe_lengths(); - let dep_version = indexer.vecs.inputs.txout_index.version() - + indexer.vecs.outputs.first_txout_index.version() - + indexer.vecs.inputs.first_txin_index.version() - + indexer.vecs.outputs.value.version(); + let dep_version = indexer.vecs().inputs.txout_index.version() + + indexer.vecs().outputs.first_txout_index.version() + + indexer.vecs().inputs.first_txin_index.version() + + indexer.vecs().outputs.value.version(); self.txin_index .validate_computed_version_or_reset(dep_version)?; - let target_height = indexer.vecs.blocks.blockhash.len(); + let target_height = indexer.vecs().blocks.blockhash.len(); if target_height == 0 { return Ok(exit.lock()); } @@ -35,10 +35,9 @@ impl Vecs { self.txin_index .truncate_if_needed(TxOutIndex::from(min_txout_index))?; - let txin_index_to_txout_index = &indexer.vecs.inputs.txout_index; - + let txin_index_to_txout_index = &indexer.vecs().inputs.txout_index; // Find min_height via binary search (first_txout_index is monotonically non-decreasing) - let first_txout_index_vec = &indexer.vecs.outputs.first_txout_index; + let first_txout_index_vec = &indexer.vecs().outputs.first_txout_index; let min_height = if min_txout_index == 0 { Height::ZERO } else if min_txout_index >= starting_lengths.txout_index.to_usize() { @@ -67,7 +66,7 @@ impl Vecs { let first_txout_index_data = first_txout_index_vec.collect_range_at(offset, target_height.to_usize() + 1); let first_txin_index_data = indexer - .vecs + .vecs() .inputs .first_txin_index .collect_range_at(offset, target_height.to_usize() + 2); @@ -87,7 +86,7 @@ impl Vecs { // Fill txout_index up to batch_end_height + 1 let batch_txout_index = if batch_end_height >= target_height { - indexer.vecs.outputs.value.len() + indexer.vecs().outputs.value.len() } else { first_txout_index_data[batch_end_height.to_usize() + 1 - offset].to_usize() }; @@ -98,7 +97,7 @@ impl Vecs { let txin_start = first_txin_index_data[batch_start_height.to_usize() - offset].to_usize(); let txin_end = if batch_end_height >= target_height { - indexer.vecs.inputs.txout_index.len() + indexer.vecs().inputs.txout_index.len() } else { first_txin_index_data[batch_end_height.to_usize() + 1 - offset].to_usize() }; diff --git a/crates/brk_computer/src/outputs/value/compute.rs b/crates/brk_computer/src/outputs/value/compute.rs index e3ff580e0..b6d37e4a4 100644 --- a/crates/brk_computer/src/outputs/value/compute.rs +++ b/crates/brk_computer/src/outputs/value/compute.rs @@ -17,13 +17,13 @@ impl Vecs { let height_vec = &mut self.op_return.cumulative.sats.height; // Validate computed versions against dependencies - let dep_version = indexer.vecs.outputs.first_txout_index.version() - + indexer.vecs.outputs.output_type.version() - + indexer.vecs.outputs.value.version(); + let dep_version = indexer.vecs().outputs.first_txout_index.version() + + indexer.vecs().outputs.output_type.version() + + indexer.vecs().outputs.value.version(); height_vec.validate_computed_version_or_reset(dep_version)?; // Get target height - let target_len = indexer.vecs.outputs.first_txout_index.len(); + let target_len = indexer.vecs().outputs.first_txout_index.len(); if target_len == 0 { self.op_return .compute_cents(starting_lengths.height, prices, exit)?; @@ -38,9 +38,10 @@ impl Vecs { if starting_height <= target_height { // Pre-collect height-indexed data let first_txout_indexes: Vec = - indexer.vecs.outputs.first_txout_index.collect_range_at( + indexer.vecs().outputs.first_txout_index.collect_range_at( starting_height.to_usize(), - target_height.to_usize() + 2.min(indexer.vecs.outputs.first_txout_index.len()), + target_height.to_usize() + + 2.min(indexer.vecs().outputs.first_txout_index.len()), ); let mut output_types_buf: Vec = Vec::new(); @@ -62,19 +63,19 @@ impl Vecs { if let Some(&next) = first_txout_indexes.get(local_idx + 1) { next } else { - TxOutIndex::from(indexer.vecs.outputs.value.len()) + TxOutIndex::from(indexer.vecs().outputs.value.len()) }; let out_start = first_txout_index.to_usize(); let out_end = next_first_txout_index.to_usize(); // Pre-collect both vecs into reusable buffers - indexer.vecs.outputs.output_type.collect_range_into_at( + indexer.vecs().outputs.output_type.collect_range_into_at( out_start, out_end, &mut output_types_buf, ); - indexer.vecs.outputs.value.collect_range_into_at( + indexer.vecs().outputs.value.collect_range_into_at( out_start, out_end, &mut values_buf, diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index 66f9fa2e7..763558d5f 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -137,20 +137,20 @@ impl Vecs { let starting_height = indexer.safe_lengths().height; let dep_version: Version = [ - indexer.vecs.blocks.coinbase_tag.version(), - indexer.vecs.transactions.first_tx_index.version(), - indexer.vecs.transactions.first_txout_index.version(), + indexer.vecs().blocks.coinbase_tag.version(), + indexer.vecs().transactions.first_tx_index.version(), + indexer.vecs().transactions.first_txout_index.version(), indexes.tx_index.output_count.version(), - indexer.vecs.outputs.output_type.version(), - indexer.vecs.outputs.type_index.version(), - indexer.vecs.addrs.p2pk65.bytes.version(), - indexer.vecs.addrs.p2pk33.bytes.version(), - indexer.vecs.addrs.p2pkh.bytes.version(), - indexer.vecs.addrs.p2sh.bytes.version(), - indexer.vecs.addrs.p2wpkh.bytes.version(), - indexer.vecs.addrs.p2wsh.bytes.version(), - indexer.vecs.addrs.p2tr.bytes.version(), - indexer.vecs.addrs.p2a.bytes.version(), + indexer.vecs().outputs.output_type.version(), + indexer.vecs().outputs.type_index.version(), + indexer.vecs().addrs.p2pk65.bytes.version(), + indexer.vecs().addrs.p2pk33.bytes.version(), + indexer.vecs().addrs.p2pkh.bytes.version(), + indexer.vecs().addrs.p2sh.bytes.version(), + indexer.vecs().addrs.p2wpkh.bytes.version(), + indexer.vecs().addrs.p2wsh.bytes.version(), + indexer.vecs().addrs.p2tr.bytes.version(), + indexer.vecs().addrs.p2a.bytes.version(), ] .into_iter() .sum(); @@ -165,17 +165,17 @@ impl Vecs { } self.pool.validate_computed_version_or_reset(dep_version)?; - let first_txout_index = indexer.vecs.transactions.first_txout_index.reader(); - let output_type = indexer.vecs.outputs.output_type.reader(); - let type_index = indexer.vecs.outputs.type_index.reader(); - let p2pk65 = indexer.vecs.addrs.p2pk65.bytes.reader(); - let p2pk33 = indexer.vecs.addrs.p2pk33.bytes.reader(); - let p2pkh = indexer.vecs.addrs.p2pkh.bytes.reader(); - let p2sh = indexer.vecs.addrs.p2sh.bytes.reader(); - let p2wpkh = indexer.vecs.addrs.p2wpkh.bytes.reader(); - let p2wsh = indexer.vecs.addrs.p2wsh.bytes.reader(); - let p2tr = indexer.vecs.addrs.p2tr.bytes.reader(); - let p2a = indexer.vecs.addrs.p2a.bytes.reader(); + let first_txout_index = indexer.vecs().transactions.first_txout_index.reader(); + let output_type = indexer.vecs().outputs.output_type.reader(); + let type_index = indexer.vecs().outputs.type_index.reader(); + let p2pk65 = indexer.vecs().addrs.p2pk65.bytes.reader(); + let p2pk33 = indexer.vecs().addrs.p2pk33.bytes.reader(); + let p2pkh = indexer.vecs().addrs.p2pkh.bytes.reader(); + let p2sh = indexer.vecs().addrs.p2sh.bytes.reader(); + let p2wpkh = indexer.vecs().addrs.p2wpkh.bytes.reader(); + let p2wsh = indexer.vecs().addrs.p2wsh.bytes.reader(); + let p2tr = indexer.vecs().addrs.p2tr.bytes.reader(); + let p2a = indexer.vecs().addrs.p2a.bytes.reader(); let unknown = self.pools.get_unknown(); @@ -184,17 +184,17 @@ impl Vecs { // Cursors avoid per-height PcoVec page decompression. // Heights are sequential, tx_index values derived from them are monotonically // increasing, so both cursors only advance forward. - let mut first_tx_index_cursor = indexer.vecs.transactions.first_tx_index.cursor(); + let mut first_tx_index_cursor = indexer.vecs().transactions.first_tx_index.cursor(); first_tx_index_cursor.advance(min); let mut output_count_cursor = indexes.tx_index.output_count.cursor(); self.pool.truncate_if_needed_at(min)?; self.pool_heights.truncate(min); - let len = indexer.vecs.blocks.coinbase_tag.len(); + let len = indexer.vecs().blocks.coinbase_tag.len(); let mut next_height = min; - indexer.vecs.blocks.coinbase_tag.try_for_each_range_at( + indexer.vecs().blocks.coinbase_tag.try_for_each_range_at( min, len, |coinbase_tag| -> Result<()> { diff --git a/crates/brk_computer/src/price/compute.rs b/crates/brk_computer/src/price/compute.rs index 50b2c1d80..9cbc0eb5a 100644 --- a/crates/brk_computer/src/price/compute.rs +++ b/crates/brk_computer/src/price/compute.rs @@ -29,12 +29,12 @@ impl Vecs { let starting_height = indexer.safe_lengths().height; let source_version = [ - indexer.vecs.transactions.txid.version(), - indexer.vecs.transactions.first_tx_index.version(), - indexer.vecs.outputs.first_txout_index.version(), - indexer.vecs.transactions.first_txout_index.version(), - indexer.vecs.outputs.value.version(), - indexer.vecs.outputs.output_type.version(), + indexer.vecs().transactions.txid.version(), + indexer.vecs().transactions.first_tx_index.version(), + indexer.vecs().outputs.first_txout_index.version(), + indexer.vecs().transactions.first_txout_index.version(), + indexer.vecs().outputs.value.version(), + indexer.vecs().outputs.output_type.version(), ] .into_iter() .sum(); @@ -44,7 +44,7 @@ impl Vecs { .inner .validate_computed_version_or_reset(source_version)?; - let total_heights = indexer.vecs.blocks.timestamp.len(); + let total_heights = indexer.vecs().blocks.timestamp.len(); if total_heights <= START_HEIGHT_SLOW { return Ok(()); @@ -195,22 +195,22 @@ impl Vecs { c.height.to_usize(), ), None => ( - indexer.vecs.transactions.txid.len(), - indexer.vecs.outputs.value.len(), - indexer.vecs.transactions.first_tx_index.len(), + indexer.vecs().transactions.txid.len(), + indexer.vecs().outputs.value.len(), + indexer.vecs().transactions.first_tx_index.len(), ), }; // Pre-collect height-indexed data for the range (plus one extra for next-block lookups) let collect_end = (range.end + 1).min(height_len); let first_tx_indexes: Vec = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(range.start, collect_end); let out_firsts: Vec = indexer - .vecs + .vecs() .outputs .first_txout_index .collect_range_at(range.start, collect_end); @@ -218,7 +218,7 @@ impl Vecs { // Cursor avoids per-block PcoVec page decompression for the // tx-indexed first_txout_index lookup. Accessed tx_index values // are strictly increasing across blocks, so it only advances forward. - let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor(); + let mut txout_cursor = indexer.vecs().transactions.first_txout_index.cursor(); // Reusable buffers: avoid per-block allocation. `tx_starts` holds the // first txout index of each non-coinbase tx in the current block. @@ -249,11 +249,11 @@ impl Vecs { let out_start = tx_starts.first().copied().unwrap_or(out_end); indexer - .vecs + .vecs() .outputs .value .collect_range_into_at(out_start, out_end, &mut values); - indexer.vecs.outputs.output_type.collect_range_into_at( + indexer.vecs().outputs.output_type.collect_range_into_at( out_start, out_end, &mut output_types, diff --git a/crates/brk_computer/src/transactions/features/compute.rs b/crates/brk_computer/src/transactions/features/compute.rs index 63e4cc153..41efb8347 100644 --- a/crates/brk_computer/src/transactions/features/compute.rs +++ b/crates/brk_computer/src/transactions/features/compute.rs @@ -7,8 +7,7 @@ use super::Vecs; impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { let starting_height = indexer.safe_lengths().height; - let source = &indexer.vecs.transaction_features.count; - + let source = &indexer.vecs().transaction_features.count; for (metrics, source) in [ (&mut self.count.inscription, &source.inscription), (&mut self.count.annex, &source.annex), diff --git a/crates/brk_computer/src/transactions/fees/compute.rs b/crates/brk_computer/src/transactions/fees/compute.rs index 90c5c90b6..bd030b164 100644 --- a/crates/brk_computer/src/transactions/fees/compute.rs +++ b/crates/brk_computer/src/transactions/fees/compute.rs @@ -25,16 +25,16 @@ impl Vecs { self.input_value.compute_sum_from_indexes( starting_lengths.tx_index, - &indexer.vecs.transactions.first_txin_index, + &indexer.vecs().transactions.first_txin_index, &indexes.tx_index.input_count, input_values, exit, )?; self.output_value.compute_sum_from_indexes( starting_lengths.tx_index, - &indexer.vecs.transactions.first_txout_index, + &indexer.vecs().transactions.first_txout_index, &indexes.tx_index.output_count, - &indexer.vecs.outputs.value, + &indexer.vecs().outputs.value, exit, )?; @@ -75,9 +75,9 @@ impl Vecs { let dep_version = self.input_value.version() + self.output_value.version() + size_vecs.vsize.tx_index.version() - + indexer.vecs.inputs.outpoint.version() - + indexer.vecs.transactions.first_tx_index.version() - + indexer.vecs.transactions.first_txin_index.version() + + indexer.vecs().inputs.outpoint.version() + + indexer.vecs().transactions.first_tx_index.version() + + indexer.vecs().transactions.first_txin_index.version() + indexes.height.tx_index_count.version(); self.fee @@ -114,7 +114,7 @@ impl Vecs { .min(self.is_cpfp_child.len()) .min(starting_lengths.tx_index.to_usize()); let max_height = indexer - .vecs + .vecs() .transactions .first_tx_index .len() @@ -142,7 +142,7 @@ impl Vecs { } let start_tx = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_one_at(start_height) @@ -163,7 +163,7 @@ impl Vecs { self.count.cpfp_child.truncate_if_needed_at(start_height)?; let mut tx_count = indexes.height.tx_index_count.cursor(); - let mut next_block_input = indexer.vecs.inputs.first_txin_index.cursor(); + let mut next_block_input = indexer.vecs().inputs.first_txin_index.cursor(); tx_count.advance(start_height); next_block_input.advance(start_height + 1); @@ -193,7 +193,7 @@ impl Vecs { .tx_index .collect_range_into_at(first_tx, first_tx + n, &mut vsizes); indexer - .vecs + .vecs() .transactions .first_txin_index .collect_range_into_at(first_tx, first_tx + n, &mut txin_starts); @@ -201,9 +201,9 @@ impl Vecs { let input_end = if h + 1 < max_height { next_block_input.next().unwrap().to_usize() } else { - indexer.vecs.inputs.outpoint.len() + indexer.vecs().inputs.outpoint.len() }; - indexer.vecs.inputs.outpoint.collect_range_into_at( + indexer.vecs().inputs.outpoint.collect_range_into_at( input_begin, input_end, &mut outpoints, diff --git a/crates/brk_computer/src/transactions/patterns/compute.rs b/crates/brk_computer/src/transactions/patterns/compute.rs index 506fb76e6..7b517188b 100644 --- a/crates/brk_computer/src/transactions/patterns/compute.rs +++ b/crates/brk_computer/src/transactions/patterns/compute.rs @@ -16,18 +16,18 @@ impl Vecs { indexes: &indexes::Vecs, exit: &Exit, ) -> Result<()> { - let features = &indexer.vecs.transaction_features; + let features = &indexer.vecs().transaction_features; let version = indexes.tx_index.input_count.version() + indexes.tx_index.output_count.version() - + indexer.vecs.transactions.first_tx_index.version() - + indexer.vecs.transactions.first_txin_index.version() - + indexer.vecs.transactions.first_txout_index.version() + + indexer.vecs().transactions.first_tx_index.version() + + indexer.vecs().transactions.first_txin_index.version() + + indexer.vecs().transactions.first_txout_index.version() + input_values.version() - + indexer.vecs.inputs.output_type.version() - + indexer.vecs.inputs.type_index.version() - + indexer.vecs.outputs.value.version() - + indexer.vecs.outputs.output_type.version() - + indexer.vecs.outputs.type_index.version() + + indexer.vecs().inputs.output_type.version() + + indexer.vecs().inputs.type_index.version() + + indexer.vecs().outputs.value.version() + + indexer.vecs().outputs.output_type.version() + + indexer.vecs().outputs.type_index.version() + features.has_op_return.version() + features.has_inscription.version() + indexes.height.tx_index_count.version(); @@ -70,7 +70,7 @@ impl Vecs { return Ok(()); } - let first_tx = &indexer.vecs.transactions.first_tx_index; + let first_tx = &indexer.vecs().transactions.first_tx_index; let start_tx = first_tx.collect_one_at(start_height).unwrap().to_usize(); self.is_coinjoin.truncate_if_needed_at(start_tx)?; self.is_consolidation.truncate_if_needed_at(start_tx)?; @@ -84,14 +84,14 @@ impl Vecs { .truncate_if_needed_at(start_height)?; let first_txin = indexer - .vecs + .vecs() .transactions .first_txin_index .collect_one_at(start_tx) .unwrap() .to_usize(); let first_txout = indexer - .vecs + .vecs() .transactions .first_txout_index .collect_one_at(start_tx) @@ -101,11 +101,11 @@ impl Vecs { let mut input_count = indexes.tx_index.input_count.cursor(); let mut output_count = indexes.tx_index.output_count.cursor(); let mut input_value = input_values.cursor(); - let mut input_type = indexer.vecs.inputs.output_type.cursor(); - let mut input_type_index = indexer.vecs.inputs.type_index.cursor(); - let mut output_value = indexer.vecs.outputs.value.reader().cursor(); - let mut output_type = indexer.vecs.outputs.output_type.reader().cursor(); - let mut output_type_index = indexer.vecs.outputs.type_index.reader().cursor(); + let mut input_type = indexer.vecs().inputs.output_type.cursor(); + let mut input_type_index = indexer.vecs().inputs.type_index.cursor(); + let mut output_value = indexer.vecs().outputs.value.reader().cursor(); + let mut output_type = indexer.vecs().outputs.output_type.reader().cursor(); + let mut output_type_index = indexer.vecs().outputs.type_index.reader().cursor(); let mut has_op_return = features.has_op_return.cursor(); let mut has_inscription = features.has_inscription.cursor(); let mut tx_count = indexes.height.tx_index_count.cursor(); diff --git a/crates/brk_computer/src/transactions/policy/compute.rs b/crates/brk_computer/src/transactions/policy/compute.rs index 1253e9e76..b44d410c8 100644 --- a/crates/brk_computer/src/transactions/policy/compute.rs +++ b/crates/brk_computer/src/transactions/policy/compute.rs @@ -17,11 +17,11 @@ impl Vecs { fees: &fees::Vecs, exit: &Exit, ) -> Result<()> { - let features = &indexer.vecs.transaction_features; + let features = &indexer.vecs().transaction_features; let version = features.is_unconditionally_nonstandard.version() + features.has_dust_output.version() + fees.fee.tx_index.version() - + indexer.vecs.transactions.first_tx_index.version() + + indexer.vecs().transactions.first_tx_index.version() + indexes.height.tx_index_count.version(); self.is_nonstandard .validate_computed_version_or_reset(version)?; @@ -57,7 +57,7 @@ impl Vecs { return Ok(()); } - let first_tx = &indexer.vecs.transactions.first_tx_index; + let first_tx = &indexer.vecs().transactions.first_tx_index; let start_tx = first_tx.collect_one_at(start_height).unwrap().to_usize(); self.is_nonstandard.truncate_if_needed_at(start_tx)?; self.count.nonstandard.truncate_if_needed_at(start_height)?; diff --git a/crates/brk_computer/src/transactions/sigops/compute.rs b/crates/brk_computer/src/transactions/sigops/compute.rs index 31900066b..94180f301 100644 --- a/crates/brk_computer/src/transactions/sigops/compute.rs +++ b/crates/brk_computer/src/transactions/sigops/compute.rs @@ -15,9 +15,9 @@ impl Vecs { ) -> Result<()> { self.total.compute_cumulative_sum_from_indexes( indexer.safe_lengths().height, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, - &indexer.vecs.transactions.total_sigop_cost, + &indexer.vecs().transactions.total_sigop_cost, |value| StoredU64::from(u64::from(u32::from(value))), exit, ) diff --git a/crates/brk_computer/src/transactions/size/compute.rs b/crates/brk_computer/src/transactions/size/compute.rs index 351bcce8c..4d1c2e44f 100644 --- a/crates/brk_computer/src/transactions/size/compute.rs +++ b/crates/brk_computer/src/transactions/size/compute.rs @@ -18,7 +18,7 @@ impl Vecs { indexer, indexes, &starting_lengths, - &indexer.vecs.transactions.weight, + &indexer.vecs().transactions.weight, exit, )?; diff --git a/crates/brk_computer/src/transactions/size/import.rs b/crates/brk_computer/src/transactions/size/import.rs index 910f1e497..72508878f 100644 --- a/crates/brk_computer/src/transactions/size/import.rs +++ b/crates/brk_computer/src/transactions/size/import.rs @@ -21,7 +21,7 @@ impl Vecs { let tx_index_to_vsize = LazyVec::transformed::( "tx_vsize", version, - indexer.vecs.transactions.weight.read_only_boxed_clone(), + indexer.vecs().transactions.weight.read_only_boxed_clone(), ); let vsize = LazyPerTxDistributionTransformed::new::( diff --git a/crates/brk_computer/src/transactions/versions/compute.rs b/crates/brk_computer/src/transactions/versions/compute.rs index 7ce065082..c7690ce21 100644 --- a/crates/brk_computer/src/transactions/versions/compute.rs +++ b/crates/brk_computer/src/transactions/versions/compute.rs @@ -8,8 +8,7 @@ impl Vecs { pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { let lengths = indexer.safe_lengths(); let starting_height = lengths.height; - let counts = &indexer.vecs.transaction_features.count; - + let counts = &indexer.vecs().transaction_features.count; for (metrics, source) in [ (&mut self.v1, &counts.v1), (&mut self.v2, &counts.v2), diff --git a/crates/brk_computer/src/transactions/volume/compute.rs b/crates/brk_computer/src/transactions/volume/compute.rs index 8a9d47807..3c2fdf24e 100644 --- a/crates/brk_computer/src/transactions/volume/compute.rs +++ b/crates/brk_computer/src/transactions/volume/compute.rs @@ -20,7 +20,7 @@ impl Vecs { self.transfer_volume.compute_filtered_from_indexes( starting_height, prices, - &indexer.vecs.transactions.first_tx_index, + &indexer.vecs().transactions.first_tx_index, &indexes.height.tx_index_count, &fees_vecs.input_value, |sats| !sats.is_max(), diff --git a/crates/brk_error/src/lib.rs b/crates/brk_error/src/lib.rs index 540bc424e..2bdbda55e 100644 --- a/crates/brk_error/src/lib.rs +++ b/crates/brk_error/src/lib.rs @@ -178,15 +178,42 @@ impl Error { /// Lock errors are transient and should not trigger data deletion. #[cfg(feature = "vecdb")] pub fn is_lock_error(&self) -> bool { - matches!(self, Error::VecDB(e) if e.is_lock_error()) + let is_vecdb_lock = matches!(self, Error::VecDB(e) if e.is_lock_error()); + #[cfg(feature = "fjall")] + { + is_vecdb_lock || matches!(self, Error::Fjall(fjall::Error::Locked)) + } + #[cfg(not(feature = "fjall"))] + { + is_vecdb_lock + } } /// Returns true if this error indicates data corruption or version incompatibility. /// These errors may require resetting/deleting the data to recover. #[cfg(feature = "vecdb")] pub fn is_data_error(&self) -> bool { - matches!(self, Error::VecDB(e) if e.is_data_error()) - || matches!(self, Error::VersionMismatch { .. }) + let is_vecdb_data = matches!(self, Error::VecDB(e) if e.is_data_error()) + || matches!(self, Error::VersionMismatch { .. }); + #[cfg(feature = "fjall")] + { + is_vecdb_data + || matches!( + self, + Error::Fjall( + fjall::Error::JournalRecovery(_) + | fjall::Error::InvalidVersion(_) + | fjall::Error::Decompress(_) + | fjall::Error::InvalidTrailer + | fjall::Error::InvalidTag(_) + | fjall::Error::Unrecoverable + ) + ) + } + #[cfg(not(feature = "fjall"))] + { + is_vecdb_data + } } /// Returns true if this network/fetch error indicates a permanent/blocking condition diff --git a/crates/brk_indexer/examples/indexer.rs b/crates/brk_indexer/examples/indexer.rs index 60cb39b5f..5bbeee7dc 100644 --- a/crates/brk_indexer/examples/indexer.rs +++ b/crates/brk_indexer/examples/indexer.rs @@ -32,7 +32,7 @@ fn main() -> color_eyre::Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); debug!("Reader created."); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; debug!("Indexer imported."); let exit = Exit::new(); @@ -40,7 +40,7 @@ fn main() -> color_eyre::Result<()> { loop { let i = Instant::now(); - indexer.checked_index(&reader, &client, &exit)?; + indexer.checked_index(&exit)?; indexer.advance_safe_lengths()?; info!("Done in {:?}", i.elapsed()); diff --git a/crates/brk_indexer/examples/indexer_bench.rs b/crates/brk_indexer/examples/indexer_bench.rs index 8650d1a6f..961b83520 100644 --- a/crates/brk_indexer/examples/indexer_bench.rs +++ b/crates/brk_indexer/examples/indexer_bench.rs @@ -32,7 +32,7 @@ fn main() -> Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; let mut bencher = Bencher::from_cargo_env(env!("CARGO_PKG_NAME"), &outputs_dir.join("indexed"))?; @@ -47,7 +47,7 @@ fn main() -> Result<()> { }); let i = Instant::now(); - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; indexer.advance_safe_lengths()?; info!("Done in {:?}", i.elapsed()); diff --git a/crates/brk_indexer/examples/indexer_bench2.rs b/crates/brk_indexer/examples/indexer_bench2.rs index 7cc0d04c1..1db670eb1 100644 --- a/crates/brk_indexer/examples/indexer_bench2.rs +++ b/crates/brk_indexer/examples/indexer_bench2.rs @@ -32,7 +32,7 @@ fn main() -> Result<()> { let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let mut indexer = Indexer::forced_import(&outputs_dir)?; + let mut indexer = Indexer::import(&outputs_dir, &reader)?; let mut bencher = Bencher::from_cargo_env(env!("CARGO_PKG_NAME"), &outputs_dir.join("indexed"))?; @@ -48,7 +48,7 @@ fn main() -> Result<()> { loop { let i = Instant::now(); - indexer.index(&reader, &client, &exit)?; + indexer.index(&exit)?; indexer.advance_safe_lengths()?; info!("Done in {:?}", i.elapsed()); diff --git a/crates/brk_indexer/examples/indexer_read.rs b/crates/brk_indexer/examples/indexer_read.rs index 7e0990492..f706154aa 100644 --- a/crates/brk_indexer/examples/indexer_read.rs +++ b/crates/brk_indexer/examples/indexer_read.rs @@ -2,6 +2,8 @@ use std::{fs, path::Path}; use brk_error::Result; use brk_indexer::Indexer; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use vecdb::ReadableVec; fn main() -> Result<()> { @@ -10,9 +12,18 @@ fn main() -> Result<()> { let outputs_dir = Path::new(&std::env::var("HOME").unwrap()).join(".brk"); fs::create_dir_all(&outputs_dir)?; - let indexer = Indexer::forced_import(&outputs_dir)?; + let bitcoin_dir = Client::default_bitcoin_path(); + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + )?; + let reader = Reader::new(bitcoin_dir.join("blocks"), &client); + let indexer = Indexer::import(&outputs_dir, &reader)?; - println!("{:?}", indexer.vecs.outputs.value.collect_range_at(0, 200)); + println!( + "{:?}", + indexer.vecs().outputs.value.collect_range_at(0, 200) + ); Ok(()) } diff --git a/crates/brk_indexer/examples/indexer_read_speed.rs b/crates/brk_indexer/examples/indexer_read_speed.rs index 6157ea7c3..517772aaa 100644 --- a/crates/brk_indexer/examples/indexer_read_speed.rs +++ b/crates/brk_indexer/examples/indexer_read_speed.rs @@ -2,6 +2,8 @@ use std::{fs, path::Path, time::Instant}; use brk_error::Result; use brk_indexer::Indexer; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use brk_types::Sats; use vecdb::ReadableVec; @@ -10,7 +12,7 @@ fn run_benchmark(indexer: &Indexer) -> (Sats, std::time::Duration, usize) { let mut sum = Sats::ZERO; let mut count = 0; - indexer.vecs.outputs.value.for_each(|value| { + indexer.vecs().outputs.value.for_each(|value| { sum += value; count += 1; }); @@ -30,7 +32,13 @@ fn main() -> Result<()> { println!("╚════════════════════════════════════════════════════════╝\n"); println!("Loading indexer from: {}", outputs_dir.display()); - let indexer = Indexer::forced_import(&outputs_dir)?; + let bitcoin_dir = Client::default_bitcoin_path(); + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + )?; + let reader = Reader::new(bitcoin_dir.join("blocks"), &client); + let indexer = Indexer::import(&outputs_dir, &reader)?; println!("Indexer loaded.\n"); // Warmup run diff --git a/crates/brk_indexer/src/constants.rs b/crates/brk_indexer/src/constants.rs index c31fa989e..eaa9d63de 100644 --- a/crates/brk_indexer/src/constants.rs +++ b/crates/brk_indexer/src/constants.rs @@ -4,7 +4,7 @@ use brk_types::{TxIndex, Txid, TxidPrefix, Version}; // One version for all data sources // Increment on **change _OR_ addition** -pub const VERSION: Version = Version::new(30); +pub const VERSION: Version = Version::new(31); pub const SNAPSHOT_BLOCK_RANGE: usize = 1_000; /// Known duplicate Bitcoin transactions (BIP30) diff --git a/crates/brk_indexer/src/lengths.rs b/crates/brk_indexer/src/lengths.rs index 386b5772e..1d67f3fc6 100644 --- a/crates/brk_indexer/src/lengths.rs +++ b/crates/brk_indexer/src/lengths.rs @@ -1,3 +1,4 @@ +use brk_error::Result; use brk_types::{ EmptyOutputIndex, Height, OpReturnIndex, OutputType, P2AAddrIndex, P2MSOutputIndex, P2PK33AddrIndex, P2PK65AddrIndex, P2PKHAddrIndex, P2SHAddrIndex, P2TRAddrIndex, @@ -6,7 +7,7 @@ use brk_types::{ use tracing::info; use vecdb::{AnyStoredVec, PcoVec, PcoVecValue, ReadableVec, VecIndex, VecValue, WritableVec}; -use crate::{Stores, Vecs}; +use crate::{Stores, Vecs, stores::IndexerStores as _}; /// Pipeline-wide length/count snapshot. Lengths semantics: /// `bound.f = N` means positions `0..N` are fully written; readers @@ -31,6 +32,11 @@ pub struct Lengths { pub unknown_output_index: UnknownOutputIndex, } +pub trait IndexerLengths: Sized { + fn from_local(vecs: &Vecs, stores: &Stores) -> Result>; + fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result>; +} + impl Lengths { pub fn to_type_index(&self, output_type: OutputType) -> TypeIndex { match output_type { @@ -134,18 +140,23 @@ impl Lengths { } /// Read current local lengths. `None` pre-genesis. - pub fn from_local(vecs: &Vecs, stores: &Stores) -> Option { - let height = vecs.next_height().min(stores.next_height()); - Self::collect_at(height, vecs) + fn read_local(vecs: &Vecs, stores: &Stores) -> Result> { + let Some(height) = matching_height(vecs.next_height(), stores.next_height()?) else { + return Ok(None); + }; + Ok(Self::collect_at(height, vecs)) } /// Read lengths to resume at `required_height`. Reorg-aware: + /// - if vector and store checkpoints differ, return `None` (full reset); /// - if local is ahead, clamp down to `required_height`; /// - if local is behind, return `None` (caller must full-reset). - pub fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Option { - let local = vecs.next_height().min(stores.next_height()); + fn read_resume(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result> { + let Some(local) = matching_height(vecs.next_height(), stores.next_height()?) else { + return Ok(None); + }; if local < required_height { - return None; + return Ok(None); } let height = if local > required_height { info!( @@ -156,7 +167,7 @@ impl Lengths { } else { local }; - Self::collect_at(height, vecs) + Ok(Self::collect_at(height, vecs)) } fn collect_at(height: Height, vecs: &Vecs) -> Option { @@ -229,6 +240,29 @@ impl Lengths { } } +impl IndexerLengths for Lengths { + fn from_local(vecs: &Vecs, stores: &Stores) -> Result> { + Self::read_local(vecs, stores) + } + + fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result> { + Self::read_resume(required_height, vecs, stores) + } +} + +fn matching_height(vec_height: Height, store_height: Option) -> Option { + let store_height = store_height?; + if vec_height == store_height { + Some(vec_height) + } else { + info!( + "Indexer checkpoint mismatch: vectors at {}, stores at {}; full reset required", + vec_height, store_height + ); + None + } +} + /// Per-type next-to-write counter at `next_height`. `None` pre-genesis. fn next_index( height_to_index: &PcoVec, @@ -240,11 +274,58 @@ where T: VecValue, { let h = Height::from(height_to_index.stamp()); - if h.is_zero() { + if next_height.is_zero() { None - } else if h + 1_u32 == next_height { + } else if h.incremented() == next_height { Some(I::from(index_to_else.len())) } else { height_to_index.collect_one(next_height) } } + +#[cfg(test)] +mod checkpoint_tests { + use super::*; + use brk_types::StoredU32; + use vecdb::{Database, ImportableVec, Stamp, Version}; + + #[test] + fn matching_checkpoint_is_accepted() { + let height = Height::new(42); + assert_eq!(matching_height(height, Some(height)), Some(height)); + } + + #[test] + fn mismatched_checkpoint_requires_reset() { + assert_eq!( + matching_height(Height::new(42), Some(Height::new(41))), + None + ); + assert_eq!( + matching_height(Height::new(41), Some(Height::new(42))), + None + ); + assert_eq!(matching_height(Height::ZERO, None), None); + } + + #[test] + fn genesis_stamp_uses_current_length() { + let dir = tempfile::tempdir().unwrap(); + let db = Database::open(dir.path()).unwrap(); + let mut first_index = + PcoVec::::forced_import(&db, "first_index", Version::ONE).unwrap(); + let mut values = + PcoVec::::forced_import(&db, "values", Version::ONE).unwrap(); + + first_index.push(TxIndex::ZERO); + values.push(StoredU32::from(1_u32)); + values.push(StoredU32::from(2_u32)); + first_index.stamped_write(Stamp::from(0_u64)).unwrap(); + + assert_eq!( + next_index(&first_index, &values, Height::new(1)), + Some(TxIndex::new(2)) + ); + assert_eq!(next_index(&first_index, &values, Height::ZERO), None); + } +} diff --git a/crates/brk_indexer/src/lib.rs b/crates/brk_indexer/src/lib.rs index bde221d57..cdfc84846 100644 --- a/crates/brk_indexer/src/lib.rs +++ b/crates/brk_indexer/src/lib.rs @@ -1,21 +1,20 @@ #![doc = include_str!("../README.md")] use std::{ - fs, - path::{Path, PathBuf}, + fs::{self, File}, + io::ErrorKind, + path::Path, thread, time::{Duration, Instant}, }; -use brk_error::Result; -use brk_reader::{Reader, XORBytes}; -use brk_rpc::Client; -use brk_types::{BlockHash, Height}; -use fjall::PersistMode; -use rayon::prelude::*; +use brk_error::{Error, Result}; +use brk_reader::{Reader, XOR_LEN, XORBytes}; +use brk_types::{BlkPosition, BlockHash, Height}; use tracing::{debug, error, info}; use vecdb::{ - Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec, unlikely, + AnyVec, Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec, + unlikely, }; mod constants; mod lengths; @@ -26,22 +25,109 @@ mod stores; mod vecs; use constants::*; +use lengths::IndexerLengths as _; use processor::{BlockBuffers, BlockProcessor}; use readers::Readers; +use stores::IndexerStores as _; +use vecs::{IndexerVecs as _, TransactionCounts, TxFeatureFlags}; pub use lengths::Lengths; -pub use safe_lengths::SafeLengths; pub use stores::Stores; -pub use vecs::*; +pub use vecs::{ + AddrTypeVecs, AddrsVecs, BlocksVecs, InputsVecs, OpReturnVecs, OutputsVecs, ScriptTypeVecs, + ScriptTypeWithSigOpsVecs, ScriptsVecs, TransactionCountVecs, TransactionFeaturesVecs, + TransactionsVecs, TxMetadataVecs, Vecs, +}; + +use safe_lengths::SafeLengths; pub struct Indexer { - path: PathBuf, - pub vecs: Vecs, - pub stores: Stores, + inner: IndexerInner, +} + +struct IndexerInner { + reader: Reader, + vecs: Vecs, + stores: Stores, buffers: BlockBuffers, safe_lengths: SafeLengths, } +enum ImportValidation { + Valid(Lengths), + Reset(&'static str), +} + +enum XorMarker { + Missing, + Invalid(usize), + Valid(XORBytes), +} + +fn is_export_height(height: Height) -> bool { + height != 0 && height % SNAPSHOT_BLOCK_RANGE == 0 +} + +fn final_export_height(completed_height: Option) -> Option { + completed_height.filter(|height| !is_export_height(*height)) +} + +fn read_xor_marker(path: &Path) -> Result { + let bytes = match fs::read(path.join("xor.dat")) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(XorMarker::Missing), + Err(err) => return Err(err.into()), + }; + Ok(match <[u8; XOR_LEN]>::try_from(bytes) { + Ok(bytes) => XorMarker::Valid(XORBytes::from(bytes)), + Err(bytes) => XorMarker::Invalid(bytes.len()), + }) +} + +fn validate_reader_source(reader: &Reader) -> Result<()> { + let current = match read_xor_marker(reader.blocks_dir())? { + XorMarker::Missing => XORBytes::from([0; XOR_LEN]), + XorMarker::Invalid(received) => { + return Err(Error::WrongLength { + expected: XOR_LEN, + received, + }); + } + XorMarker::Valid(xor) => xor, + }; + if current != reader.xor_bytes() { + return Err(Error::Internal( + "Block source changed after the Reader was created", + )); + } + Ok(()) +} + +fn write_xor_marker(path: &Path, source_xor: XORBytes) -> Result<()> { + fs::create_dir_all(path)?; + let pending = path.join("xor.pending"); + fs::write(&pending, *source_xor)?; + File::open(&pending)?.sync_all()?; + fs::rename(&pending, path.join("xor.dat"))?; + File::open(path)?.sync_all()?; + Ok(()) +} + +fn read_block_hash_at(reader: &Reader, position: BlkPosition) -> Result { + let bytes = reader.read_raw_bytes(position, bitcoin::block::Header::SIZE)?; + let header: bitcoin::block::Header = bitcoin::consensus::deserialize(&bytes)?; + Ok(BlockHash::from(header.block_hash())) +} + +fn recreate_indexed_dir(path: &Path, source_xor: XORBytes) -> Result<()> { + match fs::remove_dir_all(path) { + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + write_xor_marker(path, source_xor) +} + impl Indexer { /// Tip block hash at the pipeline-safe ceiling. /// @@ -56,6 +142,7 @@ impl Indexer { pub fn tip_blockhash(&self) -> BlockHash { match self.safe_lengths().height.decremented() { Some(h) => self + .inner .vecs .blocks .blockhash @@ -69,24 +156,65 @@ impl Indexer { /// advance and lower this internally; readers clamp non-series /// answers against this loaded snapshot. pub fn safe_lengths(&self) -> Lengths { - self.safe_lengths.load() + self.inner.safe_lengths.load() + } + + pub fn reader(&self) -> &Reader { + &self.inner.reader + } + + #[inline] + pub fn vecs(&self) -> &Vecs { + &self.inner.vecs + } + + #[inline] + pub fn stores(&self) -> &Stores { + &self.inner.stores } } impl Indexer { /// Live indexer stamp for diagnostics. For data reads use - /// [`crate::SafeLengths::load`] (via `Query::height`). + /// `SafeLengths::load` (via `Query::height`). pub fn indexed_height(&self) -> Height { - Height::from(self.vecs.blocks.blockhash.inner.stamp()) + Height::from(self.inner.vecs.blocks.blockhash.inner.stamp()) } } impl Indexer { - pub fn forced_import(outputs_dir: &Path) -> Result { - Self::forced_import_inner(outputs_dir, true) + /// Imports and validates an indexer for writing against `reader`. + /// + /// Any reset happens before this function returns, after all handles from + /// the failed import attempt have been dropped. + pub fn import(outputs_dir: &Path, reader: &Reader) -> Result { + Ok(Self { + inner: IndexerInner::import(outputs_dir, reader)?, + }) } - fn forced_import_inner(outputs_dir: &Path, can_retry: bool) -> Result { + pub fn index(&mut self, exit: &Exit) -> Result<()> { + self.inner.index(exit, false) + } + + pub fn checked_index(&mut self, exit: &Exit) -> Result<()> { + self.inner.index(exit, true) + } + + /// Publish disk state as the new safe-lengths snapshot. Drains pending + /// bg ingest first so stores are queryable at the new bound. + pub fn advance_safe_lengths(&mut self) -> Result<()> { + self.inner.advance_safe_lengths() + } +} + +impl IndexerInner { + fn import(outputs_dir: &Path, reader: &Reader) -> Result { + validate_reader_source(reader)?; + Self::import_inner(outputs_dir, reader, true) + } + + fn import_inner(outputs_dir: &Path, reader: &Reader, can_retry: bool) -> Result { info!("Importing indexer..."); let indexed_path = outputs_dir.join("indexed"); @@ -100,74 +228,132 @@ impl Indexer { let stores = Stores::forced_import(&indexed_path, VERSION)?; info!("Imported stores in {:?}", i.elapsed()); - let safe_lengths = SafeLengths::new(); - if let Some(lengths) = Lengths::from_local(&vecs, &stores) { - safe_lengths.advance(lengths); - } - Ok(Self { - path: indexed_path.clone(), + reader: reader.clone(), vecs, stores, buffers: BlockBuffers::default(), - safe_lengths, + safe_lengths: SafeLengths::new(), }) }; - match try_import() { - Ok(result) => Ok(result), + let mut indexer = match try_import() { + Ok(indexer) => indexer, Err(err) if err.is_lock_error() => { // Lock errors are transient - another process has the database open. // Don't delete data, just return the error. - Err(err) + return Err(err); } Err(err) if can_retry && err.is_data_error() => { - // Data corruption or version mismatch - safe to delete and retry + // The failed attempt has returned, so all of its local database + // handles have been dropped before the directory is removed. info!("{err:?}, deleting {indexed_path:?} and retrying"); - fs::remove_dir_all(&indexed_path)?; - Self::forced_import_inner(outputs_dir, false) + recreate_indexed_dir(&indexed_path, reader.xor_bytes())?; + return Self::import_inner(outputs_dir, reader, false); } - Err(err) => Err(err), + Err(err) => return Err(err), + }; + + match indexer.validate_import(&indexed_path)? { + ImportValidation::Valid(lengths) => { + indexer.rollback_to(&lengths)?; + indexer.safe_lengths.advance(lengths); + Ok(indexer) + } + ImportValidation::Reset(reason) if can_retry => { + info!("{reason}, deleting {indexed_path:?} and retrying"); + drop(indexer); + recreate_indexed_dir(&indexed_path, reader.xor_bytes())?; + Self::import_inner(outputs_dir, reader, false) + } + ImportValidation::Reset(reason) => Err(Error::Internal(reason)), } } - /// Fully resets the indexer by deleting stores from disk and reimporting. - /// Unlike stores.reset() which uses keyspace.clear() (leaving a journal - /// record that gets replayed on every recovery), this cleanly recreates. - fn full_reset(&mut self) -> Result<()> { - info!("Full reset..."); - self.buffers.reset(); - self.safe_lengths.reset(); - self.vecs.reset()?; - let stores_path = self.path.join("stores"); - fs::remove_dir_all(&stores_path).ok(); - self.stores = Stores::forced_import(&self.path, VERSION)?; - Ok(()) + fn validate_import(&self, indexed_path: &Path) -> Result { + let reader = &self.reader; + let vec_height = self.vecs.next_height(); + let store_height = self.stores.next_height()?; + let is_empty = vec_height.is_zero() && store_height == Some(Height::ZERO); + let local_lengths = if is_empty { + Lengths::default() + } else if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores)? { + lengths + } else { + return Ok(ImportValidation::Reset( + "Indexer checkpoints are missing, inconsistent, or incomplete", + )); + }; + + match read_xor_marker(indexed_path)? { + XorMarker::Missing if is_empty => write_xor_marker(indexed_path, reader.xor_bytes())?, + XorMarker::Valid(marker) if marker == reader.xor_bytes() => {} + XorMarker::Missing | XorMarker::Invalid(_) | XorMarker::Valid(_) => { + return Ok(ImportValidation::Reset( + "Indexer block source marker is missing, invalid, or changed", + )); + } + } + + let Some(hash) = self.vecs.blocks.blockhash.collect_last() else { + return Ok(ImportValidation::Valid(local_lengths)); + }; + + let tip_height = Height::from(self.vecs.blocks.blockhash.len() - 1); + let Some(position) = self.vecs.blocks.position.collect_one(tip_height) else { + return Ok(ImportValidation::Reset( + "Indexer tip block position is missing", + )); + }; + if read_block_hash_at(reader, position)? != hash { + return Ok(ImportValidation::Reset( + "Indexer block positions belong to a different block source", + )); + } + + reader.client().wait_for_synced_node()?; + let (height, _) = reader.client().get_closest_valid_height(hash)?; + match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores)? { + Some(lengths) => Ok(ImportValidation::Valid(lengths)), + None => Ok(ImportValidation::Reset( + "Indexer state cannot resume from the active chain", + )), + } } - pub fn index(&mut self, reader: &Reader, client: &Client, exit: &Exit) -> Result<()> { - self.index_(reader, client, exit, false) + fn rollback_to(&mut self, starting_lengths: &Lengths) -> Result<()> { + let local_height = self.vecs.next_height(); + if local_height == starting_lengths.height { + return Ok(()); + } + if local_height < starting_lengths.height { + return Err(Error::Internal("Cannot roll back beyond local state")); + } + + let completed_height = starting_lengths + .height + .decremented() + .ok_or(Error::Internal("Cannot roll back before genesis"))?; + self.stores + .rollback_if_needed(&self.vecs, starting_lengths)?; + self.vecs.rollback_if_needed(starting_lengths)?; + + let checkpoint = self.stores.begin_commit(completed_height)?; + let persisted = self.stores.persist(checkpoint)?; + self.vecs.flush(completed_height)?; + persisted.publish() } - pub fn checked_index(&mut self, reader: &Reader, client: &Client, exit: &Exit) -> Result<()> { - self.index_(reader, client, exit, true) - } - - fn index_( - &mut self, - reader: &Reader, - client: &Client, - exit: &Exit, - check_collisions: bool, - ) -> Result<()> { - self.vecs.db.sync_bg_tasks()?; - - self.check_xor_bytes(reader)?; + fn index(&mut self, exit: &Exit, check_collisions: bool) -> Result<()> { + let reader = self.reader.clone(); + validate_reader_source(&reader)?; + let client = reader.client(); + self.vecs.sync_bg_tasks()?; debug!("Starting indexing..."); let last_blockhash = self.vecs.blocks.blockhash.collect_last(); - // Rollback sim + // Rollback sim: do not remove // let last_blockhash = self // .vecs // .blocks @@ -177,7 +363,7 @@ impl Indexer { let (starting_lengths, prev_hash) = if let Some(hash) = last_blockhash { let (height, hash) = client.get_closest_valid_height(hash)?; - match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores) { + match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores)? { Some(starting_lengths) => { if starting_lengths.height > client.get_last_height()? { info!("Up to date, nothing to index."); @@ -186,9 +372,9 @@ impl Indexer { (starting_lengths, Some(hash)) } None => { - info!("Data inconsistency detected, resetting indexer..."); - self.full_reset()?; - (Lengths::default(), None) + return Err(Error::Internal( + "Indexer became inconsistent after import; drop and re-import it", + )); } } } else { @@ -198,44 +384,43 @@ impl Indexer { let lock = exit.lock(); self.safe_lengths.lower_before(&starting_lengths); - self.stores - .rollback_if_needed(&mut self.vecs, &starting_lengths)?; - debug!("Rollback stores done."); - self.vecs.rollback_if_needed(&starting_lengths)?; - debug!("Rollback vecs done."); + self.rollback_to(&starting_lengths)?; + debug!("Rollback done."); drop(lock); self.buffers.continue_from(prev_hash); let mut lengths = starting_lengths; + let mut completed_height = None; - let is_export_height = - |height: Height| -> bool { height != 0 && height % SNAPSHOT_BLOCK_RANGE == 0 }; - - let export = move |stores: &mut Stores, vecs: &mut Vecs, height: Height| -> Result<()> { - info!("Exporting..."); - let i = Instant::now(); - let _lock = exit.lock(); - thread::scope(|s| -> Result<()> { - let stores_res = s.spawn(|| -> Result<()> { - let i = Instant::now(); - stores.commit(height)?; - debug!("Stores exported in {:?}", i.elapsed()); + let export = + move |stores: &mut Stores, vecs: &mut Vecs, completed_height: Height| -> Result<()> { + info!("Exporting..."); + let i = Instant::now(); + let _lock = exit.lock(); + let checkpoint = stores.begin_commit(completed_height)?; + thread::scope(|s| -> Result<()> { + let stores_res = s.spawn(|| { + let i = Instant::now(); + let persisted = stores.persist(checkpoint)?; + debug!("Stores persisted in {:?}", i.elapsed()); + Ok::<_, brk_error::Error>(persisted) + }); + let vecs_res = s.spawn(|| -> Result<()> { + let i = Instant::now(); + vecs.flush(completed_height)?; + debug!("Vecs exported in {:?}", i.elapsed()); + Ok(()) + }); + let persisted = stores_res.join().unwrap()?; + vecs_res.join().unwrap()?; + // The shared checkpoint is visible only after both databases are durable. + persisted.publish()?; Ok(()) - }); - let vecs_res = s.spawn(|| -> Result<()> { - let i = Instant::now(); - vecs.flush(height)?; - debug!("Vecs exported in {:?}", i.elapsed()); - Ok(()) - }); - stores_res.join().unwrap()?; - vecs_res.join().unwrap()?; + })?; + info!("Exported in {:?}", i.elapsed()); Ok(()) - })?; - info!("Exported in {:?}", i.elapsed()); - Ok(()) - }; + }; let mut readers = Readers::new(&self.vecs); @@ -303,6 +488,7 @@ impl Indexer { .lengths .add_block(tx_count, input_count, output_count); buffers.finish_block(*block.hash()); + completed_height = Some(height); if is_export_height(height) { drop(readers); @@ -313,62 +499,40 @@ impl Indexer { drop(readers); - let lock = exit.lock(); - let tasks = self.stores.take_all_pending_ingests(lengths.height)?; - self.vecs.stamped_write(lengths.height)?; - let fjall_db = self.stores.db.clone(); + let Some(completed_height) = final_export_height(completed_height) else { + return Ok(()); + }; - self.vecs.db.run_bg(move |db| { + let lock = exit.lock(); + let deferred_commit = self.stores.take_deferred_commit(completed_height)?; + self.vecs.stamped_write(completed_height)?; + + self.vecs.run_bg(move |db| { let _lock = lock; db.bg_sleep(Duration::from_secs(3)); info!("Exporting..."); - let i = Instant::now(); + let total_i = Instant::now(); - if !tasks.is_empty() { - let i = Instant::now(); - tasks - .into_par_iter() - .try_for_each(|task| task().map_err(vecdb::RawDBError::other))?; - debug!("Stores committed in {:?}", i.elapsed()); - - let i = Instant::now(); - fjall_db - .persist(PersistMode::SyncData) - .map_err(RawDBError::other)?; - debug!("Stores persisted in {:?}", i.elapsed()); - } + let commit_i = Instant::now(); + let persisted = deferred_commit.persist().map_err(RawDBError::other)?; + debug!("Stores persisted in {:?}", commit_i.elapsed()); db.compact()?; + // Keep the checkpoint invalid until the vector write is durable too. + persisted.publish().map_err(RawDBError::other)?; - info!("Exported in {:?}", i.elapsed()); + info!("Exported in {:?}", total_i.elapsed()); Ok(()) }); Ok(()) } - fn check_xor_bytes(&mut self, reader: &Reader) -> Result<()> { - let current = reader.xor_bytes(); - let cached = XORBytes::from(self.path.as_path()); - - if cached == current { - return Ok(()); - } - - self.full_reset()?; - - fs::write(self.path.join("xor.dat"), *current)?; - - Ok(()) - } - - /// Publish disk state as the new safe-lengths snapshot. Drains pending - /// bg ingest first so stores are queryable at the new bound. - pub fn advance_safe_lengths(&mut self) -> Result<()> { - self.vecs.db.sync_bg_tasks()?; - if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores) { + fn advance_safe_lengths(&mut self) -> Result<()> { + self.vecs.sync_bg_tasks()?; + if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores)? { self.safe_lengths.advance(lengths); } Ok(()) @@ -380,11 +544,189 @@ impl ReadOnlyClone for Indexer { fn read_only_clone(&self) -> Indexer { Indexer { - path: self.path.clone(), - vecs: self.vecs.read_only_clone(), - stores: self.stores.clone(), - buffers: BlockBuffers::default(), - safe_lengths: self.safe_lengths.clone(), + inner: IndexerInner { + reader: self.inner.reader.clone(), + vecs: self.inner.vecs.read_only_clone(), + stores: self.inner.stores.clone(), + buffers: BlockBuffers::default(), + safe_lengths: self.inner.safe_lengths.clone(), + }, } } } + +#[cfg(test)] +mod import_tests { + use super::*; + use brk_rpc::{Auth, Client}; + use brk_types::BlockHashPrefix; + + fn empty_reader(path: &Path) -> Reader { + let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap(); + Reader::new_without_rlimit(path.join("blocks"), &client) + } + + #[test] + fn final_export_requires_an_unsnapshotted_completed_block() { + let snapshot_height = Height::from(SNAPSHOT_BLOCK_RANGE); + + assert_eq!(final_export_height(None), None); + assert_eq!(final_export_height(Some(Height::ZERO)), Some(Height::ZERO)); + assert_eq!(final_export_height(Some(snapshot_height)), None); + assert_eq!( + final_export_height(Some(snapshot_height.incremented())), + Some(snapshot_height.incremented()) + ); + } + + #[test] + fn recreate_drops_old_contents_and_seeds_source_identity() { + let dir = tempfile::tempdir().unwrap(); + let indexed = dir.path().join("indexed"); + fs::create_dir_all(&indexed).unwrap(); + fs::write(indexed.join("stale"), b"stale").unwrap(); + let source_xor = XORBytes::from([7_u8; 8]); + + recreate_indexed_dir(&indexed, source_xor).unwrap(); + + assert!(!indexed.join("stale").exists()); + assert!(matches!( + read_xor_marker(&indexed).unwrap(), + XorMarker::Valid(marker) if marker == source_xor + )); + } + + #[test] + fn empty_import_writes_identity_marker() -> Result<()> { + let dir = tempfile::tempdir()?; + let reader = empty_reader(dir.path()); + + drop(Indexer::import(dir.path(), &reader)?); + + assert!(matches!( + read_xor_marker(&dir.path().join("indexed"))?, + XorMarker::Valid(marker) if marker == XORBytes::from([0; XOR_LEN]) + )); + Ok(()) + } + + #[test] + fn malformed_xor_marker_recreates_the_index() -> Result<()> { + let dir = tempfile::tempdir()?; + let indexed = dir.path().join("indexed"); + let reader = empty_reader(dir.path()); + drop(Indexer::import(dir.path(), &reader)?); + fs::write(indexed.join("xor.dat"), [0_u8; 3])?; + fs::write(indexed.join("stale"), b"stale")?; + + drop(Indexer::import(dir.path(), &reader)?); + + assert!(!indexed.join("stale").exists()); + assert!(matches!( + read_xor_marker(&indexed)?, + XorMarker::Valid(marker) if marker == reader.xor_bytes() + )); + Ok(()) + } + + #[test] + fn malformed_source_xor_never_deletes_data() -> Result<()> { + let dir = tempfile::tempdir()?; + let indexed = dir.path().join("indexed"); + let reader = empty_reader(dir.path()); + drop(Indexer::import(dir.path(), &reader)?); + fs::write(indexed.join("stale"), b"stale")?; + fs::create_dir_all(dir.path().join("blocks"))?; + fs::write(dir.path().join("blocks/xor.dat"), [0_u8; 3])?; + let reader = empty_reader(dir.path()); + + assert!(Indexer::import(dir.path(), &reader).is_err()); + assert!(indexed.join("stale").exists()); + Ok(()) + } + + #[test] + fn xor_marker_io_error_never_deletes_data() -> Result<()> { + let dir = tempfile::tempdir()?; + let indexed = dir.path().join("indexed"); + let marker = indexed.join("xor.dat"); + let reader = empty_reader(dir.path()); + drop(Indexer::import(dir.path(), &reader)?); + fs::remove_file(&marker)?; + fs::create_dir(&marker)?; + fs::write(indexed.join("stale"), b"stale")?; + + assert!(Indexer::import(dir.path(), &reader).is_err()); + assert!(indexed.join("stale").exists()); + Ok(()) + } + + #[test] + fn checkpoint_io_error_never_deletes_data() -> Result<()> { + let dir = tempfile::tempdir()?; + let indexed = dir.path().join("indexed"); + let checkpoint = indexed.join("stores/height"); + let reader = empty_reader(dir.path()); + drop(Indexer::import(dir.path(), &reader)?); + fs::remove_file(&checkpoint)?; + fs::create_dir(&checkpoint)?; + fs::write(indexed.join("stale"), b"stale")?; + + assert!(Indexer::import(dir.path(), &reader).is_err()); + assert!(indexed.join("stale").exists()); + Ok(()) + } + + #[test] + fn block_position_is_verified_against_its_header() -> Result<()> { + let dir = tempfile::tempdir()?; + let blocks = dir.path().join("blocks"); + fs::create_dir(&blocks)?; + let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Bitcoin); + fs::write( + blocks.join("blk00000.dat"), + bitcoin::consensus::serialize(&genesis.header), + )?; + let reader = empty_reader(dir.path()); + + assert_eq!( + read_block_hash_at(&reader, BlkPosition::new(0, 0))?, + BlockHash::from(genesis.block_hash()) + ); + Ok(()) + } + + #[test] + fn fjall_lock_never_triggers_deletion() { + let error = Error::from(fjall::Error::Locked); + + assert!(error.is_lock_error()); + assert!(!error.is_data_error()); + } + + #[test] + fn invalid_checkpoint_drops_handles_and_recreates_entire_index() -> Result<()> { + let dir = tempfile::tempdir()?; + let indexed = dir.path().join("indexed"); + let reader = empty_reader(dir.path()); + + { + let mut indexer = Indexer::import(dir.path(), &reader)?; + indexer + .inner + .stores + .insert_block_height(BlockHashPrefix::from(1_u64), Height::ZERO); + let checkpoint = indexer.inner.stores.begin_commit(Height::ZERO)?; + let persisted = indexer.inner.stores.persist(checkpoint)?; + drop(persisted); + } + fs::write(indexed.join("stale"), b"stale")?; + + let indexer = Indexer::import(dir.path(), &reader)?; + + assert!(!indexed.join("stale").exists()); + assert_eq!(indexer.vecs().next_height(), Height::ZERO); + assert_eq!(indexer.stores().next_height()?, Some(Height::ZERO)); + Ok(()) + } +} diff --git a/crates/brk_indexer/src/processor/block/mod.rs b/crates/brk_indexer/src/processor/block/mod.rs index 896ef4e99..0342c7dcd 100644 --- a/crates/brk_indexer/src/processor/block/mod.rs +++ b/crates/brk_indexer/src/processor/block/mod.rs @@ -4,6 +4,7 @@ use tracing::error; use vecdb::{WritableVec, unlikely}; use super::{BlockProcessor, transaction::ComputedTx}; +use crate::stores::IndexerStores as _; impl BlockProcessor<'_> { pub(crate) fn process_block_metadata(&mut self) -> Result<()> { @@ -14,9 +15,8 @@ impl BlockProcessor<'_> { if unlikely(self.check_collisions) && self .stores - .blockhash_prefix_to_height - .get(&blockhash_prefix)? - .is_some_and(|prev_height| *prev_height != height) + .block_height(&blockhash_prefix)? + .is_some_and(|prev_height| prev_height != height) { error!("BlockHash: {blockhash}"); return Err(Error::Internal("BlockHash prefix collision")); @@ -24,9 +24,7 @@ impl BlockProcessor<'_> { self.lengths.push(self.vecs); - self.stores - .blockhash_prefix_to_height - .insert(blockhash_prefix, height); + self.stores.insert_block_height(blockhash_prefix, height); self.vecs .blocks diff --git a/crates/brk_indexer/src/processor/buffer.rs b/crates/brk_indexer/src/processor/buffer.rs index 1bde47c1a..957b9f1e1 100644 --- a/crates/brk_indexer/src/processor/buffer.rs +++ b/crates/brk_indexer/src/processor/buffer.rs @@ -21,9 +21,4 @@ impl BlockBuffers { pub(crate) fn finish_block(&mut self, blockhash: BlockHash) { self.tip = Some(blockhash); } - - pub(crate) fn reset(&mut self) { - self.addresses.clear_cache(); - self.tip = None; - } } diff --git a/crates/brk_indexer/src/processor/transaction/mod.rs b/crates/brk_indexer/src/processor/transaction/mod.rs index a428346ef..e7c410ce0 100644 --- a/crates/brk_indexer/src/processor/transaction/mod.rs +++ b/crates/brk_indexer/src/processor/transaction/mod.rs @@ -8,8 +8,11 @@ use rayon::prelude::*; use tracing::error; use vecdb::{AnyVec, WritableVec, likely, unlikely}; -use crate::constants::DUPLICATE_TXIDS; -use crate::{TransactionCounts, TransactionFeaturesVecs, TxMetadataVecs}; +use crate::{ + TransactionCounts, TransactionFeaturesVecs, TxMetadataVecs, + constants::DUPLICATE_TXIDS, + stores::{IndexerStores as _, TransactionStoresMut}, +}; pub(super) use computed::ComputedTx; @@ -39,11 +42,7 @@ impl<'a> BlockProcessor<'a> { true } else { let txid_prefix = TxidPrefix::from(&txid); - let prev_tx_index = self - .stores - .txid_prefix_to_tx_index - .get(&txid_prefix)? - .map(|value| *value); + let prev_tx_index = self.stores.tx_index(&txid_prefix)?; if let Some(prev_tx_index) = prev_tx_index { self.validate_txid_collision(tx_index, prev_tx_index)?; @@ -125,10 +124,12 @@ impl<'a> BlockProcessor<'a> { let transaction_features = &mut self.vecs.transaction_features; let height = self.height; - let addr_hash_stores = &mut self.stores.addr_type_to_addr_hash_to_addr_index; - let addr_tx_index_stores = &mut self.stores.addr_type_to_addr_index_and_tx_index; - let addr_outpoint_stores = &mut self.stores.addr_type_to_addr_index_and_unspent_outpoint; - let txid_prefix_store = &mut self.stores.txid_prefix_to_tx_index; + let TransactionStoresMut { + addr_hashes, + addr_tx_indexes, + addr_unspent_outpoints, + txid_prefixes, + } = self.stores.transaction_stores_mut(); rayon::join( || { @@ -141,9 +142,9 @@ impl<'a> BlockProcessor<'a> { addrs, scripts, op_return, - addr_hash_stores, - addr_tx_index_stores, - addr_outpoint_stores, + addr_hashes, + addr_tx_indexes, + addr_unspent_outpoints, &mut txouts, addresses, ); @@ -153,8 +154,8 @@ impl<'a> BlockProcessor<'a> { base_txin_index, first_txin_index, inputs, - addr_tx_index_stores, - addr_outpoint_stores, + addr_tx_indexes, + addr_unspent_outpoints, txins, &txouts, ); @@ -164,7 +165,7 @@ impl<'a> BlockProcessor<'a> { height, txs, transaction_analyses, - txid_prefix_store, + txid_prefixes, &mut tx_metadata, transaction_features, ) diff --git a/crates/brk_indexer/src/processor/txin/resolver.rs b/crates/brk_indexer/src/processor/txin/resolver.rs index 0929a1831..387d99088 100644 --- a/crates/brk_indexer/src/processor/txin/resolver.rs +++ b/crates/brk_indexer/src/processor/txin/resolver.rs @@ -241,11 +241,7 @@ impl ReadBatch { .zip(previous_parent_prefixes.par_iter()) .try_for_each(|read| { let (read, txid_prefix) = read; - let store_result = processor - .stores - .txid_prefix_to_tx_index - .get(txid_prefix)? - .map(|value| *value); + let store_result = processor.stores.tx_index(txid_prefix)?; let tx_index = match store_result { Some(tx_index) if tx_index < current_tx_index => tx_index, diff --git a/crates/brk_indexer/src/processor/txout/address.rs b/crates/brk_indexer/src/processor/txout/address.rs index fd4a5115d..2f4f40ca0 100644 --- a/crates/brk_indexer/src/processor/txout/address.rs +++ b/crates/brk_indexer/src/processor/txout/address.rs @@ -71,16 +71,14 @@ impl BlockAddresses { self.lookups .sort_unstable_by_key(|lookup| (lookup.output_type, lookup.hash)); - let stores = &processor.stores.addr_type_to_addr_hash_to_addr_index; let lengths = &*processor.lengths; self.lookups .par_iter_mut() .try_for_each(|lookup| -> Result<()> { - lookup.type_index = stores - .get_unwrap(lookup.output_type) - .get(&lookup.hash)? - .map(|type_index| *type_index) + lookup.type_index = processor + .stores + .addr_index(lookup.output_type, &lookup.hash)? .filter(|type_index| *type_index < lengths.to_type_index(lookup.output_type)); Ok(()) })?; diff --git a/crates/brk_indexer/src/safe_lengths.rs b/crates/brk_indexer/src/safe_lengths.rs index 80ddb8c44..f86bfb3ef 100644 --- a/crates/brk_indexer/src/safe_lengths.rs +++ b/crates/brk_indexer/src/safe_lengths.rs @@ -20,10 +20,6 @@ impl SafeLengths { self.0.read().clone() } - pub fn reset(&self) { - *self.0.write() = Lengths::default(); - } - pub fn advance(&self, next: Lengths) { let mut g = self.0.write(); debug_assert!( diff --git a/crates/brk_indexer/src/stores.rs b/crates/brk_indexer/src/stores.rs index 3b6fb93da..8d7aac669 100644 --- a/crates/brk_indexer/src/stores.rs +++ b/crates/brk_indexer/src/stores.rs @@ -1,54 +1,161 @@ -use std::{fs, path::Path, time::Instant}; +use std::{fs, ops::Range, path::Path, time::Instant}; use rustc_hash::FxHashSet; use brk_cohort::ByAddrType; -use brk_error::{Error, Result}; -use brk_store::{AnyStore, Kind, Mode, Store}; +use brk_error::{Error, OptionData, Result}; +use brk_store::{AnyStore, Kind, Mode, PendingIngest, Store}; use brk_types::{ AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, BlockHashPrefix, Height, OutPoint, OutputType, TxIndex, TxOutIndex, TxidPrefix, TypeIndex, Unit, Version, Vout, }; -use fjall::{Database, PersistMode}; +use fjall::Database; use rayon::prelude::*; -use tracing::{debug, info}; +use tracing::debug; use vecdb::{AnyVec, ReadableVec, VecIndex}; -use crate::{Lengths, constants::DUPLICATE_TXID_PREFIXES}; +use crate::{Lengths, constants::DUPLICATE_TXID_PREFIXES, vecs::IndexerVecs as _}; use super::Vecs; +mod checkpoint; + +use checkpoint::{ + DeferredStoresCommit, PendingStoresCheckpoint, PersistedStoresCheckpoint, StoresCheckpoint, +}; + #[derive(Clone)] pub struct Stores { - pub db: Database, + inner: StoresInner, +} - pub addr_type_to_addr_hash_to_addr_index: ByAddrType>, - pub addr_type_to_addr_index_and_tx_index: ByAddrType>, - pub addr_type_to_addr_index_and_unspent_outpoint: ByAddrType>, - pub blockhash_prefix_to_height: Store, - pub txid_prefix_to_tx_index: Store, +#[derive(Clone)] +struct StoresInner { + db: Database, + checkpoint: StoresCheckpoint, + + addr_type_to_addr_hash_to_addr_index: ByAddrType>, + addr_type_to_addr_index_and_tx_index: ByAddrType>, + addr_type_to_addr_index_and_unspent_outpoint: ByAddrType>, + blockhash_prefix_to_height: Store, + txid_prefix_to_tx_index: Store, +} + +pub struct TransactionStoresMut<'a> { + pub addr_hashes: &'a mut ByAddrType>, + pub addr_tx_indexes: &'a mut ByAddrType>, + pub addr_unspent_outpoints: &'a mut ByAddrType>, + pub txid_prefixes: &'a mut Store, +} + +pub trait IndexerStores: Sized { + fn forced_import(parent: &Path, version: Version) -> Result; + fn next_height(&self) -> Result>; + fn begin_commit(&self, completed_height: Height) -> Result; + fn persist(&mut self, checkpoint: PendingStoresCheckpoint) + -> Result; + fn take_deferred_commit(&mut self, completed_height: Height) -> Result; + fn rollback_if_needed(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()>; + fn insert_block_height(&mut self, prefix: BlockHashPrefix, height: Height); + fn transaction_stores_mut(&mut self) -> TransactionStoresMut<'_>; } impl Stores { - pub fn forced_import(parent: &Path, version: Version) -> Result { - Self::forced_import_inner(parent, version, true) + #[inline] + pub fn addr_index(&self, addr_type: OutputType, hash: &AddrHash) -> Result> { + Ok(self + .inner + .addr_type_to_addr_hash_to_addr_index + .get(addr_type) + .data()? + .get(hash)? + .map(|index| index.into_owned())) } - fn forced_import_inner(parent: &Path, version: Version, can_retry: bool) -> Result { + pub fn addr_hash_range( + &self, + addr_type: OutputType, + range: Range, + ) -> Result + '_> { + Ok(self + .inner + .addr_type_to_addr_hash_to_addr_index + .get(addr_type) + .data()? + .range(range)) + } + + pub fn addr_tx_indexes( + &self, + addr_type: OutputType, + addr_index: TypeIndex, + ) -> Result + '_> { + Ok(self + .inner + .addr_type_to_addr_index_and_tx_index + .get(addr_type) + .data()? + .prefix(addr_index) + .map(|(key, _)| key.tx_index())) + } + + pub fn addr_tx_indexes_before( + &self, + addr_type: OutputType, + addr_index: TypeIndex, + before: TxIndex, + ) -> Result + '_> { + let min = AddrIndexTxIndex::min_for_addr(addr_index); + let cursor = AddrIndexTxIndex::from((addr_index, before)); + Ok(self + .inner + .addr_type_to_addr_index_and_tx_index + .get(addr_type) + .data()? + .range(min..cursor) + .map(|(key, _)| key.tx_index())) + } + + pub fn addr_unspent_outpoints( + &self, + addr_type: OutputType, + addr_index: TypeIndex, + ) -> Result + '_> { + Ok(self + .inner + .addr_type_to_addr_index_and_unspent_outpoint + .get(addr_type) + .data()? + .prefix(addr_index) + .map(|(key, _)| (key.tx_index(), key.vout()))) + } + + #[inline] + pub fn block_height(&self, prefix: &BlockHashPrefix) -> Result> { + Ok(self + .inner + .blockhash_prefix_to_height + .get(prefix)? + .map(|height| height.into_owned())) + } + + #[inline] + pub fn tx_index(&self, prefix: &TxidPrefix) -> Result> { + Ok(self + .inner + .txid_prefix_to_tx_index + .get(prefix)? + .map(|index| index.into_owned())) + } +} + +impl StoresInner { + fn open(parent: &Path, version: Version) -> Result { let pathbuf = parent.join("stores"); let path = pathbuf.as_path(); fs::create_dir_all(&pathbuf)?; - - let database = match brk_store::open_database(path) { - Ok(database) => database, - Err(err) if can_retry => { - info!("Failed to open stores at {path:?}: {err:?}, deleting and retrying"); - fs::remove_dir_all(path)?; - return Self::forced_import_inner(parent, version, false); - } - Err(err) => return Err(err.into()), - }; + let database = brk_store::open_database(path)?; let database_ref = &database; @@ -87,6 +194,7 @@ impl Stores { let stores = Self { db: database.clone(), + checkpoint: StoresCheckpoint::new(path), addr_type_to_addr_hash_to_addr_index: ByAddrType::new_with_index( create_addr_hash_to_addr_index_store, @@ -116,37 +224,15 @@ impl Stores { )?, }; + if stores.checkpoint.next_height()?.is_none() && stores.is_empty()? { + stores.checkpoint.initialize_empty()?; + } + Ok(stores) } - pub fn next_height(&self) -> Height { - self.iter_any() - .map(|store| store.height().map(Height::incremented).unwrap_or_default()) - .min() - .unwrap() - } - - fn iter_any(&self) -> impl Iterator { - [ - &self.blockhash_prefix_to_height as &dyn AnyStore, - &self.txid_prefix_to_tx_index, - ] - .into_iter() - .chain( - self.addr_type_to_addr_hash_to_addr_index - .values() - .map(|s| s as &dyn AnyStore), - ) - .chain( - self.addr_type_to_addr_index_and_tx_index - .values() - .map(|s| s as &dyn AnyStore), - ) - .chain( - self.addr_type_to_addr_index_and_unspent_outpoint - .values() - .map(|s| s as &dyn AnyStore), - ) + fn checkpoint_height(&self) -> Result> { + self.checkpoint.next_height() } fn par_iter_any_mut(&mut self) -> impl ParallelIterator { @@ -172,32 +258,34 @@ impl Stores { ) } - pub fn commit(&mut self, height: Height) -> Result<()> { - let i = Instant::now(); - self.par_iter_any_mut() - .try_for_each(|store| store.commit(height))?; - debug!("Stores committed in {:?}", i.elapsed()); + fn prepare_checkpoint(&self, completed_height: Height) -> Result { + self.checkpoint.begin(completed_height) + } + + fn persist_checkpoint( + &mut self, + checkpoint: PendingStoresCheckpoint, + ) -> Result { + let db = self.db.clone(); let i = Instant::now(); - self.db.persist(PersistMode::SyncData)?; + let persisted = checkpoint.persist(&db, || { + self.par_iter_any_mut() + .try_for_each(|store| store.ingest_pending()) + })?; debug!("Stores persisted in {:?}", i.elapsed()); - Ok(()) + Ok(persisted) } /// Takes all pending puts/dels from every store and returns closures /// that can ingest them on a background thread. - #[allow(clippy::type_complexity)] - pub fn take_all_pending_ingests( - &mut self, - height: Height, - ) -> Result Result<()> + Send>>> { - let h = height; + fn take_pending_ingests(&mut self) -> Vec { let mut tasks = Vec::new(); macro_rules! take { ($store:expr) => { - tasks.extend($store.take_pending_ingest(h)?); + tasks.extend($store.take_pending_ingest()); }; } @@ -217,16 +305,21 @@ impl Stores { take!(store); } - Ok(tasks) + tasks } - /// Rewrites reverse-key entries below the lowered bound. In-flight - /// readers may briefly see torn state. - pub fn rollback_if_needed( - &mut self, - vecs: &mut Vecs, - starting_lengths: &Lengths, - ) -> Result<()> { + fn defer_commit(&mut self, completed_height: Height) -> Result { + let checkpoint = self.checkpoint.begin(completed_height)?; + let ingests = self.take_pending_ingests(); + Ok(DeferredStoresCommit::new( + self.db.clone(), + ingests, + checkpoint, + )) + } + + /// Stages reverse-key entries below the lowered bound for persistence. + fn rollback(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> { if self.is_empty()? { return Ok(()); } @@ -239,11 +332,6 @@ impl Stores { self.rollback_txids(vecs, starting_lengths); self.rollback_outputs_and_inputs(vecs, starting_lengths)?; - let rollback_height = starting_lengths.height.decremented().unwrap_or_default(); - self.par_iter_any_mut() - .try_for_each(|store| store.export_meta(rollback_height))?; - self.commit(rollback_height)?; - Ok(()) } @@ -264,11 +352,7 @@ impl Stores { .try_fold(true, |acc, s| s.is_empty().map(|empty| acc && empty))?) } - fn rollback_block_metadata( - &mut self, - vecs: &mut Vecs, - starting_lengths: &Lengths, - ) -> Result<()> { + fn rollback_block_metadata(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> { vecs.blocks.blockhash.for_each_range_at( starting_lengths.height.to_usize(), vecs.blocks.blockhash.len(), @@ -289,7 +373,7 @@ impl Stores { Ok(()) } - fn rollback_txids(&mut self, vecs: &mut Vecs, starting_lengths: &Lengths) { + fn rollback_txids(&mut self, vecs: &Vecs, starting_lengths: &Lengths) { let start = starting_lengths.tx_index.to_usize(); let end = vecs.transactions.txid.len(); let mut current_index = start; @@ -317,7 +401,7 @@ impl Stores { fn rollback_outputs_and_inputs( &mut self, - vecs: &mut Vecs, + vecs: &Vecs, starting_lengths: &Lengths, ) -> Result<()> { let tx_index_to_first_txout_index_reader = vecs.transactions.first_txout_index.reader(); @@ -414,6 +498,50 @@ impl Stores { } } +impl IndexerStores for Stores { + fn forced_import(parent: &Path, version: Version) -> Result { + Ok(Self { + inner: StoresInner::open(parent, version)?, + }) + } + + fn next_height(&self) -> Result> { + self.inner.checkpoint_height() + } + + fn begin_commit(&self, completed_height: Height) -> Result { + self.inner.prepare_checkpoint(completed_height) + } + + fn persist( + &mut self, + checkpoint: PendingStoresCheckpoint, + ) -> Result { + self.inner.persist_checkpoint(checkpoint) + } + + fn take_deferred_commit(&mut self, completed_height: Height) -> Result { + self.inner.defer_commit(completed_height) + } + + fn rollback_if_needed(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> { + self.inner.rollback(vecs, starting_lengths) + } + + fn insert_block_height(&mut self, prefix: BlockHashPrefix, height: Height) { + self.inner.blockhash_prefix_to_height.insert(prefix, height); + } + + fn transaction_stores_mut(&mut self) -> TransactionStoresMut<'_> { + TransactionStoresMut { + addr_hashes: &mut self.inner.addr_type_to_addr_hash_to_addr_index, + addr_tx_indexes: &mut self.inner.addr_type_to_addr_index_and_tx_index, + addr_unspent_outpoints: &mut self.inner.addr_type_to_addr_index_and_unspent_outpoint, + txid_prefixes: &mut self.inner.txid_prefix_to_tx_index, + } + } +} + fn valid_rollback_boundaries( first_txout_indexes: &[TxOutIndex], rollback_start: usize, @@ -456,8 +584,65 @@ fn txout_ranges( #[cfg(test)] mod tests { + use fjall::PersistMode; + use super::*; + #[test] + fn empty_stores_initialize_zero_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let stores = Stores::forced_import(dir.path(), Version::ZERO)?; + + assert_eq!(stores.next_height()?, Some(Height::ZERO)); + Ok(()) + } + + #[test] + fn missing_checkpoint_with_data_stays_invalid() -> Result<()> { + let dir = tempfile::tempdir()?; + + { + let mut stores = Stores::forced_import(dir.path(), Version::ZERO)?; + let inner = &mut stores.inner; + inner + .blockhash_prefix_to_height + .insert(BlockHashPrefix::from(1_u64), Height::ZERO); + inner + .blockhash_prefix_to_height + .take_pending_ingest() + .unwrap()()?; + inner.db.persist(PersistMode::SyncData)?; + + let pending_checkpoint = inner.checkpoint.begin(Height::ZERO)?; + drop(pending_checkpoint); + } + + let reopened = Stores::forced_import(dir.path(), Version::ZERO)?; + assert_eq!(reopened.next_height()?, None); + Ok(()) + } + + #[test] + fn synchronous_commit_persists_data_and_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let prefix = BlockHashPrefix::from(1_u64); + + { + let mut stores = Stores::forced_import(dir.path(), Version::ZERO)?; + stores + .inner + .blockhash_prefix_to_height + .insert(prefix, Height::ZERO); + let checkpoint = stores.begin_commit(Height::new(42))?; + stores.persist(checkpoint)?.publish()?; + } + + let reopened = Stores::forced_import(dir.path(), Version::ZERO)?; + assert_eq!(reopened.next_height()?, Some(Height::new(43))); + assert_eq!(reopened.block_height(&prefix)?, Some(Height::ZERO)); + Ok(()) + } + #[test] fn rollback_output_ranges_reconstruct_tx_indexes_and_vouts() { let first_txout_indexes = [100_usize, 103, 103, 105].map(TxOutIndex::from); diff --git a/crates/brk_indexer/src/stores/checkpoint.rs b/crates/brk_indexer/src/stores/checkpoint.rs new file mode 100644 index 000000000..c76c66751 --- /dev/null +++ b/crates/brk_indexer/src/stores/checkpoint.rs @@ -0,0 +1,296 @@ +use std::{ + fs::{self, File}, + io::{self, ErrorKind}, + path::{Path, PathBuf}, +}; + +use brk_error::Result; +use brk_store::PendingIngest; +use brk_types::Height; +use fjall::{Database, PersistMode}; +use rayon::prelude::*; + +/// Sole durability marker for the shared stores database. +/// +/// The file contains the next block height to index. A missing or malformed +/// file means a commit was interrupted and the stores must not be resumed. +#[derive(Debug, Clone)] +pub struct StoresCheckpoint { + path: PathBuf, +} + +impl StoresCheckpoint { + pub fn new(stores_path: &Path) -> Self { + Self { + path: stores_path.join("height"), + } + } + + pub fn next_height(&self) -> Result> { + let bytes = match fs::read(&self.path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + let Ok(bytes) = <[u8; size_of::()]>::try_from(bytes) else { + return Ok(None); + }; + Ok(Some(Height::new(u32::from_le_bytes(bytes)))) + } + + pub fn initialize_empty(&self) -> Result<()> { + let pending_path = self.invalidate()?; + PersistedStoresCheckpoint(PendingStoresCheckpoint { + next_height: Height::ZERO, + path: self.path.clone(), + pending_path, + }) + .publish() + } + + pub fn begin(&self, completed_height: Height) -> Result { + let pending_path = self.invalidate()?; + + Ok(PendingStoresCheckpoint { + next_height: completed_height.incremented(), + path: self.path.clone(), + pending_path, + }) + } + + fn invalidate(&self) -> Result { + let pending_path = self.path.with_extension("pending"); + let removed_checkpoint = remove_if_exists(&self.path)?; + let removed_pending = remove_if_exists(&pending_path)?; + + if removed_checkpoint || removed_pending { + sync_parent(&self.path)?; + } + + Ok(pending_path) + } +} + +#[must_use = "dropping a pending checkpoint leaves the stores checkpoint invalid"] +pub struct PendingStoresCheckpoint { + next_height: Height, + path: PathBuf, + pending_path: PathBuf, +} + +impl PendingStoresCheckpoint { + pub fn persist( + self, + db: &Database, + ingest: impl FnOnce() -> Result<()>, + ) -> Result { + ingest()?; + db.persist(PersistMode::SyncData)?; + Ok(PersistedStoresCheckpoint(self)) + } +} + +#[must_use = "publish this checkpoint after every related database is durable"] +pub struct PersistedStoresCheckpoint(PendingStoresCheckpoint); + +impl PersistedStoresCheckpoint { + pub fn publish(self) -> Result<()> { + let pending = self.0; + pending.next_height.write(&pending.pending_path)?; + File::open(&pending.pending_path)?.sync_all()?; + fs::rename(&pending.pending_path, &pending.path)?; + sync_parent(&pending.path)?; + Ok(()) + } +} + +#[must_use = "persist this deferred commit before publishing its checkpoint"] +pub struct DeferredStoresCommit { + checkpoint: PendingStoresCheckpoint, + db: Database, + ingests: Vec, +} + +impl DeferredStoresCommit { + pub fn new( + db: Database, + ingests: Vec, + checkpoint: PendingStoresCheckpoint, + ) -> Self { + Self { + checkpoint, + db, + ingests, + } + } + + pub fn persist(self) -> Result { + self.checkpoint.persist(&self.db, || { + self.ingests.into_par_iter().try_for_each(|ingest| ingest()) + }) + } +} + +fn remove_if_exists(path: &Path) -> io::Result { + match fs::remove_file(path) { + Ok(()) => Ok(true), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(false), + Err(err) => Err(err), + } +} + +fn sync_parent(path: &Path) -> io::Result<()> { + File::open(path.parent().expect("checkpoint has a parent"))?.sync_all() +} + +#[cfg(test)] +mod tests { + use brk_error::Error; + use brk_store::{Kind, Mode, Store}; + use brk_types::{AddrIndexTxIndex, TxIndex, TypeIndex, Unit, Version}; + + use super::*; + + fn key(address: u32, transaction: u32) -> AddrIndexTxIndex { + AddrIndexTxIndex::from((TypeIndex::new(address), TxIndex::new(transaction))) + } + + fn open_store(db: &Database, path: &Path, name: &str) -> Result> { + Store::import(db, path, name, Version::ZERO, Mode::Any, Kind::Vec) + } + + #[test] + fn dropped_commit_leaves_checkpoint_invalid() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + let db = brk_store::open_database(dir.path())?; + + checkpoint + .begin(Height::new(41))? + .persist(&db, || Ok(()))? + .publish()?; + assert_eq!(checkpoint.next_height()?, Some(Height::new(42))); + + let pending = checkpoint.begin(Height::new(42))?; + assert_eq!(checkpoint.next_height()?, None); + drop(pending); + + let reopened = StoresCheckpoint::new(dir.path()); + assert_eq!(reopened.next_height()?, None); + Ok(()) + } + + #[test] + fn failed_ingest_does_not_publish_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + let pending = checkpoint.begin(Height::new(42))?; + let db = brk_store::open_database(dir.path())?; + let ingests: Vec = vec![Box::new(|| { + Err(Error::Internal("simulated ingest failure")) + })]; + + assert!( + DeferredStoresCommit::new(db, ingests, pending) + .persist() + .is_err() + ); + assert_eq!(checkpoint.next_height()?, None); + Ok(()) + } + + #[test] + fn persisted_commit_is_not_published_early() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + let pending = checkpoint.begin(Height::new(42))?; + let db = brk_store::open_database(dir.path())?; + + let persisted = DeferredStoresCommit::new(db, vec![], pending).persist()?; + assert_eq!(checkpoint.next_height()?, None); + persisted.publish()?; + + let reopened = StoresCheckpoint::new(dir.path()); + assert_eq!(reopened.next_height()?, Some(Height::new(43))); + Ok(()) + } + + #[test] + fn dropped_deferred_ingest_reopens_without_value_or_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + + { + let db = brk_store::open_database(dir.path())?; + let mut store = open_store(&db, dir.path(), "dropped_deferred_ingest")?; + + checkpoint + .begin(Height::new(41))? + .persist(&db, || Ok(()))? + .publish()?; + store.insert(key(1, 1), Unit); + + let pending_checkpoint = checkpoint.begin(Height::new(42))?; + let pending_ingest = store.take_pending_ingest().unwrap(); + drop(pending_ingest); + drop(pending_checkpoint); + } + + let db = brk_store::open_database(dir.path())?; + let store = open_store(&db, dir.path(), "dropped_deferred_ingest")?; + + assert_eq!(checkpoint.next_height()?, None); + assert!(store.get(&key(1, 1))?.is_none()); + Ok(()) + } + + #[test] + fn successful_ingest_reopens_with_value_and_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + + { + let db = brk_store::open_database(dir.path())?; + let mut store = open_store(&db, dir.path(), "successful_ingest")?; + store.insert(key(1, 1), Unit); + + let pending_checkpoint = checkpoint.begin(Height::new(42))?; + let pending_ingest = store.take_pending_ingest().unwrap(); + DeferredStoresCommit::new(db.clone(), vec![pending_ingest], pending_checkpoint) + .persist()? + .publish()?; + } + + let db = brk_store::open_database(dir.path())?; + let store = open_store(&db, dir.path(), "successful_ingest")?; + + assert_eq!(checkpoint.next_height()?, Some(Height::new(43))); + assert!(store.get(&key(1, 1))?.is_some()); + Ok(()) + } + + #[test] + fn empty_database_has_an_explicit_zero_checkpoint() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + + assert_eq!(checkpoint.next_height()?, None); + checkpoint.initialize_empty()?; + assert_eq!(checkpoint.next_height()?, Some(Height::ZERO)); + Ok(()) + } + + #[test] + fn malformed_checkpoint_is_invalid_but_io_errors_propagate() -> Result<()> { + let dir = tempfile::tempdir()?; + let checkpoint = StoresCheckpoint::new(dir.path()); + + fs::write(&checkpoint.path, [0_u8; 3])?; + assert_eq!(checkpoint.next_height()?, None); + + fs::remove_file(&checkpoint.path)?; + fs::create_dir(&checkpoint.path)?; + assert!(checkpoint.next_height().is_err()); + Ok(()) + } +} diff --git a/crates/brk_indexer/src/vecs/macros.rs b/crates/brk_indexer/src/vecs/macros.rs index 6be993eef..1450152f2 100644 --- a/crates/brk_indexer/src/vecs/macros.rs +++ b/crates/brk_indexer/src/vecs/macros.rs @@ -1,5 +1,5 @@ /// Imports multiple items in parallel using thread::scope. -/// Each expression must return Result. +/// Each expression must return `Result`. /// /// # Example /// ```ignore diff --git a/crates/brk_indexer/src/vecs/mod.rs b/crates/brk_indexer/src/vecs/mod.rs index 563ce5502..bc44d9da5 100644 --- a/crates/brk_indexer/src/vecs/mod.rs +++ b/crates/brk_indexer/src/vecs/mod.rs @@ -4,11 +4,11 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{AddrHash, Height, OutputType, Version}; use rayon::prelude::*; -use vecdb::{AnyStoredVec, Database, Rw, Stamp, StorageMode}; +use vecdb::{AnyStoredVec, AnyVec, Database, RawDBError, Rw, Stamp, StorageMode}; const PAGE_SIZE: usize = 4096; -use crate::parallel_import; +use crate::{Lengths, parallel_import}; mod addrs; mod blocks; @@ -27,12 +27,10 @@ pub use outputs::*; pub use scripts::*; pub use transactions::*; -use crate::Lengths; - #[derive(Traversable)] pub struct Vecs { #[traversable(skip)] - pub db: Database, + db: Database, pub blocks: BlocksVecs, #[traversable(wrap = "transactions", rename = "raw")] pub transactions: TransactionsVecs, @@ -50,8 +48,25 @@ pub struct Vecs { pub op_return: OpReturnVecs, } -impl Vecs { - pub fn forced_import(parent: &Path, version: Version) -> Result { +pub trait IndexerVecs: Sized { + fn forced_import(parent: &Path, version: Version) -> Result; + fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()>; + fn flush(&mut self, height: Height) -> Result<()>; + fn stamped_write(&mut self, height: Height) -> Result<()>; + fn sync_bg_tasks(&self) -> Result<()>; + fn run_bg( + &self, + f: impl FnOnce(&Database) -> std::result::Result<(), RawDBError> + Send + 'static, + ); + fn iter_addr_hashes_from( + &self, + addr_type: OutputType, + height: Height, + ) -> Result + '_>>; +} + +impl IndexerVecs for Vecs { + fn forced_import(parent: &Path, version: Version) -> Result { tracing::debug!("Opening vecs database..."); let db = Database::open(&parent.join("vecs"))?; tracing::debug!("Setting min len..."); @@ -99,7 +114,7 @@ impl Vecs { Ok(this) } - pub fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()> { + fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()> { let saved_height = starting_lengths.height.decremented().unwrap_or_default(); let stamp = Stamp::from(u64::from(saved_height)); @@ -150,46 +165,49 @@ impl Vecs { Ok(()) } - pub fn flush(&mut self, height: Height) -> Result<()> { + fn flush(&mut self, height: Height) -> Result<()> { self.stamped_write(height)?; self.db.flush()?; Ok(()) } - pub fn next_height(&self) -> Height { - self.iter_any_stored_vec() - .map(|vec| { - let h = Height::from(vec.stamp()); - if h > Height::ZERO { h.incremented() } else { h } - }) - .min() - .unwrap() - } - - pub fn stamped_write(&mut self, height: Height) -> Result<()> { + fn stamped_write(&mut self, height: Height) -> Result<()> { self.par_iter_mut_any_stored_vec() .try_for_each(|vec| vec.stamped_write(Stamp::from(height)))?; Ok(()) } - pub fn compact(&self) -> Result<()> { - self.db.compact()?; + fn sync_bg_tasks(&self) -> Result<()> { + self.db.sync_bg_tasks()?; Ok(()) } - pub fn reset(&mut self) -> Result<()> { - self.par_iter_mut_any_stored_vec() - .try_for_each(|vec| vec.any_reset())?; - Ok(()) + fn run_bg( + &self, + f: impl FnOnce(&Database) -> std::result::Result<(), RawDBError> + Send + 'static, + ) { + self.db.run_bg(f); } - pub fn iter_addr_hashes_from( + fn iter_addr_hashes_from( &self, addr_type: OutputType, height: Height, ) -> Result + '_>> { self.addrs.iter_hashes_from(addr_type, height) } +} + +impl Vecs { + pub fn next_height(&self) -> Height { + let min_stamp = self + .iter_any_stored_vec() + .map(|vec| vec.stamp()) + .min() + .unwrap(); + + next_height_from_min_stamp(min_stamp, !self.blocks.blockhash.is_empty()) + } fn par_iter_mut_any_stored_vec( &mut self, @@ -217,3 +235,32 @@ impl Vecs { .chain(self.op_return.iter_any()) } } + +fn next_height_from_min_stamp(min_stamp: Stamp, has_blocks: bool) -> Height { + if has_blocks { + Height::from(min_stamp).incremented() + } else { + Height::ZERO + } +} + +#[cfg(test)] +mod checkpoint_tests { + use super::*; + + #[test] + fn zero_stamp_distinguishes_empty_from_genesis() { + let zero = Stamp::from(0_u64); + + assert_eq!(next_height_from_min_stamp(zero, false), Height::ZERO); + assert_eq!(next_height_from_min_stamp(zero, true), Height::new(1)); + } + + #[test] + fn nonzero_stamp_advances_to_next_height() { + assert_eq!( + next_height_from_min_stamp(Stamp::from(41_u64), true), + Height::new(42) + ); + } +} diff --git a/crates/brk_oracle/Cargo.toml b/crates/brk_oracle/Cargo.toml index 08d719575..e7ac65454 100644 --- a/crates/brk_oracle/Cargo.toml +++ b/crates/brk_oracle/Cargo.toml @@ -13,5 +13,7 @@ brk_types = { workspace = true } [dev-dependencies] brk_indexer = { workspace = true } +brk_reader = { workspace = true } +brk_rpc = { workspace = true } serde_json = { workspace = true } vecdb = { workspace = true } diff --git a/crates/brk_oracle/examples/common/mod.rs b/crates/brk_oracle/examples/common/mod.rs new file mode 100644 index 000000000..80130bd0f --- /dev/null +++ b/crates/brk_oracle/examples/common/mod.rs @@ -0,0 +1,16 @@ +use std::path::Path; + +use brk_indexer::Indexer; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; + +pub fn import_indexer(data_dir: &Path) -> Indexer { + let bitcoin_dir = Client::default_bitcoin_path(); + let client = Client::new( + Client::default_url(), + Auth::CookieFile(bitcoin_dir.join(".cookie")), + ) + .expect("Failed to connect to Bitcoin Core"); + let reader = Reader::new(bitcoin_dir.join("blocks"), &client); + Indexer::import(data_dir, &reader).expect("Failed to import indexer") +} diff --git a/crates/brk_oracle/examples/determinism.rs b/crates/brk_oracle/examples/determinism.rs index 1145c10e8..100b065c0 100644 --- a/crates/brk_oracle/examples/determinism.rs +++ b/crates/brk_oracle/examples/determinism.rs @@ -11,7 +11,6 @@ use std::path::PathBuf; -use brk_indexer::Indexer; use brk_oracle::{ Config, HistogramRaw, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, @@ -19,6 +18,8 @@ use brk_oracle::{ use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; +mod common; + struct Block { height: usize, values: Vec, @@ -52,8 +53,8 @@ fn main() { PathBuf::from(home).join(".brk") }); - let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); - let total_heights = indexer.vecs.blocks.timestamp.len(); + let indexer = common::import_indexer(&data_dir); + let total_heights = indexer.vecs().blocks.timestamp.len(); let fast_config = Config::default(); let window_size = fast_config.window_size; @@ -75,12 +76,12 @@ fn main() { "Loading {} blocks ({load_start}..{end_height})...", end_height - load_start ); - let total_txs = indexer.vecs.transactions.txid.len(); - let total_outputs = indexer.vecs.outputs.value.len(); - let first_tx_index: Vec = indexer.vecs.transactions.first_tx_index.collect(); - let out_first: Vec = indexer.vecs.outputs.first_txout_index.collect(); + let total_txs = indexer.vecs().transactions.txid.len(); + let total_outputs = indexer.vecs().outputs.value.len(); + let first_tx_index: Vec = indexer.vecs().transactions.first_tx_index.collect(); + let out_first: Vec = indexer.vecs().outputs.first_txout_index.collect(); let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -109,12 +110,12 @@ fn main() { let out_start = tx_starts.first().copied().unwrap_or(out_end); let values: Vec = indexer - .vecs + .vecs() .outputs .value .collect_range_at(out_start, out_end); let output_types: Vec = indexer - .vecs + .vecs() .outputs .output_type .collect_range_at(out_start, out_end); diff --git a/crates/brk_oracle/examples/dump_hist.rs b/crates/brk_oracle/examples/dump_hist.rs index fbcfa1e5f..af4f4c0b2 100644 --- a/crates/brk_oracle/examples/dump_hist.rs +++ b/crates/brk_oracle/examples/dump_hist.rs @@ -21,10 +21,11 @@ use std::{ path::PathBuf, }; -use brk_indexer::Indexer; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; +mod common; + fn main() { let data_dir = std::env::var("BRK_DIR") .map(PathBuf::from) @@ -39,8 +40,8 @@ fn main() { .and_then(|s| s.parse().ok()) .unwrap_or(510_000); - let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); - let total_heights = indexer.vecs.blocks.timestamp.len(); + let indexer = common::import_indexer(&data_dir); + let total_heights = indexer.vecs().blocks.timestamp.len(); let end = end.min(total_heights); let manifest_dir = env!("CARGO_MANIFEST_DIR"); @@ -50,13 +51,13 @@ fn main() { ) .expect("parse height OHLC"); - let timestamps: Vec = indexer.vecs.blocks.timestamp.collect(); - let total_txs = indexer.vecs.transactions.txid.len(); - let total_outputs = indexer.vecs.outputs.value.len(); - let first_tx_index: Vec = indexer.vecs.transactions.first_tx_index.collect(); - let out_first: Vec = indexer.vecs.outputs.first_txout_index.collect(); + let timestamps: Vec = indexer.vecs().blocks.timestamp.collect(); + let total_txs = indexer.vecs().transactions.txid.len(); + let total_outputs = indexer.vecs().outputs.value.len(); + let first_tx_index: Vec = indexer.vecs().transactions.first_tx_index.collect(); + let out_first: Vec = indexer.vecs().outputs.first_txout_index.collect(); let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -99,12 +100,12 @@ fn main() { let out_start = tx_starts.first().copied().unwrap_or(out_end); let values: Vec = indexer - .vecs + .vecs() .outputs .value .collect_range_at(out_start, out_end); let output_types: Vec = indexer - .vecs + .vecs() .outputs .output_type .collect_range_at(out_start, out_end); diff --git a/crates/brk_oracle/examples/experiment.rs b/crates/brk_oracle/examples/experiment.rs index df86a0eee..f216f5c6d 100644 --- a/crates/brk_oracle/examples/experiment.rs +++ b/crates/brk_oracle/examples/experiment.rs @@ -9,7 +9,6 @@ use std::{cmp::Ordering, env, path::PathBuf}; -use brk_indexer::Indexer; use brk_oracle::{ BINS_PER_DECADE, Config, NUM_BINS, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, seed_bin as oracle_seed_bin, @@ -17,6 +16,8 @@ use brk_oracle::{ use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; +mod common; + const GENESIS_DAY: u32 = 14252; const BINS_5PCT: f64 = 4.24; const BINS_10PCT: f64 = 8.28; @@ -455,8 +456,8 @@ fn main() { .unwrap_or(START_HEIGHT_SLOW) .max(START_HEIGHT_SLOW); - let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); - let total_heights = indexer.vecs.blocks.timestamp.len(); + let indexer = common::import_indexer(&data_dir); + let total_heights = indexer.vecs().blocks.timestamp.len(); let end = end_override.unwrap_or(total_heights).min(total_heights); let manifest_dir = env!("CARGO_MANIFEST_DIR"); @@ -478,7 +479,7 @@ fn main() { }) .collect(); - let timestamps: Vec = indexer.vecs.blocks.timestamp.collect(); + let timestamps: Vec = indexer.vecs().blocks.timestamp.collect(); let height_years: Vec = timestamps .iter() .map(|ts| timestamp_to_year(**ts)) @@ -530,12 +531,12 @@ fn main() { .map(|cfg| Variant::new(cfg, seed_bin)) .collect(); - let total_txs = indexer.vecs.transactions.txid.len(); - let total_outputs = indexer.vecs.outputs.value.len(); - let first_tx_index: Vec = indexer.vecs.transactions.first_tx_index.collect(); - let out_first: Vec = indexer.vecs.outputs.first_txout_index.collect(); + let total_txs = indexer.vecs().transactions.txid.len(); + let total_outputs = indexer.vecs().outputs.value.len(); + let first_tx_index: Vec = indexer.vecs().transactions.first_tx_index.collect(); + let out_first: Vec = indexer.vecs().outputs.first_txout_index.collect(); let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -580,11 +581,11 @@ fn main() { let out_start = tx_starts.first().copied().unwrap_or(out_end); indexer - .vecs + .vecs() .outputs .value .collect_range_into_at(out_start, out_end, &mut values); - indexer.vecs.outputs.output_type.collect_range_into_at( + indexer.vecs().outputs.output_type.collect_range_into_at( out_start, out_end, &mut output_types, diff --git a/crates/brk_oracle/examples/report.rs b/crates/brk_oracle/examples/report.rs index f1734d159..208955ada 100644 --- a/crates/brk_oracle/examples/report.rs +++ b/crates/brk_oracle/examples/report.rs @@ -4,13 +4,14 @@ use std::path::PathBuf; -use brk_indexer::Indexer; use brk_oracle::{ Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, }; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; +mod common; + /// Day1 1 = Jan 9, 2009 (block 1). For dates after genesis week: /// day1 = floor(timestamp / 86400) - 14252. const GENESIS_DAY: u32 = 14252; @@ -131,8 +132,8 @@ fn main() { PathBuf::from(home).join(".brk") }); - let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); - let total_heights = indexer.vecs.blocks.timestamp.len(); + let indexer = common::import_indexer(&data_dir); + let total_heights = indexer.vecs().blocks.timestamp.len(); let manifest_dir = env!("CARGO_MANIFEST_DIR"); let height_ohlc: Vec<[f64; 4]> = serde_json::from_str( @@ -161,7 +162,7 @@ fn main() { .collect(); // Read block timestamps for year + day1 mapping. - let timestamps: Vec = indexer.vecs.blocks.timestamp.collect(); + let timestamps: Vec = indexer.vecs().blocks.timestamp.collect(); let height_years: Vec = timestamps .iter() .map(|ts| timestamp_to_year(**ts)) @@ -173,15 +174,15 @@ fn main() { let mut oracle = Oracle::from_seed(); - let total_txs = indexer.vecs.transactions.txid.len(); - let total_outputs = indexer.vecs.outputs.value.len(); + let total_txs = indexer.vecs().transactions.txid.len(); + let total_outputs = indexer.vecs().outputs.value.len(); // Pre-collect height-indexed vecs (small). Transaction-indexed vecs are too // large, so the tx-indexed first_txout_index is read through a forward cursor. - let first_tx_index: Vec = indexer.vecs.transactions.first_tx_index.collect(); - let out_first: Vec = indexer.vecs.outputs.first_txout_index.collect(); + let first_tx_index: Vec = indexer.vecs().transactions.first_tx_index.collect(); + let out_first: Vec = indexer.vecs().outputs.first_txout_index.collect(); let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -225,12 +226,12 @@ fn main() { let out_start = tx_starts.first().copied().unwrap_or(out_end); let values: Vec = indexer - .vecs + .vecs() .outputs .value .collect_range_at(out_start, out_end); let output_types: Vec = indexer - .vecs + .vecs() .outputs .output_type .collect_range_at(out_start, out_end); diff --git a/crates/brk_oracle/examples/report_from.rs b/crates/brk_oracle/examples/report_from.rs index 25af8bf8f..025f7e95a 100644 --- a/crates/brk_oracle/examples/report_from.rs +++ b/crates/brk_oracle/examples/report_from.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; -use brk_indexer::Indexer; use brk_oracle::{ Config, HistogramEma, HistogramRaw, NUM_BINS, PaymentFilter, START_HEIGHT_FAST, bin_to_cents, cents_to_bin, pre_oracle_price_cents, @@ -15,6 +14,8 @@ use brk_oracle::{ use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; +mod common; + /// Day1 1 = Jan 9, 2009 (block 1). For dates after genesis week: /// day1 = floor(timestamp / 86400) - 14252. const GENESIS_DAY: u32 = 14252; @@ -542,8 +543,8 @@ fn main() { .and_then(|s| s.parse().ok()) .unwrap_or(5000); - let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer"); - let total_heights = indexer.vecs.blocks.timestamp.len(); + let indexer = common::import_indexer(&data_dir); + let total_heights = indexer.vecs().blocks.timestamp.len(); let manifest_dir = env!("CARGO_MANIFEST_DIR"); let height_ohlc: Vec<[f64; 4]> = serde_json::from_str( @@ -572,7 +573,7 @@ fn main() { .collect(); // Read block timestamps for year + day1 mapping. - let timestamps: Vec = indexer.vecs.blocks.timestamp.collect(); + let timestamps: Vec = indexer.vecs().blocks.timestamp.collect(); let height_years: Vec = timestamps .iter() .map(|ts| timestamp_to_year(**ts)) @@ -835,15 +836,15 @@ fn main() { let mut sharp_ema = HistogramEma::zeros(); eprintln!(" sharp: span={sharp_span:.0} window={sharp_window} alpha={sharp_alpha:.5}"); - let total_txs = indexer.vecs.transactions.txid.len(); - let total_outputs = indexer.vecs.outputs.value.len(); + let total_txs = indexer.vecs().transactions.txid.len(); + let total_outputs = indexer.vecs().outputs.value.len(); // Pre-collect height-indexed vecs (small). Transaction-indexed vecs are too // large, so the tx-indexed first_txout_index is read through a forward cursor. - let first_tx_index: Vec = indexer.vecs.transactions.first_tx_index.collect(); - let out_first: Vec = indexer.vecs.outputs.first_txout_index.collect(); + let first_tx_index: Vec = indexer.vecs().transactions.first_tx_index.collect(); + let out_first: Vec = indexer.vecs().outputs.first_txout_index.collect(); let mut txout_cursor = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() @@ -893,12 +894,12 @@ fn main() { let out_start = tx_starts.first().copied().unwrap_or(out_end); let values: Vec = indexer - .vecs + .vecs() .outputs .value .collect_range_at(out_start, out_end); let output_types: Vec = indexer - .vecs + .vecs() .outputs .output_type .collect_range_at(out_start, out_end); diff --git a/crates/brk_query/README.md b/crates/brk_query/README.md index b63302164..9046d1489 100644 --- a/crates/brk_query/README.md +++ b/crates/brk_query/README.md @@ -18,7 +18,7 @@ Query blocks, transactions, addresses, and 1000+ on-chain metrics through a unif ## Core API ```rust,ignore -let query = Query::build(&reader, &indexer, &computer, Some(mempool)); +let query = Query::build(&indexer, &computer, Some(mempool)); // Current height let height = query.height(); @@ -55,7 +55,7 @@ let stats = query.address(address)?; ## Async Usage ```rust,ignore -let async_query = AsyncQuery::build(&reader, &indexer, &computer, mempool); +let async_query = AsyncQuery::build(&indexer, &computer, mempool); // Run blocking queries in thread pool let result = async_query.run(|q| q.block_by_height(height)).await; diff --git a/crates/brk_query/examples/list.rs b/crates/brk_query/examples/list.rs index 08da2b661..30eb4995e 100644 --- a/crates/brk_query/examples/list.rs +++ b/crates/brk_query/examples/list.rs @@ -3,13 +3,17 @@ use std::{env, fs, path::Path}; use brk_computer::Computer; use brk_indexer::Indexer; use brk_query::Vecs; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use vecdb::ReadOnlyClone; pub fn main() -> brk_error::Result<()> { let tmp = env::temp_dir().join("brk_search_gen"); fs::create_dir_all(&tmp)?; - let indexer = Indexer::forced_import(&tmp)?; + let client = Client::new("http://127.0.0.1:1", Auth::None)?; + let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client); + let indexer = Indexer::import(&tmp, &reader)?; let computer = Computer::forced_import(&tmp, &indexer)?; let indexer_ro = indexer.read_only_clone(); diff --git a/crates/brk_query/examples/query.rs b/crates/brk_query/examples/query.rs index fd9cd9b33..d9cf5c125 100644 --- a/crates/brk_query/examples/query.rs +++ b/crates/brk_query/examples/query.rs @@ -7,7 +7,7 @@ use brk_mempool::Mempool; use brk_query::Query; use brk_reader::Reader; use brk_rpc::{Auth, Client}; -use brk_types::{Addr, OutputType}; +use brk_types::Addr; use vecdb::Exit; pub fn main() -> Result<()> { @@ -33,7 +33,7 @@ pub fn main() -> Result<()> { let reader = Reader::new(blocks_dir, &client); - let indexer = Indexer::forced_import(&outputs_dir)?; + let indexer = Indexer::import(&outputs_dir, &reader)?; let computer = Computer::forced_import(&outputs_dir, &indexer)?; @@ -43,15 +43,7 @@ pub fn main() -> Result<()> { mempool_clone.start(); }); - let query = Query::build(&reader, &indexer, &computer, Some(mempool)); - - dbg!( - indexer - .stores - .addr_type_to_addr_hash_to_addr_index - .get_unwrap(OutputType::P2WSH) - .approximate_len() - ); + let query = Query::build(&indexer, &computer, Some(mempool)); let _ = dbg!(query.addr(Addr::from( "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string(), diff --git a/crates/brk_query/src/async.rs b/crates/brk_query/src/async.rs index 6ed76e68c..da11a15e7 100644 --- a/crates/brk_query/src/async.rs +++ b/crates/brk_query/src/async.rs @@ -2,7 +2,6 @@ use brk_computer::Computer; use brk_error::Result; use brk_indexer::Indexer; use brk_mempool::Mempool; -use brk_reader::Reader; use tokio::task::spawn_blocking; use crate::Query; @@ -11,13 +10,8 @@ use crate::Query; pub struct AsyncQuery(Query); impl AsyncQuery { - pub fn build( - reader: &Reader, - indexer: &Indexer, - computer: &Computer, - mempool: Option, - ) -> Self { - Self(Query::build(reader, indexer, computer, mempool)) + pub fn build(indexer: &Indexer, computer: &Computer, mempool: Option) -> Self { + Self(Query::build(indexer, computer, mempool)) } /// Run a blocking query operation on a spawn_blocking thread. diff --git a/crates/brk_query/src/impl/addr/activity.rs b/crates/brk_query/src/impl/addr/activity.rs index 024b3203d..7d20b8e2b 100644 --- a/crates/brk_query/src/impl/addr/activity.rs +++ b/crates/brk_query/src/impl/addr/activity.rs @@ -1,5 +1,5 @@ -use brk_error::{Error, OptionData, Result}; -use brk_types::{Addr, AddrIndexTxIndex, Height, Txid, Unit}; +use brk_error::{Error, Result}; +use brk_types::{Addr, Height, Txid}; use crate::Query; @@ -14,29 +14,20 @@ impl Query { before_txid: Option<&Txid>, ) -> Result { let (output_type, type_index) = self.resolve_addr(addr)?; - let store = self - .indexer() - .stores - .addr_type_to_addr_index_and_tx_index - .get(output_type) - .data()?; + let stores = self.indexer().stores(); let tx_index_len = self.safe_lengths().tx_index; let last_tx_index = match before_txid { Some(txid) => { let before_tx_index = self.resolve_tx_index(txid)?; - let min = AddrIndexTxIndex::min_for_addr(type_index); - let cursor = AddrIndexTxIndex::from((type_index, before_tx_index)); - store - .range(min..cursor) + stores + .addr_tx_indexes_before(output_type, type_index, before_tx_index)? .rev() - .map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index()) .find(|tx_index| *tx_index < tx_index_len) .ok_or(Error::UnknownAddr)? } - None => store - .prefix(type_index) + None => stores + .addr_tx_indexes(output_type, type_index)? .rev() - .map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index()) .find(|tx_index| *tx_index < tx_index_len) .ok_or(Error::UnknownAddr)?, }; diff --git a/crates/brk_query/src/impl/addr/hash_prefix.rs b/crates/brk_query/src/impl/addr/hash_prefix.rs index 886faac80..cd0904fca 100644 --- a/crates/brk_query/src/impl/addr/hash_prefix.rs +++ b/crates/brk_query/src/impl/addr/hash_prefix.rs @@ -1,4 +1,4 @@ -use brk_error::{Error, OptionData, Result}; +use brk_error::{Error, Result}; use brk_types::{Addr, AddrHash, AddrHashPrefixMatches, OutputType}; use crate::Query; @@ -16,19 +16,14 @@ impl Query { } let prefix = AddrHashPrefix::parse(prefix)?; - let store = self - .indexer() - .stores - .addr_type_to_addr_hash_to_addr_index - .get(addr_type) - .data()?; + let stores = self.indexer().stores(); let safe_type_index = self.safe_lengths().to_type_index(addr_type); - let addr_readers = self.indexer().vecs.addrs.addr_readers(); + let addr_readers = self.indexer().vecs().addrs.addr_readers(); let mut addresses = Vec::new(); let max_hash = AddrHash::new(u64::MAX); if let Some(upper) = prefix.upper { - for (_, type_index) in store.range(prefix.lower..upper) { + for (_, type_index) in stores.addr_hash_range(addr_type, prefix.lower..upper)? { if type_index >= safe_type_index { continue; } @@ -41,7 +36,7 @@ impl Query { } } } else { - for (_, type_index) in store.range(prefix.lower..max_hash) { + for (_, type_index) in stores.addr_hash_range(addr_type, prefix.lower..max_hash)? { if type_index >= safe_type_index { continue; } @@ -55,7 +50,7 @@ impl Query { } if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT - && let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned()) + && let Some(type_index) = stores.addr_index(addr_type, &max_hash)? && type_index < safe_type_index { let script = addr_readers.script_pubkey(addr_type, type_index); diff --git a/crates/brk_query/src/impl/addr/resolve.rs b/crates/brk_query/src/impl/addr/resolve.rs index 07e29807c..192591616 100644 --- a/crates/brk_query/src/impl/addr/resolve.rs +++ b/crates/brk_query/src/impl/addr/resolve.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use brk_error::{Error, OptionData, Result}; +use brk_error::{Error, Result}; use brk_types::{Addr, AddrBytes, AddrHash, OutputType, TypeIndex}; use crate::Query; @@ -22,12 +22,8 @@ impl Query { hash: &AddrHash, ) -> Result { self.indexer() - .stores - .addr_type_to_addr_hash_to_addr_index - .get(output_type) - .data()? - .get(hash)? - .map(|cow| cow.into_owned()) + .stores() + .addr_index(output_type, hash)? .ok_or(Error::UnknownAddr) } } diff --git a/crates/brk_query/src/impl/addr/txs.rs b/crates/brk_query/src/impl/addr/txs.rs index 4eb09aad2..b932c8110 100644 --- a/crates/brk_query/src/impl/addr/txs.rs +++ b/crates/brk_query/src/impl/addr/txs.rs @@ -1,5 +1,5 @@ -use brk_error::{OptionData, Result}; -use brk_types::{Addr, AddrIndexTxIndex, Transaction, TxIndex, Txid, Unit}; +use brk_error::Result; +use brk_types::{Addr, Transaction, TxIndex, Txid}; use crate::Query; @@ -21,7 +21,7 @@ impl Query { limit: usize, ) -> Result> { let txindices = self.addr_txindices(&addr, after_txid, limit)?; - let txid_reader = self.indexer().vecs.transactions.txid.reader(); + let txid_reader = self.indexer().vecs().transactions.txid.reader(); Ok(txindices .into_iter() .map(|tx_index| txid_reader.get(tx_index)) @@ -34,33 +34,23 @@ impl Query { after_txid: Option, limit: usize, ) -> Result> { - let stores = &self.indexer().stores; + let stores = self.indexer().stores(); let (output_type, type_index) = self.resolve_addr(addr)?; - - let store = stores - .addr_type_to_addr_index_and_tx_index - .get(output_type) - .data()?; - let tx_index_len = self.safe_lengths().tx_index; if let Some(after_txid) = after_txid { let after_tx_index = self.resolve_tx_index(&after_txid)?; - let min = AddrIndexTxIndex::min_for_addr(type_index); - let cursor = AddrIndexTxIndex::from((type_index, after_tx_index)); - Ok(store - .range(min..cursor) + Ok(stores + .addr_tx_indexes_before(output_type, type_index, after_tx_index)? .rev() - .map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index()) .filter(|tx_index| *tx_index < tx_index_len) .take(limit) .collect()) } else { - Ok(store - .prefix(type_index) + Ok(stores + .addr_tx_indexes(output_type, type_index)? .rev() - .map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index()) .filter(|tx_index| *tx_index < tx_index_len) .take(limit) .collect()) diff --git a/crates/brk_query/src/impl/addr/utxos.rs b/crates/brk_query/src/impl/addr/utxos.rs index 49635c378..de700b0e9 100644 --- a/crates/brk_query/src/impl/addr/utxos.rs +++ b/crates/brk_query/src/impl/addr/utxos.rs @@ -1,25 +1,19 @@ -use brk_error::{Error, OptionData, Result}; -use brk_types::{Addr, AddrIndexOutPoint, Height, TxIndex, TxStatus, Unit, Utxo, Vout}; +use brk_error::{Error, Result}; +use brk_types::{Addr, Height, TxIndex, TxStatus, Utxo, Vout}; use crate::Query; impl Query { pub fn addr_utxos(&self, addr: Addr, max_utxos: usize) -> Result> { let indexer = self.indexer(); - let stores = &indexer.stores; - let vecs = &indexer.vecs; + let stores = indexer.stores(); + let vecs = indexer.vecs(); let (output_type, type_index) = self.resolve_addr(&addr)?; - let store = stores - .addr_type_to_addr_index_and_unspent_outpoint - .get(output_type) - .data()?; - let tx_index_len = self.safe_lengths().tx_index; - let outpoints: Vec<(TxIndex, Vout)> = store - .prefix(type_index) - .map(|(key, _): (AddrIndexOutPoint, Unit)| (key.tx_index(), key.vout())) + let outpoints: Vec<(TxIndex, Vout)> = stores + .addr_unspent_outpoints(output_type, type_index)? .filter(|(tx_index, _)| *tx_index < tx_index_len) .take(max_utxos + 1) .collect(); diff --git a/crates/brk_query/src/impl/block/info.rs b/crates/brk_query/src/impl/block/info.rs index 208ce8a80..212c42f54 100644 --- a/crates/brk_query/src/impl/block/info.rs +++ b/crates/brk_query/src/impl/block/info.rs @@ -85,7 +85,7 @@ impl Query { if height >= self.safe_lengths().height { return Err(Error::OutOfRange("Block height out of range".into())); } - self.indexer().vecs.blocks.blockhash.get(height).data() + self.indexer().vecs().blocks.blockhash.get(height).data() } /// Most recent `count` blocks ending at `start_height` (default tip), @@ -122,12 +122,16 @@ impl Query { // Bulk read all indexed data. `end <= safe.height` ⇒ these per-block // vecs are populated for `[begin, end)`, so short reads are impossible. - let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end); - let difficulties = indexer.vecs.blocks.difficulty.collect_range_at(begin, end); - let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(begin, end); - let sizes = indexer.vecs.blocks.total.collect_range_at(begin, end); - let weights = indexer.vecs.blocks.weight.collect_range_at(begin, end); - let positions = indexer.vecs.blocks.position.collect_range_at(begin, end); + let blockhashes = indexer.vecs().blocks.blockhash.collect_range_at(begin, end); + let difficulties = indexer + .vecs() + .blocks + .difficulty + .collect_range_at(begin, end); + let timestamps = indexer.vecs().blocks.timestamp.collect_range_at(begin, end); + let sizes = indexer.vecs().blocks.total.collect_range_at(begin, end); + let weights = indexer.vecs().blocks.weight.collect_range_at(begin, end); + let positions = indexer.vecs().blocks.position.collect_range_at(begin, end); debug_assert_eq!(blockhashes.len(), count); debug_assert_eq!(difficulties.len(), count); debug_assert_eq!(timestamps.len(), count); @@ -139,7 +143,7 @@ impl Query { // exclusive height bound. Tip block falls back to `tx_index_len` in the loop. let tx_index_end = (end + 1).min(height_len); let first_tx_indexes: Vec = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(begin, tx_index_end); @@ -148,7 +152,7 @@ impl Query { // Bulk read median time window let median_start = begin.saturating_sub(10); let median_timestamps: Vec = indexer - .vecs + .vecs() .blocks .timestamp .collect_range_at(median_start, end); @@ -210,12 +214,16 @@ impl Query { let all_pools = pools(); // Bulk read all indexed data - let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end); - let difficulties = indexer.vecs.blocks.difficulty.collect_range_at(begin, end); - let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(begin, end); - let sizes = indexer.vecs.blocks.total.collect_range_at(begin, end); - let weights = indexer.vecs.blocks.weight.collect_range_at(begin, end); - let positions = indexer.vecs.blocks.position.collect_range_at(begin, end); + let blockhashes = indexer.vecs().blocks.blockhash.collect_range_at(begin, end); + let difficulties = indexer + .vecs() + .blocks + .difficulty + .collect_range_at(begin, end); + let timestamps = indexer.vecs().blocks.timestamp.collect_range_at(begin, end); + let sizes = indexer.vecs().blocks.total.collect_range_at(begin, end); + let weights = indexer.vecs().blocks.weight.collect_range_at(begin, end); + let positions = indexer.vecs().blocks.position.collect_range_at(begin, end); let pool_slugs = computer.pools.pool.collect_range_at(begin, end); let pool_block_numbers = computer .pools @@ -226,16 +234,24 @@ impl Query { // exclusive height bound. Tip block falls back to `tx_index_len` in the loop. let tx_index_end = (end + 1).min(height_len); let first_tx_indexes: Vec = indexer - .vecs + .vecs() .transactions .first_tx_index .collect_range_at(begin, tx_index_end); // Bulk read segwit stats - let segwit_txs = indexer.vecs.blocks.segwit_txs.collect_range_at(begin, end); - let segwit_sizes = indexer.vecs.blocks.segwit_size.collect_range_at(begin, end); + let segwit_txs = indexer + .vecs() + .blocks + .segwit_txs + .collect_range_at(begin, end); + let segwit_sizes = indexer + .vecs() + .blocks + .segwit_size + .collect_range_at(begin, end); let segwit_weights = indexer - .vecs + .vecs() .blocks .segwit_weight .collect_range_at(begin, end); @@ -310,7 +326,7 @@ impl Query { // Bulk read median time window let median_start = begin.saturating_sub(10); let median_timestamps = indexer - .vecs + .vecs() .blocks .timestamp .collect_range_at(median_start, end); @@ -515,15 +531,13 @@ impl Query { let indexer = self.indexer(); let prefix = BlockHashPrefix::from(hash); let height = indexer - .stores - .blockhash_prefix_to_height - .get(&prefix)? - .map(|h| *h) + .stores() + .block_height(&prefix)? .ok_or(Error::NotFound("Block not found".into()))?; if height >= self.safe_lengths().height { return Err(Error::NotFound("Block not found".into())); } - match indexer.vecs.blocks.blockhash.get(height) { + match indexer.vecs().blocks.blockhash.get(height) { Some(stored) if &stored == hash => Ok(height), _ => Err(Error::NotFound("Block not found".into())), } @@ -536,7 +550,7 @@ impl Query { pub fn read_block_header(&self, height: Height) -> Result { let position = self .indexer() - .vecs + .vecs() .blocks .position .collect_one(height) diff --git a/crates/brk_query/src/impl/block/raw.rs b/crates/brk_query/src/impl/block/raw.rs index 7a3e26d99..3c2223e94 100644 --- a/crates/brk_query/src/impl/block/raw.rs +++ b/crates/brk_query/src/impl/block/raw.rs @@ -19,8 +19,8 @@ impl Query { } let indexer = self.indexer(); - let position = indexer.vecs.blocks.position.collect_one(height).data()?; - let size = indexer.vecs.blocks.total.collect_one(height).data()?; + let position = indexer.vecs().blocks.position.collect_one(height).data()?; + let size = indexer.vecs().blocks.total.collect_one(height).data()?; self.reader().read_raw_bytes(position, *size as usize) } diff --git a/crates/brk_query/src/impl/block/status.rs b/crates/brk_query/src/impl/block/status.rs index d1924e4b2..22c8b143d 100644 --- a/crates/brk_query/src/impl/block/status.rs +++ b/crates/brk_query/src/impl/block/status.rs @@ -20,7 +20,7 @@ impl Query { let next_best = if height < tip { Some( self.indexer() - .vecs + .vecs() .blocks .blockhash .get(height.incremented()) diff --git a/crates/brk_query/src/impl/block/timestamp.rs b/crates/brk_query/src/impl/block/timestamp.rs index 74a5624da..5e3330f18 100644 --- a/crates/brk_query/src/impl/block/timestamp.rs +++ b/crates/brk_query/src/impl/block/timestamp.rs @@ -41,7 +41,7 @@ impl Query { let start: usize = usize::from(first_height_of_day).min(tip); - let mut ts_cursor = indexer.vecs.blocks.timestamp.cursor(); + let mut ts_cursor = indexer.vecs().blocks.timestamp.cursor(); let mut best: Option<(usize, Timestamp)> = None; let mut above_streak = 0usize; @@ -82,7 +82,7 @@ impl Query { best.ok_or_else(|| Error::NotFound("No block at or before timestamp".into()))?; let height = Height::from(best_height); - let blockhash = indexer.vecs.blocks.blockhash.collect_one(height).data()?; + let blockhash = indexer.vecs().blocks.blockhash.collect_one(height).data()?; let ts_secs: i64 = (*best_ts).into(); let iso_timestamp = JiffTimestamp::from_second(ts_secs) diff --git a/crates/brk_query/src/impl/block/txs.rs b/crates/brk_query/src/impl/block/txs.rs index a93ab685f..03311e78e 100644 --- a/crates/brk_query/src/impl/block/txs.rs +++ b/crates/brk_query/src/impl/block/txs.rs @@ -66,7 +66,7 @@ impl Query { let (first, tx_count) = self.block_tx_range(height)?; let txids = self .indexer() - .vecs + .vecs() .transactions .txid .collect_range_at(first, first + tx_count); @@ -85,7 +85,7 @@ impl Query { return Err(Error::OutOfRange("Transaction index out of range".into())); } self.indexer() - .vecs + .vecs() .transactions .txid .collect_one_at(first + index) @@ -124,11 +124,11 @@ impl Query { // ── Phase 1: Decode all transactions, collect outpoints ───────── - let txid_cursor = indexer.vecs.transactions.txid.reader().cursor(); - let mut total_size_cursor = indexer.vecs.transactions.total_size.cursor(); - let mut sigops_cursor = indexer.vecs.transactions.total_sigop_cost.cursor(); - let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor(); - let mut position_cursor = indexer.vecs.transactions.position.cursor(); + let txid_cursor = indexer.vecs().transactions.txid.reader().cursor(); + let mut total_size_cursor = indexer.vecs().transactions.total_size.cursor(); + let mut sigops_cursor = indexer.vecs().transactions.total_sigop_cost.cursor(); + let mut first_txin_cursor = indexer.vecs().transactions.first_txin_index.cursor(); + let mut position_cursor = indexer.vecs().transactions.position.cursor(); struct DecodedTx { pos: usize, @@ -191,9 +191,9 @@ impl Query { // sequential cursors avoid re-decompressing the same pages. // Reading output_type/type_index/value HERE from inputs vecs (sequential) // avoids random-reading them from outputs vecs in Phase 2. - let mut outpoint_cursor = indexer.vecs.inputs.outpoint.cursor(); - let mut input_output_type_cursor = indexer.vecs.inputs.output_type.cursor(); - let mut input_type_index_cursor = indexer.vecs.inputs.type_index.cursor(); + let mut outpoint_cursor = indexer.vecs().inputs.outpoint.cursor(); + let mut input_output_type_cursor = indexer.vecs().inputs.output_type.cursor(); + let mut input_type_index_cursor = indexer.vecs().inputs.type_index.cursor(); let mut input_value_cursor = self.computer().inputs.value.cursor(); let mut prevout_input_data: FxHashMap = @@ -220,7 +220,7 @@ impl Query { // Sort by (output_type, type_index) for sequential BytesVec access // within each address type's file. - let addr_readers = indexer.vecs.addrs.addr_readers(); + let addr_readers = indexer.vecs().addrs.addr_readers(); let mut sorted_prevouts: Vec<(OutPoint, OutputType, TypeIndex, Sats)> = Vec::with_capacity(prevout_input_data.len()); @@ -315,7 +315,7 @@ impl Query { if height >= safe.height { return Err(Error::OutOfRange("Block height out of range".into())); } - let first_tx_index_vec = &self.indexer().vecs.transactions.first_tx_index; + let first_tx_index_vec = &self.indexer().vecs().transactions.first_tx_index; let first: usize = first_tx_index_vec.collect_one(height).data()?.into(); let next_height = height.incremented(); let next: usize = if next_height < safe.height { diff --git a/crates/brk_query/src/impl/cpfp/confirmed.rs b/crates/brk_query/src/impl/cpfp/confirmed.rs index 3585b4198..05d8742d6 100644 --- a/crates/brk_query/src/impl/cpfp/confirmed.rs +++ b/crates/brk_query/src/impl/cpfp/confirmed.rs @@ -35,7 +35,7 @@ impl Query { let descendants = self.resolve_entries(&walk.descendants)?; let sigops = self .indexer() - .vecs + .vecs() .transactions .total_sigop_cost .collect_one(seed) @@ -56,9 +56,9 @@ impl Query { ) -> Result> { let indexer = self.indexer(); let computer = self.computer(); - let mut weight = indexer.vecs.transactions.weight.cursor(); + let mut weight = indexer.vecs().transactions.weight.cursor(); let mut fee = computer.transactions.fees.fee.tx_index.cursor(); - let txid = indexer.vecs.transactions.txid.reader(); + let txid = indexer.vecs().transactions.txid.reader(); members .iter() @@ -79,9 +79,9 @@ impl Query { fn resolve_entries(&self, indexes: &[TxIndex]) -> Result> { let indexer = self.indexer(); let computer = self.computer(); - let mut weight = indexer.vecs.transactions.weight.cursor(); + let mut weight = indexer.vecs().transactions.weight.cursor(); let mut fee = computer.transactions.fees.fee.tx_index.cursor(); - let txid = indexer.vecs.transactions.txid.reader(); + let txid = indexer.vecs().transactions.txid.reader(); indexes .iter() @@ -100,7 +100,7 @@ impl Query { let indexer = self.indexer(); let computer = self.computer(); let safe = self.safe_lengths(); - let first_tx = &indexer.vecs.transactions.first_tx_index; + let first_tx = &indexer.vecs().transactions.first_tx_index; let block_first = first_tx.collect_one(height).data()?; let next_height = height.incremented(); let block_end = if next_height < safe.height { @@ -109,18 +109,18 @@ impl Query { safe.tx_index }; - let mut first_txin = indexer.vecs.transactions.first_txin_index.cursor(); + let mut first_txin = indexer.vecs().transactions.first_txin_index.cursor(); let mut input_count = computer.indexes.tx_index.input_count.cursor(); - let mut outpoint = indexer.vecs.inputs.outpoint.cursor(); + let mut outpoint = indexer.vecs().inputs.outpoint.cursor(); let first_txout = indexer - .vecs + .vecs() .transactions .first_txout_index .reader() .cursor(); let mut output_count = computer.indexes.tx_index.output_count.cursor(); let spent = computer.outputs.spent.txin_index.reader().cursor(); - let mut spending_tx = indexer.vecs.inputs.tx_index.cursor(); + let mut spending_tx = indexer.vecs().inputs.tx_index.cursor(); let mut parents_of = |tx: TxIndex| -> Result> { let position = tx.to_usize(); diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index d03a9af76..5f827f0e2 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -50,20 +50,18 @@ impl Query { return FxHashMap::default(); } let safe = indexer.safe_lengths(); - let first_txout_reader = indexer.vecs.transactions.first_txout_index.reader(); - let output_type_reader = indexer.vecs.outputs.output_type.reader(); - let type_index_reader = indexer.vecs.outputs.type_index.reader(); - let value_reader = indexer.vecs.outputs.value.reader(); - let addr_readers = indexer.vecs.addrs.addr_readers(); + let first_txout_reader = indexer.vecs().transactions.first_txout_index.reader(); + let output_type_reader = indexer.vecs().outputs.output_type.reader(); + let type_index_reader = indexer.vecs().outputs.type_index.reader(); + let value_reader = indexer.vecs().outputs.value.reader(); + let addr_readers = indexer.vecs().addrs.addr_readers(); holes .iter() .filter_map(|(prev_txid, vout)| { let prev_tx_index = indexer - .stores - .txid_prefix_to_tx_index - .get(&TxidPrefix::from(prev_txid)) - .ok()?? - .into_owned(); + .stores() + .tx_index(&TxidPrefix::from(prev_txid)) + .ok()??; if prev_tx_index >= safe.tx_index { return None; } diff --git a/crates/brk_query/src/impl/mining/block_sizes.rs b/crates/brk_query/src/impl/mining/block_sizes.rs index bd4ee3125..92feb4871 100644 --- a/crates/brk_query/src/impl/mining/block_sizes.rs +++ b/crates/brk_query/src/impl/mining/block_sizes.rs @@ -14,7 +14,7 @@ impl Query { /// corresponding metric. Single bucket-pass: built via `.map(...).unzip()` /// to avoid re-walking buckets. pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result { - let blocks = &self.indexer().vecs.blocks; + let blocks = &self.indexer().vecs().blocks; let bw = BlockWindow::new(self, time_period)?; let block_sizes: Vec = bw.read(&blocks.total)?; diff --git a/crates/brk_query/src/impl/mining/block_window.rs b/crates/brk_query/src/impl/mining/block_window.rs index 015e8c561..ebe4fbee4 100644 --- a/crates/brk_query/src/impl/mining/block_window.rs +++ b/crates/brk_query/src/impl/mining/block_window.rs @@ -94,7 +94,7 @@ impl BlockWindow { let timestamps: Vec = query .indexer() - .vecs + .vecs() .blocks .timestamp .collect_range(start, end); diff --git a/crates/brk_query/src/impl/mining/difficulty.rs b/crates/brk_query/src/impl/mining/difficulty.rs index ae5e22332..7124decd3 100644 --- a/crates/brk_query/src/impl/mining/difficulty.rs +++ b/crates/brk_query/src/impl/mining/difficulty.rs @@ -53,7 +53,7 @@ impl Query { .collect_one(current_epoch) .data()?; let current_timestamp = indexer - .vecs + .vecs() .blocks .timestamp .collect_one(current_height) @@ -105,13 +105,13 @@ impl Query { .data()?; let prev_difficulty = indexer - .vecs + .vecs() .blocks .difficulty .collect_one(prev_epoch_start) .data()?; let curr_difficulty = indexer - .vecs + .vecs() .blocks .difficulty .collect_one(epoch_start_height) diff --git a/crates/brk_query/src/impl/mining/hashrate.rs b/crates/brk_query/src/impl/mining/hashrate.rs index 8c0937ccd..db3a9b86e 100644 --- a/crates/brk_query/src/impl/mining/hashrate.rs +++ b/crates/brk_query/src/impl/mining/hashrate.rs @@ -40,7 +40,7 @@ impl Query { let current_height = self.height(); let current_difficulty = *indexer - .vecs + .vecs() .blocks .difficulty .collect_one(current_height) diff --git a/crates/brk_query/src/impl/oracle.rs b/crates/brk_query/src/impl/oracle.rs index de8aa215d..25df580e0 100644 --- a/crates/brk_query/src/impl/oracle.rs +++ b/crates/brk_query/src/impl/oracle.rs @@ -230,8 +230,7 @@ impl Query { let indexer = self.indexer(); let safe_height = safe.height.to_usize(); let total_outputs = safe.txout_index.to_usize(); - let first_txout_index = &indexer.vecs.outputs.first_txout_index; - + let first_txout_index = &indexer.vecs().outputs.first_txout_index; let out_start = first_txout_index .collect_one_at(range.start) .unwrap() @@ -245,7 +244,7 @@ impl Query { let mut hist = HistogramRaw::zeros(); indexer - .vecs + .vecs() .outputs .value .for_each_range_at(out_start, out_end, |sats| { diff --git a/crates/brk_query/src/impl/series.rs b/crates/brk_query/src/impl/series.rs index 624a2ca0c..ae10008e1 100644 --- a/crates/brk_query/src/impl/series.rs +++ b/crates/brk_query/src/impl/series.rs @@ -225,7 +225,7 @@ impl Query { } fn entity_index_at(&self, index: Index, h: Height) -> Option { - let v = &self.indexer().vecs; + let v = self.indexer().vecs(); match index { Index::TxIndex => v .transactions diff --git a/crates/brk_query/src/impl/tx.rs b/crates/brk_query/src/impl/tx.rs index ff9133341..fcf312a33 100644 --- a/crates/brk_query/src/impl/tx.rs +++ b/crates/brk_query/src/impl/tx.rs @@ -22,10 +22,8 @@ impl Query { #[inline] pub(crate) fn resolve_tx_index(&self, txid: &Txid) -> Result { self.indexer() - .stores - .txid_prefix_to_tx_index - .get(&TxidPrefix::from(txid))? - .map(|cow| cow.into_owned()) + .stores() + .tx_index(&TxidPrefix::from(txid))? .ok_or(Error::UnknownTxid) } @@ -47,7 +45,7 @@ impl Query { return Err(Error::OutOfRange("Transaction index out of range".into())); } self.indexer() - .vecs + .vecs() .transactions .txid .collect_one(index) @@ -90,8 +88,8 @@ impl Query { #[inline] pub(crate) fn block_hash_and_time(&self, height: Height) -> Result<(BlockHash, Timestamp)> { let indexer = self.indexer(); - let hash = indexer.vecs.blocks.blockhash.collect_one(height).data()?; - let time = indexer.vecs.blocks.timestamp.collect_one(height).data()?; + let hash = indexer.vecs().blocks.blockhash.collect_one(height).data()?; + let time = indexer.vecs().blocks.timestamp.collect_one(height).data()?; Ok((hash, time)) } @@ -224,11 +222,11 @@ impl Query { ) -> Result> { let indexer = self.indexer(); let txin_index_reader = self.computer().outputs.spent.txin_index.reader(); - let txid_reader = indexer.vecs.transactions.txid.reader(); + let txid_reader = indexer.vecs().transactions.txid.reader(); let tx_heights = &self.computer().indexes.tx_heights; - let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor(); - let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor(); + let mut input_tx_cursor = indexer.vecs().inputs.tx_index.cursor(); + let mut first_txin_cursor = indexer.vecs().transactions.first_txin_index.cursor(); let bound = self.safe_lengths(); @@ -277,20 +275,20 @@ impl Query { fn build_outspend(&self, txin_index: TxInIndex) -> Result { let indexer = self.indexer(); let spending_tx_index: TxIndex = indexer - .vecs + .vecs() .inputs .tx_index .collect_one(txin_index) .data()?; let spending_first_txin: TxInIndex = indexer - .vecs + .vecs() .transactions .first_txin_index .collect_one(spending_tx_index) .data()?; let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin)); let spending_txid = indexer - .vecs + .vecs() .transactions .txid .collect_one(spending_tx_index) @@ -317,7 +315,7 @@ impl Query { if tx_index >= safe.tx_index { return Err(Error::UnknownTxid); } - let first_txout_vec = &self.indexer().vecs.transactions.first_txout_index; + let first_txout_vec = &self.indexer().vecs().transactions.first_txout_index; let first_txout_reader = first_txout_vec.reader(); let first = first_txout_reader.try_get(tx_index).ok_or(Error::Internal( "resolve_tx_outputs: first txout index past data", @@ -346,13 +344,13 @@ impl Query { fn transaction_raw_by_index(&self, tx_index: TxIndex) -> Result> { let indexer = self.indexer(); let total_size = indexer - .vecs + .vecs() .transactions .total_size .collect_one(tx_index) .data()?; let position = indexer - .vecs + .vecs() .transactions .position .collect_one(tx_index) @@ -388,7 +386,7 @@ impl Query { let (tx_index, height) = self.resolve_tx(txid)?; let first_tx = self .indexer() - .vecs + .vecs() .transactions .first_tx_index .collect_one(height) diff --git a/crates/brk_query/src/lib.rs b/crates/brk_query/src/lib.rs index e59d5c61d..6ed7a6a4f 100644 --- a/crates/brk_query/src/lib.rs +++ b/crates/brk_query/src/lib.rs @@ -31,8 +31,6 @@ pub use vecs::Vecs; pub struct Query(Arc>); struct QueryInner<'a> { vecs: &'a Vecs<'a>, - client: Client, - reader: Reader, indexer: &'a Indexer, computer: &'a Computer, mempool: Option, @@ -40,22 +38,13 @@ struct QueryInner<'a> { } impl Query { - pub fn build( - reader: &Reader, - indexer: &Indexer, - computer: &Computer, - mempool: Option, - ) -> Self { - let client = reader.client().clone(); - let reader = reader.clone(); + pub fn build(indexer: &Indexer, computer: &Computer, mempool: Option) -> Self { let indexer = Box::leak(Box::new(indexer.read_only_clone())); let computer = Box::leak(Box::new(computer.read_only_clone())); let vecs = Box::leak(Box::new(Vecs::build(indexer, computer))); Self(Arc::new(QueryInner { vecs, - client, - reader, indexer, computer, mempool, @@ -105,7 +94,7 @@ impl Query { let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height)); let last_indexed_at_unix = self .indexer() - .vecs + .vecs() .blocks .timestamp .collect_one(self.height()) @@ -123,17 +112,17 @@ impl Query { #[inline] pub fn reader(&self) -> &Reader { - &self.0.reader + self.0.indexer.reader() } #[inline] pub fn client(&self) -> &Client { - &self.0.client + self.reader().client() } #[inline] pub fn blocks_dir(&self) -> &Path { - self.0.reader.blocks_dir() + self.reader().blocks_dir() } #[inline] diff --git a/crates/brk_query/src/vecs.rs b/crates/brk_query/src/vecs.rs index 2e97fb313..5a85ea464 100644 --- a/crates/brk_query/src/vecs.rs +++ b/crates/brk_query/src/vecs.rs @@ -26,8 +26,8 @@ pub struct Vecs<'a> { impl<'a> Vecs<'a> { pub fn build(indexer: &'a Indexer, computer: &'a Computer) -> Self { Self::build_from( - indexer.vecs.iter_any_visible(), - indexer.vecs.to_tree_node(), + indexer.vecs().iter_any_visible(), + indexer.vecs().to_tree_node(), computer.iter_named_visible(), computer.to_tree_node(), ) @@ -35,8 +35,8 @@ impl<'a> Vecs<'a> { pub fn build_rw(indexer: &'a Indexer, computer: &'a Computer) -> Self { Self::build_from( - indexer.vecs.iter_any_visible(), - indexer.vecs.to_tree_node(), + indexer.vecs().iter_any_visible(), + indexer.vecs().to_tree_node(), computer.iter_named_visible(), computer.to_tree_node(), ) diff --git a/crates/brk_server/examples/bindgen.rs b/crates/brk_server/examples/bindgen.rs index e0f1d66ee..4596fb68a 100644 --- a/crates/brk_server/examples/bindgen.rs +++ b/crates/brk_server/examples/bindgen.rs @@ -4,6 +4,8 @@ use aide::axum::ApiRouter; use brk_computer::Computer; use brk_indexer::Indexer; use brk_query::Vecs; +use brk_reader::Reader; +use brk_rpc::{Auth, Client}; use brk_server::{ApiRoutes, finish_openapi, generate_bindings}; pub fn main() -> color_eyre::Result<()> { @@ -12,7 +14,9 @@ pub fn main() -> color_eyre::Result<()> { let tmp = env::temp_dir().join("brk_bindgen"); fs::create_dir_all(&tmp)?; - let indexer = Indexer::forced_import(&tmp)?; + let client = Client::new("http://127.0.0.1:1", Auth::None)?; + let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client); + let indexer = Indexer::import(&tmp, &reader)?; let computer = Computer::forced_import(&tmp, &indexer)?; let vecs = Vecs::build_rw(&indexer, &computer); diff --git a/crates/brk_server/examples/server.rs b/crates/brk_server/examples/server.rs index 78b7486fe..882d788cb 100644 --- a/crates/brk_server/examples/server.rs +++ b/crates/brk_server/examples/server.rs @@ -23,7 +23,7 @@ pub fn main() -> Result<()> { )?; let reader = Reader::new(bitcoin_dir.join("blocks"), &client); - let indexer = Indexer::forced_import(&outputs_dir)?; + let indexer = Indexer::import(&outputs_dir, &reader)?; let computer = Computer::forced_import(&outputs_dir, &indexer)?; let mempool = Mempool::new(&client); @@ -35,7 +35,7 @@ pub fn main() -> Result<()> { let exit = Exit::new(); exit.set_ctrlc_handler(); - let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool)); + let query = AsyncQuery::build(&indexer, &computer, Some(mempool)); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index bd849a878..1dab20b05 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -55,7 +55,7 @@ impl AppState { self.sync(|q| { let height = q.height(); q.indexer() - .vecs + .vecs() .blocks .timestamp .collect_one(height) diff --git a/crates/brk_store/README.md b/crates/brk_store/README.md index a6498eac8..aea8deea8 100644 --- a/crates/brk_store/README.md +++ b/crates/brk_store/README.md @@ -8,22 +8,24 @@ Persist and query Bitcoin index data (address→outputs, txid→height, etc.) wi ## Key Features -- **Workload-optimized configs**: `Kind::Random` (bloom filters, pinned blocks), `Kind::Recent` (point-read optimized), `Kind::Sequential` (scan-friendly), `Kind::Vec` (append-heavy) -- **Write batching**: Accumulate puts/deletes in memory, commit atomically -- **Tiered caching**: In-memory LRU cache layers before hitting disk -- **Version management**: Automatic schema versioning with `StoreMeta` -- **Height-aware operations**: `insert_if_needed` / `remove_if_needed` skip work at heights already processed +- **Workload-optimized configs**: `Kind::Random` (bloom filters, pinned blocks), `Kind::Recent` (point-read optimized), and `Kind::Vec` (append-heavy) +- **Write batching**: Accumulate puts/deletes in memory, then move them into an owned ingestion batch +- **Tiered caching**: Optional bounded in-memory batches before hitting disk +- **Version management**: Automatic schema-version validation when opening a store ## Core API ```rust,ignore -let store: Store = Store::import( +let mut store: Store = Store::import( &db, &path, "txid_to_height", Version::new(1), Mode::Any, Kind::Random )?; store.insert(txid, height); -store.commit(height)?; +if let Some(ingest) = store.take_pending_ingest() { + ingest()?; +} +db.persist(PersistMode::SyncData)?; let height = store.get(&txid)?; ``` @@ -34,7 +36,6 @@ let height = store.get(&txid)?; |------|----------|--------------| | `Random` | UTXO lookups, txid queries | Aggressive bloom filters | | `Recent` | Mempool, recent blocks | Point-read hints | -| `Sequential` | Full chain scans | Minimal indexing | | `Vec` | Append-only series | Large memtables, no filters | ## Built On diff --git a/crates/brk_store/examples/store.rs b/crates/brk_store/examples/store.rs index 391a43fbc..ea3b91079 100644 --- a/crates/brk_store/examples/store.rs +++ b/crates/brk_store/examples/store.rs @@ -1,20 +1,26 @@ -// use std::path::Path; +use std::path::Path; use brk_error::Result; +use brk_store::{Kind, Mode, Store, open_database}; +use brk_types::{Height, TxIndex, Version}; +use fjall::PersistMode; fn main() -> Result<()> { - // let p = Path::new("./examples/_fjall"); + let path = Path::new("./examples/_fjall"); + let db = open_database(path)?; + let mut store: Store = + Store::import(&db, path, "numbers", Version::ZERO, Mode::Any, Kind::Random)?; - // let _keyspace = brk_store::open_keyspace(p)?; + let key = TxIndex::new(10); + let value = Height::new(50); + store.insert(key, value); - // let mut store: Store = - // brk_store::Store::import(&keyspace, p, "n", Version::ZERO, None)?; + if let Some(ingest) = store.take_pending_ingest() { + ingest()?; + } + db.persist(PersistMode::SyncData)?; - // store.insert_if_needed(Sats::new(10), Sats::FIFTY_BTC, Height::ZERO); - - // store.commit(Height::ZERO)?; - - // store.persist()?; + assert_eq!(store.get(&key)?.as_deref(), Some(&value)); Ok(()) } diff --git a/crates/brk_store/src/any.rs b/crates/brk_store/src/any.rs index f024cac4d..ebf477aaf 100644 --- a/crates/brk_store/src/any.rs +++ b/crates/brk_store/src/any.rs @@ -1,8 +1,5 @@ use brk_error::Result; -use brk_types::Height; pub trait AnyStore: Send + Sync { - fn height(&self) -> Option; - fn export_meta(&mut self, height: Height) -> Result<()>; - fn commit(&mut self, height: Height) -> Result<()>; + fn ingest_pending(&mut self) -> Result<()>; } diff --git a/crates/brk_store/src/item.rs b/crates/brk_store/src/item.rs index f57b8d2de..261445ca2 100644 --- a/crates/brk_store/src/item.rs +++ b/crates/brk_store/src/item.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -pub enum Item { +pub(super) enum Item { Value { key: K, value: V }, Tomb(K), } diff --git a/crates/brk_store/src/lib.rs b/crates/brk_store/src/lib.rs index a52413687..42eede01a 100644 --- a/crates/brk_store/src/lib.rs +++ b/crates/brk_store/src/lib.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, cmp::Ordering, fmt::Debug, fs, hash::Hash, mem, ops::Range, path::Path}; use brk_error::Result; -use brk_types::{Height, Version}; +use brk_types::Version; use byteview::ByteView; use fjall::{Database, Keyspace, KeyspaceCreateOptions, config::*}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -14,10 +14,11 @@ mod kind; mod meta; mod mode; +use item::Item; +use meta::StoreMeta; + pub use any::*; -pub use item::*; pub use kind::*; -pub use meta::*; pub use mode::*; const MAJOR_FJALL_VERSION: Version = Version::new(4); @@ -29,6 +30,8 @@ pub fn open_database(path: &Path) -> fjall::Result { .open() } +pub type PendingIngest = Box Result<()> + Send>; + #[derive(Clone)] pub struct Store { meta: StoreMeta, @@ -199,31 +202,24 @@ where } /// Takes buffered puts/dels and returns a closure that ingests them into the keyspace. - /// The store is left with empty buffers, ready for the next batch. - #[allow(clippy::type_complexity)] - pub fn take_pending_ingest( - &mut self, - height: Height, - ) -> Result Result<()> + Send>>> + /// The store is left with empty buffers, ready for the next batch. The caller must + /// persist the database after ingestion before treating the data as durable. + pub fn take_pending_ingest(&mut self) -> Option where K: Send + 'static, V: Send + 'static, for<'a> ByteView: From<&'a K> + From<&'a V>, { - self.export_meta_if_needed(height)?; - let puts = mem::take(&mut self.puts); let dels = mem::take(&mut self.dels); if puts.is_empty() && dels.is_empty() { - return Ok(None); + return None; } let keyspace = self.keyspace.clone(); - Ok(Some(Box::new(move || { - Self::ingest_owned(&keyspace, puts, dels) - }))) + Some(Box::new(move || Self::ingest_owned(&keyspace, puts, dels))) } #[inline] @@ -263,23 +259,6 @@ where self.keyspace.approximate_len() } - #[inline] - fn has(&self, height: Height) -> bool { - self.meta.has(height) - } - - fn export_meta(&mut self, height: Height) -> Result<()> { - self.meta.export(height)?; - Ok(()) - } - - fn export_meta_if_needed(&mut self, height: Height) -> Result<()> { - if !self.has(height) { - self.export_meta(height)?; - } - Ok(()) - } - fn ingest<'a>( keyspace: &Keyspace, puts: impl Iterator, @@ -367,17 +346,7 @@ where for<'a> ByteView: From + From + From<&'a K> + From<&'a V>, Self: Send + Sync, { - fn export_meta(&mut self, height: Height) -> Result<()> { - self.export_meta(height) - } - - fn height(&self) -> Option { - self.meta.height() - } - - fn commit(&mut self, height: Height) -> Result<()> { - self.export_meta_if_needed(height)?; - + fn ingest_pending(&mut self) -> Result<()> { let puts = mem::take(&mut self.puts); let dels = mem::take(&mut self.dels); diff --git a/crates/brk_store/src/meta.rs b/crates/brk_store/src/meta.rs index 7c9b24dd1..4492d671e 100644 --- a/crates/brk_store/src/meta.rs +++ b/crates/brk_store/src/meta.rs @@ -1,5 +1,5 @@ use std::{ - fs, io, + fs, path::{Path, PathBuf}, }; @@ -7,16 +7,13 @@ use brk_error::{Error, Result}; use brk_types::Version; use fjall::Keyspace; -use super::Height; - #[derive(Debug, Clone)] -pub struct StoreMeta { +pub(super) struct StoreMeta { pathbuf: PathBuf, - height: Option, } impl StoreMeta { - pub fn checked_open( + pub(super) fn checked_open( path: &Path, version: Version, open_partition_handle: F, @@ -40,7 +37,6 @@ impl StoreMeta { let slf = Self { pathbuf: path.to_owned(), - height: Height::try_from(Self::path_height_(path).as_path()).ok(), }; version.write(&slf.path_version())?; @@ -48,12 +44,7 @@ impl StoreMeta { Ok((slf, partition)) } - pub fn export(&mut self, height: Height) -> io::Result<()> { - self.height = Some(height); - height.write(&self.path_height()) - } - - pub fn path(&self) -> &Path { + pub(super) fn path(&self) -> &Path { &self.pathbuf } @@ -63,31 +54,4 @@ impl StoreMeta { fn path_version_(path: &Path) -> PathBuf { path.join("version") } - - #[inline] - pub fn height(&self) -> Option { - self.height - } - #[inline] - pub fn needs(&self, height: Height) -> bool { - self.height.is_none_or(|self_height| height > self_height) - } - #[inline] - pub fn has(&self, height: Height) -> bool { - !self.needs(height) - } - pub fn reset(&mut self) -> io::Result<()> { - self.height = None; - let path = self.path_height(); - if path.exists() { - fs::remove_file(&path)?; - } - Ok(()) - } - fn path_height(&self) -> PathBuf { - Self::path_height_(&self.pathbuf) - } - fn path_height_(path: &Path) -> PathBuf { - path.join("height") - } } diff --git a/crates/brk_store/tests/owned_ingest.rs b/crates/brk_store/tests/owned_ingest.rs index 26ad6517d..d5177f9db 100644 --- a/crates/brk_store/tests/owned_ingest.rs +++ b/crates/brk_store/tests/owned_ingest.rs @@ -1,5 +1,5 @@ use brk_store::{Kind, Mode, Store, open_database}; -use brk_types::{AddrIndexTxIndex, Height, TxIndex, TypeIndex, Unit, Version}; +use brk_types::{AddrIndexTxIndex, TxIndex, TypeIndex, Unit, Version}; use fjall::PersistMode; fn key(address: u32, transaction: u32) -> AddrIndexTxIndex { @@ -24,12 +24,12 @@ fn owned_ingest_merges_puts_and_tombstones() -> brk_error::Result<()> { store.insert(key(1, 1), Unit); store.insert(key(2, 2), Unit); - store.take_pending_ingest(Height::from(0_u32))?.unwrap()()?; + store.take_pending_ingest().unwrap()()?; store.remove(key(1, 1)); store.remove(key(3, 3)); store.insert(key(4, 4), Unit); - store.take_pending_ingest(Height::from(1_u32))?.unwrap()()?; + store.take_pending_ingest().unwrap()()?; db.persist(PersistMode::SyncData)?; assert!(store.get(&key(1, 1))?.is_none()); diff --git a/crates/fjall/src/batch/mod.rs b/crates/fjall/src/batch/mod.rs index c42268395..ed7c89361 100644 --- a/crates/fjall/src/batch/mod.rs +++ b/crates/fjall/src/batch/mod.rs @@ -115,15 +115,16 @@ impl WriteBatch { journal_writer.write_batch(self.data.iter(), self.data.len(), batch_seqno)?; if let Some(mode) = self.durability - && let Err(e) = journal_writer.persist(mode) { - self.db.is_poisoned.poison(); + && let Err(e) = journal_writer.persist(mode) + { + self.db.is_poisoned.poison(); - log::error!( - "persist failed, which is a FATAL, and possibly hardware-related, failure: {e:?}" - ); + log::error!( + "persist failed, which is a FATAL, and possibly hardware-related, failure: {e:?}" + ); - return Err(crate::Error::Poisoned); - } + return Err(crate::Error::Poisoned); + } // TODO: maybe we can use a stack alloc hashset/vec here, such as smallset #[expect(clippy::mutable_key_type)] diff --git a/crates/fjall/src/journal/mod.rs b/crates/fjall/src/journal/mod.rs index 4221c9286..a0507f3be 100644 --- a/crates/fjall/src/journal/mod.rs +++ b/crates/fjall/src/journal/mod.rs @@ -34,7 +34,10 @@ impl std::fmt::Debug for Journal { write!( f, "{}", - self.path().map_or_else(|_| String::from(""), |p| p.display().to_string()) + self.path().map_or_else( + |_| String::from(""), + |p| p.display().to_string() + ) ) } } diff --git a/crates/fjall/src/meta_keyspace.rs b/crates/fjall/src/meta_keyspace.rs index 53055e3ef..4a87f6680 100644 --- a/crates/fjall/src/meta_keyspace.rs +++ b/crates/fjall/src/meta_keyspace.rs @@ -262,8 +262,7 @@ mod tests { .inner .get( [ - b'c', 0, 0, 0, 0, 0, 0, 0, 1, b'v', b'e', b'r', - b's', b'i', b'o', b'n' + b'c', 0, 0, 0, 0, 0, 0, 0, 1, b'v', b'e', b'r', b's', b'i', b'o', b'n' ], SeqNo::MAX )? @@ -288,8 +287,7 @@ mod tests { .inner .get( [ - b'c', 0, 0, 0, 0, 0, 0, 0, 1, b'v', b'e', b'r', - b's', b'i', b'o', b'n' + b'c', 0, 0, 0, 0, 0, 0, 0, 1, b'v', b'e', b'r', b's', b'i', b'o', b'n' ], SeqNo::MAX )? diff --git a/crates/lsm-tree/src/compaction/stream.rs b/crates/lsm-tree/src/compaction/stream.rs index cb0b24241..752fde2c8 100644 --- a/crates/lsm-tree/src/compaction/stream.rs +++ b/crates/lsm-tree/src/compaction/stream.rs @@ -674,9 +674,11 @@ mod tests { #[test] fn compaction_filter_custom_mvcc() { - let vec = [kv(b"abc", 4, b"c", false), + let vec = [ + kv(b"abc", 4, b"c", false), kv(b"abc", 3, b"b", false), - kv(b"abc", 2, b"a", false)]; + kv(b"abc", 2, b"a", false), + ]; let iter = vec.iter().cloned().map(Ok); let iter = CompactionStream::new(iter, 995).with_filter(Filter { diff --git a/crates/lsm-tree/src/table/block_index/two_level.rs b/crates/lsm-tree/src/table/block_index/two_level.rs index ea0d02d11..2977868b0 100644 --- a/crates/lsm-tree/src/table/block_index/two_level.rs +++ b/crates/lsm-tree/src/table/block_index/two_level.rs @@ -67,13 +67,15 @@ impl Iter { let mut iter = OwnedIndexBlockIter::new(self.tli_block.clone(), IndexBlock::iter); if let Some((lo_key, lo_seqno)) = &self.lo - && !iter.seek_lower(lo_key, *lo_seqno) { - return false; - } + && !iter.seek_lower(lo_key, *lo_seqno) + { + return false; + } if let Some((hi_key, hi_seqno)) = &self.hi - && !iter.seek_upper(hi_key, *hi_seqno) { - return false; - } + && !iter.seek_upper(hi_key, *hi_seqno) + { + return false; + } self.tli = Some(iter); @@ -98,9 +100,10 @@ impl Iterator for Iter { fn next(&mut self) -> Option { if let Some(lo_block) = &mut self.lo_consumer - && let Some(item) = lo_block.next() { - return Some(Ok(item)); - } + && let Some(item) = lo_block.next() + { + return Some(Ok(item)); + } if self.tli.is_none() && !self.init_tli() { return None; @@ -124,13 +127,15 @@ impl Iterator for Iter { let mut iter = OwnedIndexBlockIter::new(index_block, IndexBlock::iter); if let Some((lo_key, lo_seqno)) = &self.lo - && !iter.seek_lower(lo_key, *lo_seqno) { - return None; - } + && !iter.seek_lower(lo_key, *lo_seqno) + { + return None; + } if let Some((hi_key, hi_seqno)) = &self.hi - && !iter.seek_upper(hi_key, *hi_seqno) { - return None; - } + && !iter.seek_upper(hi_key, *hi_seqno) + { + return None; + } let next_item = iter.next().map(Ok); @@ -144,9 +149,10 @@ impl Iterator for Iter { // Nothing more found, consume from hi consumer if let Some(hi_block) = &mut self.hi_consumer - && let Some(item) = hi_block.next() { - return Some(Ok(item)); - } + && let Some(item) = hi_block.next() + { + return Some(Ok(item)); + } None } @@ -155,9 +161,10 @@ impl Iterator for Iter { impl DoubleEndedIterator for Iter { fn next_back(&mut self) -> Option { if let Some(hi_block) = &mut self.hi_consumer - && let Some(item) = hi_block.next_back() { - return Some(Ok(item)); - } + && let Some(item) = hi_block.next_back() + { + return Some(Ok(item)); + } if self.tli.is_none() && !self.init_tli() { return None; @@ -181,13 +188,15 @@ impl DoubleEndedIterator for Iter { let mut iter = OwnedIndexBlockIter::new(index_block, IndexBlock::iter); if let Some((lo_key, lo_seqno)) = &self.lo - && !iter.seek_lower(lo_key, *lo_seqno) { - return None; - } + && !iter.seek_lower(lo_key, *lo_seqno) + { + return None; + } if let Some((hi_key, hi_seqno)) = &self.hi - && !iter.seek_upper(hi_key, *hi_seqno) { - return None; - } + && !iter.seek_upper(hi_key, *hi_seqno) + { + return None; + } let next_item = iter.next_back().map(Ok); @@ -201,9 +210,10 @@ impl DoubleEndedIterator for Iter { // Nothing more found, consume from lo consumer if let Some(lo_block) = &mut self.lo_consumer - && let Some(item) = lo_block.next_back() { - return Some(Ok(item)); - } + && let Some(item) = lo_block.next_back() + { + return Some(Ok(item)); + } None } diff --git a/crates/lsm-tree/src/table/block_index/volatile.rs b/crates/lsm-tree/src/table/block_index/volatile.rs index ccca93cdd..525b1fa3e 100644 --- a/crates/lsm-tree/src/table/block_index/volatile.rs +++ b/crates/lsm-tree/src/table/block_index/volatile.rs @@ -102,13 +102,15 @@ impl Iterator for Iter { let mut iter = OwnedIndexBlockIter::new(index_block, IndexBlock::iter); if let Some((lo_key, lo_seqno)) = &self.lo - && !iter.seek_lower(lo_key, *lo_seqno) { - return None; - } + && !iter.seek_lower(lo_key, *lo_seqno) + { + return None; + } if let Some((hi_key, hi_seqno)) = &self.hi - && !iter.seek_upper(hi_key, *hi_seqno) { - return None; - } + && !iter.seek_upper(hi_key, *hi_seqno) + { + return None; + } let next_item = iter.next().map(Ok); @@ -138,13 +140,15 @@ impl DoubleEndedIterator for Iter { let mut iter = OwnedIndexBlockIter::new(index_block, IndexBlock::iter); if let Some((lo_key, lo_seqno)) = &self.lo - && !iter.seek_lower(lo_key, *lo_seqno) { - return None; - } + && !iter.seek_lower(lo_key, *lo_seqno) + { + return None; + } if let Some((hi_key, hi_seqno)) = &self.hi - && !iter.seek_upper(hi_key, *hi_seqno) { - return None; - } + && !iter.seek_upper(hi_key, *hi_seqno) + { + return None; + } let next_item = iter.next_back().map(Ok); diff --git a/crates/lsm-tree/src/table/iter.rs b/crates/lsm-tree/src/table/iter.rs index 7d7e718a9..9de2b98c0 100644 --- a/crates/lsm-tree/src/table/iter.rs +++ b/crates/lsm-tree/src/table/iter.rs @@ -168,9 +168,9 @@ impl Iterator for Iter { v }) .map(Ok) - { - return Some(item); - } + { + return Some(item); + } if !self.index_initialized { // Lazily initialize the index iterator here (not in `new`) so callers can set bounds @@ -221,9 +221,9 @@ impl Iterator for Iter { v }) .map(Ok) - { - return Some(item); - } + { + return Some(item); + } // Nothing left to serve; drop both buffers so the iterator can be reused safely. self.lo_data_block = None; @@ -290,9 +290,9 @@ impl DoubleEndedIterator for Iter { v }) .map(Ok) - { - return Some(item); - } + { + return Some(item); + } if !self.index_initialized { // Mirror forward iteration: initialize lazily so bounds can be applied up-front. The @@ -307,13 +307,12 @@ impl DoubleEndedIterator for Iter { true }; - if ok - && let Some(bound) = &self.range.1 { - let key = match bound { - Bound::Included(k) | Bound::Excluded(k) => k, - }; - ok = self.index_iter.seek_upper(key, u64::MAX); - } + if ok && let Some(bound) = &self.range.1 { + let key = match bound { + Bound::Included(k) | Bound::Excluded(k) => k, + }; + ok = self.index_iter.seek_upper(key, u64::MAX); + } self.index_initialized = true; @@ -337,9 +336,9 @@ impl DoubleEndedIterator for Iter { v }) .map(Ok) - { - return Some(item); - } + { + return Some(item); + } // Nothing left to produce; reset both buffers to keep the iterator reusable. self.lo_data_block = None; diff --git a/crates/lsm-tree/src/table/tests.rs b/crates/lsm-tree/src/table/tests.rs index 3da5ab3c0..c71fd95f1 100644 --- a/crates/lsm-tree/src/table/tests.rs +++ b/crates/lsm-tree/src/table/tests.rs @@ -33,9 +33,10 @@ fn test_with_table( for (idx, item) in items.iter().enumerate() { if let Some(rotate) = rotate_every - && idx % rotate == 0 { - writer.spill_block()?; - } + && idx % rotate == 0 + { + writer.spill_block()?; + } writer.write(item.clone())?; } let (_, checksum) = writer.finish()?.unwrap(); @@ -175,9 +176,10 @@ fn test_with_table( for (idx, item) in items.iter().enumerate() { if let Some(rotate) = rotate_every - && idx % rotate == 0 { - writer.spill_block()?; - } + && idx % rotate == 0 + { + writer.spill_block()?; + } writer.write(item.clone())?; } let (_, checksum) = writer.finish()?.unwrap(); diff --git a/crates/lsm-tree/src/table/writer/mod.rs b/crates/lsm-tree/src/table/writer/mod.rs index 3139a7bee..16d74c4d3 100644 --- a/crates/lsm-tree/src/table/writer/mod.rs +++ b/crates/lsm-tree/src/table/writer/mod.rs @@ -256,10 +256,11 @@ impl Writer { if value_type == ValueType::Value && let Some((prev_key, prev_type)) = &self.previous_item - && prev_type == &ValueType::WeakTombstone && prev_key.as_ref() == user_key.as_ref() - { - self.meta.weak_tombstone_reclaimable_count += 1; - } + && prev_type == &ValueType::WeakTombstone + && prev_key.as_ref() == user_key.as_ref() + { + self.meta.weak_tombstone_reclaimable_count += 1; + } // NOTE: Check if we visit a new key if Some(&user_key) != self.current_key.as_ref() { diff --git a/crates/lsm-tree/src/version/mod.rs b/crates/lsm-tree/src/version/mod.rs index 74c4ce124..d332107ff 100644 --- a/crates/lsm-tree/src/version/mod.rs +++ b/crates/lsm-tree/src/version/mod.rs @@ -358,9 +358,10 @@ impl Version { .collect::>(); if level_idx == dest_level - && let Some(run) = Run::new(new_tables.to_vec()) { - runs.insert(0, run); - } + && let Some(run) = Run::new(new_tables.to_vec()) + { + runs.insert(0, run); + } let runs = optimize_runs(runs); @@ -399,9 +400,10 @@ impl Version { .collect::>(); if level_idx == dest_level - && let Some(run) = Run::new(affected_tables.clone()) { - runs.insert(0, run); - } + && let Some(run) = Run::new(affected_tables.clone()) + { + runs.insert(0, run); + } let runs = optimize_runs(runs); diff --git a/crates/lsm-tree/src/version/recovery.rs b/crates/lsm-tree/src/version/recovery.rs index e07d152ab..abd54a7f4 100644 --- a/crates/lsm-tree/src/version/recovery.rs +++ b/crates/lsm-tree/src/version/recovery.rs @@ -89,8 +89,6 @@ pub fn recover(folder: &Path) -> crate::Result { } let tree_type = { - - toc .section(b"tree_type") .ok_or(crate::Error::Unrecoverable) diff --git a/crates/rawdb/src/lib.rs b/crates/rawdb/src/lib.rs index 9e5cb4bea..5a05b003c 100644 --- a/crates/rawdb/src/lib.rs +++ b/crates/rawdb/src/lib.rs @@ -53,7 +53,7 @@ pub const GiB: usize = 1024 * 1024 * 1024; #[must_use = "Database should be stored to keep the database open"] pub struct Database(Arc); -/// Lock ordering: layout → regions → mmap → file → meta → dirty_bounds. +/// Lock ordering: layout → regions → mmap → file → meta → dirty_ranges. struct DatabaseInner { path: PathBuf, name: String, @@ -312,15 +312,15 @@ impl Database { /// Flushes all dirty data and metadata to disk. Returns number of flushed regions. pub fn flush(&self) -> Result { - let dirty_regions: Vec<(Region, Option<(usize, usize)>)> = self + let dirty_regions: Vec<(Region, Vec<(usize, usize)>)> = self .regions() .index_to_region() .iter() .flatten() .filter_map(|r| { - let bounds = r.take_dirty_bounds(); - if bounds.is_some() || r.meta().needs_flush() { - Some((r.clone(), bounds)) + let ranges = r.take_dirty_ranges(); + if !ranges.is_empty() || r.meta().needs_flush() { + Some((r.clone(), ranges)) } else { None } @@ -333,27 +333,80 @@ impl Database { return Ok(0); } - let (flush_start, flush_end) = dirty_regions + let mut flush_ranges = dirty_regions .iter() - .filter_map(|(r, bounds)| { - let (min, max) = (*bounds)?; - let region_start = r.meta().start(); - Some((region_start + min, region_start + max)) + .filter(|(region, _)| region.uses_sparse_flush()) + .flat_map(|(region, ranges)| { + let region_start = region.meta().start(); + ranges + .iter() + .map(move |&(start, end)| (region_start + start, region_start + end)) }) - .fold((usize::MAX, 0usize), |(min_s, max_e), (s, e)| { - (min_s.min(s), max_e.max(e)) + .collect::>(); + + let (regular_start, regular_end) = dirty_regions + .iter() + .filter(|(region, _)| !region.uses_sparse_flush()) + .flat_map(|(region, ranges)| { + let region_start = region.meta().start(); + ranges + .iter() + .map(move |&(start, end)| (region_start + start, region_start + end)) + }) + .fold((usize::MAX, 0), |(min, max), (start, end)| { + (min.min(start), max.max(end)) }); - if flush_start < flush_end { - let mmap = self.mmap(); - if let Err(e) = mmap.flush_async_range(flush_start, flush_end - flush_start) { - drop(mmap); - for (region, bounds) in dirty_regions { - if let Some((min, max)) = bounds { - region.restore_dirty_bounds(min, max); - } + if regular_start < regular_end { + let mut cursor = regular_start; + let mut sparse_regions = self + .regions() + .index_to_region() + .iter() + .flatten() + .filter(|region| region.uses_sparse_flush()) + .map(|region| { + let meta = region.meta(); + (meta.start(), meta.start() + meta.reserved()) + }) + .collect::>(); + sparse_regions.sort_unstable(); + for (start, end) in sparse_regions { + if end <= cursor || start >= regular_end { + continue; + } + if cursor < start { + flush_ranges.push((cursor, start.min(regular_end))); + } + cursor = cursor.max(end); + } + if cursor < regular_end { + flush_ranges.push((cursor, regular_end)); + } + } + + flush_ranges.sort_unstable(); + let mut merged_ranges: Vec<(usize, usize)> = Vec::with_capacity(flush_ranges.len()); + for (start, end) in flush_ranges { + if let Some((_, previous_end)) = merged_ranges.last_mut() + && start <= *previous_end + { + *previous_end = (*previous_end).max(end); + } else { + merged_ranges.push((start, end)); + } + } + + if !merged_ranges.is_empty() { + let mmap = self.mmap(); + for &(start, end) in &merged_ranges { + if let Err(error) = mmap.flush_async_range(start, end - start) { + drop(mmap); + for (region, ranges) in dirty_regions { + region.restore_dirty_ranges(&ranges); + } + return Err(error.into()); } - return Err(e.into()); } } diff --git a/crates/rawdb/src/region.rs b/crates/rawdb/src/region.rs index 30dd7ad61..58f0e5940 100644 --- a/crates/rawdb/src/region.rs +++ b/crates/rawdb/src/region.rs @@ -1,10 +1,18 @@ -use std::{fs::File, mem, sync::Arc}; +use std::{ + fs::File, + mem, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; use log::{debug, trace}; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use crate::{ - Database, Error, PAGE_SIZE, PAGE_SIZE_MINUS_1, Reader, RegionMetadata, Result, WeakDatabase, + Database, Error, HolePunch, PAGE_SIZE, PAGE_SIZE_MINUS_1, Reader, RegionMetadata, Result, + WeakDatabase, }; /// Named, dynamically-sized region within a database. @@ -17,8 +25,9 @@ pub(crate) struct RegionInner { db: WeakDatabase, index: usize, meta: RwLock, - /// (min_offset, max_offset) relative to region start. (usize::MAX, 0) = clean. - dirty_bounds: Mutex<(usize, usize)>, + /// Sorted, merged dirty byte ranges relative to the region start. + dirty_ranges: Mutex>, + sparse_flush: AtomicBool, } impl Region { @@ -34,7 +43,8 @@ impl Region { db: db.weak_clone(), index, meta: RwLock::new(RegionMetadata::new(id, start, len, reserved)), - dirty_bounds: Mutex::new((usize::MAX, 0)), + dirty_ranges: Mutex::new(Vec::new()), + sparse_flush: AtomicBool::new(false), })) } @@ -43,7 +53,8 @@ impl Region { db: db.weak_clone(), index, meta: RwLock::new(meta), - dirty_bounds: Mutex::new((usize::MAX, 0)), + dirty_ranges: Mutex::new(Vec::new()), + sparse_flush: AtomicBool::new(false), })) } @@ -174,13 +185,60 @@ impl Region { /// Appends data to the region. Not durable until `flush()`. #[inline] pub fn write(&self, data: &[u8]) -> Result<()> { - self.write_with(data, None, false) + self.write_with(data, None, false, false) } /// Writes data at offset within the region. Not durable until `flush()`. #[inline] pub fn write_at(&self, data: &[u8], at: usize) -> Result<()> { - self.write_with(data, Some(at), false) + self.write_with(data, Some(at), false, false) + } + + /// Writes data at an arbitrary reserved offset, growing the logical region + /// length when needed. Bytes skipped between the old length and `at` must + /// not be read unless they have been initialized separately. + /// + /// This is intended for sparse, fixed-layout storage whose independent + /// sections are filled out of order. + #[doc(hidden)] + #[inline] + pub fn write_at_grow(&self, data: &[u8], at: usize) -> Result<()> { + self.write_with(data, Some(at), false, true) + } + + /// Prevents database-wide flush coalescing from spanning this region's + /// intentionally sparse internal layout. + #[doc(hidden)] + pub fn use_sparse_flush(&self) { + self.0.sparse_flush.store(true, Ordering::Relaxed); + } + + #[inline] + pub(crate) fn uses_sparse_flush(&self) -> bool { + self.0.sparse_flush.load(Ordering::Relaxed) + } + + /// Deallocates initialized but unused bytes inside this region without + /// changing its logical length. + #[doc(hidden)] + pub fn punch_hole(&self, offset: usize, len: usize) -> Result<()> { + if len == 0 { + return Ok(()); + } + let end = offset.checked_add(len).ok_or(Error::RegionSizeOverflow { + current: offset, + requested: len, + })?; + let meta = self.meta_mut(); + if end > meta.reserved() { + return Err(Error::WriteOutOfBounds { + position: end, + region_len: meta.reserved(), + }); + } + let start = meta.start() + offset; + let db = self.db(); + HolePunch::punch(&db.file(), start, len) } /// Writes ascending (offset, value) pairs directly to the mmap within region bounds. @@ -231,9 +289,7 @@ impl Region { dirty_end = end_offset; } - let mut bounds = self.0.dirty_bounds.lock(); - bounds.0 = bounds.0.min(first_offset); - bounds.1 = bounds.1.max(dirty_end); + self.mark_dirty(first_offset, dirty_end - first_offset); } pub fn truncate(&self, from: usize) -> Result<()> { @@ -259,11 +315,17 @@ impl Region { /// Truncates to `at`, then writes data there. #[inline] pub fn truncate_write(&self, at: usize, data: &[u8]) -> Result<()> { - self.write_with(data, Some(at), true) + self.write_with(data, Some(at), true, false) } #[inline] - fn write_with(&self, data: &[u8], at: Option, truncate: bool) -> Result<()> { + fn write_with( + &self, + data: &[u8], + at: Option, + truncate: bool, + allow_grow: bool, + ) -> Result<()> { let db = self.db(); let index = self.index(); let meta = self.meta(); @@ -274,7 +336,8 @@ impl Region { let data_len = data.len(); - if let Some(at_val) = at + if !allow_grow + && let Some(at_val) = at && at_val > len { return Err(Error::WriteOutOfBounds { @@ -477,16 +540,18 @@ impl Region { /// Flushes dirty data and metadata to disk. Returns whether anything was flushed. pub fn flush(&self) -> Result { let db = self.db(); - let dirty_bounds = self.take_dirty_bounds(); + let dirty_ranges = self.take_dirty_ranges(); let regions = db.regions(); - let data_flushed = if let Some((min, max)) = dirty_bounds { + let data_flushed = if !dirty_ranges.is_empty() { let region_start = self.meta().start(); let mmap = db.mmap(); - if let Err(e) = mmap.flush_async_range(region_start + min, max - min) { - drop(mmap); - self.restore_dirty_bounds(min, max); - return Err(e.into()); + for &(start, end) in &dirty_ranges { + if let Err(error) = mmap.flush_async_range(region_start + start, end - start) { + drop(mmap); + self.restore_dirty_ranges(&dirty_ranges); + return Err(error.into()); + } } true } else { @@ -538,10 +603,20 @@ impl Region { #[inline] pub fn mark_dirty(&self, offset: usize, len: usize) { + if len == 0 { + return; + } let end = offset + len; - let mut bounds = self.0.dirty_bounds.lock(); - bounds.0 = bounds.0.min(offset); - bounds.1 = bounds.1.max(end); + let mut ranges = self.0.dirty_ranges.lock(); + let mut start = offset; + let mut end = end; + let at = ranges.partition_point(|&(_, range_end)| range_end < start); + while at < ranges.len() && ranges[at].0 <= end { + let range = ranges.remove(at); + start = start.min(range.0); + end = end.max(range.1); + } + ranges.insert(at, (start, end)); } #[inline] @@ -551,19 +626,14 @@ impl Region { } #[inline] - pub(crate) fn take_dirty_bounds(&self) -> Option<(usize, usize)> { - let mut bounds = self.0.dirty_bounds.lock(); - if bounds.0 < bounds.1 { - Some(mem::replace(&mut *bounds, (usize::MAX, 0))) - } else { - None - } + pub(crate) fn take_dirty_ranges(&self) -> Vec<(usize, usize)> { + mem::take(&mut *self.0.dirty_ranges.lock()) } #[inline] - pub(crate) fn restore_dirty_bounds(&self, min: usize, max: usize) { - let mut bounds = self.0.dirty_bounds.lock(); - bounds.0 = bounds.0.min(min); - bounds.1 = bounds.1.max(max); + pub(crate) fn restore_dirty_ranges(&self, ranges: &[(usize, usize)]) { + for &(start, end) in ranges { + self.mark_dirty(start, end - start); + } } } diff --git a/crates/vecdb/src/base/options/mod.rs b/crates/vecdb/src/base/options/mod.rs index fd84350f3..070a6dcfa 100644 --- a/crates/vecdb/src/base/options/mod.rs +++ b/crates/vecdb/src/base/options/mod.rs @@ -15,6 +15,8 @@ pub struct ImportOptions<'a> { pub version: Version, /// Number of stamped change files to keep for rollback support (0 to disable). pub saved_stamped_changes: u16, + /// Overrides the index type's initial value capacity when set. + pub initial_capacity: Option, } impl<'a> ImportOptions<'a> { @@ -24,6 +26,7 @@ impl<'a> ImportOptions<'a> { name, version, saved_stamped_changes: 0, + initial_capacity: None, } } @@ -31,4 +34,9 @@ impl<'a> ImportOptions<'a> { self.saved_stamped_changes = num; self } + + pub fn with_initial_capacity(mut self, capacity: usize) -> Self { + self.initial_capacity = Some(capacity); + self + } } diff --git a/crates/vecdb/src/base/read_write.rs b/crates/vecdb/src/base/read_write.rs index 0873a4a60..e68505e84 100644 --- a/crates/vecdb/src/base/read_write.rs +++ b/crates/vecdb/src/base/read_write.rs @@ -33,6 +33,7 @@ where T: VecValue, { pub fn import(options: ImportOptions, format: Format) -> Result { + let initial_capacity = options.initial_capacity.unwrap_or(I::INITIAL_CAPACITY); let region = options .db .create_region_if_needed(&vec_region_name_with::(options.name))?; @@ -45,8 +46,7 @@ where }); } - let initial_capacity = I::INITIAL_CAPACITY; - if region_len == 0 && format.is_raw() && initial_capacity > 0 { + if region_len == 0 && initial_capacity > 0 { let capacity = initial_capacity .checked_mul(size_of::()) .and_then(|bytes| bytes.checked_add(HEADER_OFFSET)) diff --git a/crates/vecdb/src/error/mod.rs b/crates/vecdb/src/error/mod.rs index 999fa60e2..b768a4c8e 100644 --- a/crates/vecdb/src/error/mod.rs +++ b/crates/vecdb/src/error/mod.rs @@ -78,7 +78,7 @@ pub enum Error { expected_len: usize, actual_len: usize, }, - #[error("Cannot remove PcodecVec: pages still referenced")] + #[error("Cannot remove vec: pages still referenced")] PagesStillReferenced, #[error("Invalid format byte: {0}")] InvalidFormat(u8), diff --git a/crates/vecdb/src/variants/columnar/lazy.rs b/crates/vecdb/src/variants/columnar/lazy.rs new file mode 100644 index 000000000..8cb4fdb6f --- /dev/null +++ b/crates/vecdb/src/variants/columnar/lazy.rs @@ -0,0 +1,200 @@ +use std::{marker::PhantomData, sync::Arc}; + +use crate::{ + AnyVec, ReadOnlyClone, ReadableVec, TypedVec, UnaryTransform, VecValue, Version, + short_type_name, +}; + +use super::{ColumnId, ReadableColumnarVec}; + +/// Lazy scalar transformation that preserves the source's column structure. +pub struct LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + name: Arc, + base_version: Version, + source: S, + compute: fn(S::T) -> T, + columns: PhantomData, +} + +impl Clone for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + compute: self.compute, + columns: PhantomData, + } + } +} + +impl LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + /// Creates a single-source lazy scalar transformation while preserving columns. + pub fn transformed(name: &str, version: Version, source: S) -> Self + where + F: UnaryTransform, + { + Self { + name: Arc::from(name), + base_version: version, + source, + compute: F::apply, + columns: PhantomData, + } + } +} + +impl AnyVec for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + } + + fn name(&self) -> &str { + &self.name + } + + fn len(&self) -> usize { + self.source.len() + } + + fn index_type_to_string(&self) -> &'static str { + self.source.index_type_to_string() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::>() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::>() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + type I = S::I; + type T = C::Row; +} + +impl ReadableColumnarVec for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + type I = S::I; + type T = T; + + fn for_each_column_chunk_at(&self, columns: &[C], from: usize, to: usize, f: &mut F) + where + F: FnMut(C, usize, &[T]), + { + let compute = self.compute; + let mut transformed = Vec::new(); + self.source.for_each_column_chunk_at( + columns, + from, + to, + &mut |column, row_start, values| { + transformed.clear(); + transformed.extend(values.iter().cloned().map(compute)); + f(column, row_start, &transformed); + }, + ); + } +} + +impl ReadableVec> for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + fn cursor_chunk_size(&self) -> usize { + self.source.cursor_chunk_size() + } + + fn read_into_at(&self, from: usize, to: usize, out: &mut Vec>) { + let from = from.min(self.len()); + let to = to.min(self.len()); + if from >= to { + return; + } + + let mut rows = Vec::with_capacity(to - from); + self.source.read_into_at(from, to, &mut rows); + let compute = self.compute; + out.extend(rows.into_iter().map(|row| C::map(row, compute))); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(C::Row)) { + let compute = self.compute; + self.source + .for_each_range_dyn_at(from, to, &mut |row| f(C::map(row, compute))); + } + + fn fold_range_at) -> B>( + &self, + from: usize, + to: usize, + init: B, + mut f: F, + ) -> B { + let compute = self.compute; + self.source + .fold_range_at(from, to, init, |acc, row| f(acc, C::map(row, compute))) + } + + fn try_fold_range_at) -> std::result::Result>( + &self, + from: usize, + to: usize, + init: B, + mut f: F, + ) -> std::result::Result { + let compute = self.compute; + self.source + .try_fold_range_at(from, to, init, |acc, row| f(acc, C::map(row, compute))) + } +} + +impl ReadOnlyClone for LazyColumnarVec +where + C: ColumnId, + S: ReadableColumnarVec, + T: VecValue, +{ + type ReadOnly = Self; + + fn read_only_clone(&self) -> Self { + self.clone() + } +} diff --git a/crates/vecdb/src/variants/columnar/mod.rs b/crates/vecdb/src/variants/columnar/mod.rs new file mode 100644 index 000000000..fc7b6a707 --- /dev/null +++ b/crates/vecdb/src/variants/columnar/mod.rs @@ -0,0 +1,275 @@ +use std::{marker::PhantomData, sync::Arc}; + +use parking_lot::RwLock; + +use crate::{ + Error, ImportOptions, MAX_UNCOMPRESSED_PAGE_SIZE, Result, SharedLen, StoredVec, VecIndex, + Version, +}; + +mod lazy; +mod read; +mod schema; +mod sum; +mod traits; + +pub use lazy::*; +pub use schema::*; +pub use sum::*; + +use read::read_rows; +use schema::validate_schema; + +const VERSION: Version = Version::new(7); + +#[inline] +pub(super) const fn rows_per_block() -> usize { + MAX_UNCOMPRESSED_PAGE_SIZE / size_of::() +} + +/// One logical vector of rows stored as page-sized, column-major blocks in `V`. +/// +/// `V` remains an ordinary flat scalar vector. Every complete block of +/// `ROWS_PER_BLOCK` rows is flattened as consecutive scalar column pages: +/// +/// ```text +/// [column 0 page][column 1 page] ... [last column page] +/// ``` +/// +/// The final incomplete block is rebuilt in column-major order whenever it is +/// persisted. Raw and compressed vectors therefore keep their existing write, +/// page, rollback, and compression logic unchanged. +#[derive(Debug)] +#[must_use = "Vector should be stored to keep data accessible"] +pub struct ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + vec: V, + stored_rows: usize, + pushed: Vec>, + visible_rows: SharedLen, + gate: Arc>, +} + +/// Lean read-only clone of a [`ColumnarVec`]. +pub struct ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + vec: V::ReadOnly, + visible_rows: SharedLen, + gate: Arc>, + columns: PhantomData, +} + +impl Clone for ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn clone(&self) -> Self { + Self { + vec: self.vec.clone(), + visible_rows: self.visible_rows.clone(), + gate: Arc::clone(&self.gate), + columns: PhantomData, + } + } +} + +/// Owned scalar projection of one columnar source. +pub struct ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + source: S, + column: C, +} + +impl Clone for ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + fn clone(&self) -> Self { + Self { + source: self.source.clone(), + column: self.column, + } + } +} + +impl ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + pub(super) const COLUMN_COUNT: usize = C::ALL.len(); + pub(super) const ROWS_PER_BLOCK: usize = rows_per_block::(); + + fn validate_layout() -> Result<()> { + validate_schema::()?; + if size_of::() == 0 || Self::ROWS_PER_BLOCK == 0 { + return Err(Error::InvalidArgument( + "ColumnarVec requires at least one non-zero-sized scalar per page", + )); + } + Ok(()) + } + + fn validate_flat_len(vec: &V) -> Result { + let len = vec.len(); + if !len.is_multiple_of(Self::COLUMN_COUNT) { + return Err(Error::CorruptedRegion { + name: vec.name().to_string(), + region_len: vec.region().meta().len(), + }); + } + Ok(len / Self::COLUMN_COUNT) + } + + fn import_inner(mut options: ImportOptions, forced: bool) -> Result { + Self::validate_layout()?; + let columns = u32::try_from(Self::COLUMN_COUNT).map_err(|_| Error::Overflow)?; + options.version = options.version + VERSION + C::VERSION + Version::new(columns); + options.initial_capacity = Some( + options + .initial_capacity + .unwrap_or(V::I::INITIAL_CAPACITY) + .checked_mul(Self::COLUMN_COUNT) + .ok_or(Error::Overflow)?, + ); + let mut vec = if forced { + V::forced_import_with(options)? + } else { + V::import_with(options)? + }; + + let stored_rows = match Self::validate_flat_len(&vec) { + Ok(len) => len, + Err(_) if forced => { + vec.reset()?; + vec.write()?; + Self::validate_flat_len(&vec)? + } + Err(error) => return Err(error), + }; + Ok(Self { + vec, + stored_rows, + pushed: Vec::new(), + visible_rows: SharedLen::new(stored_rows), + gate: Arc::new(RwLock::new(())), + }) + } + + pub fn read_only_clone(&self) -> ReadOnlyColumnarVec { + ReadOnlyColumnarVec { + vec: self.vec.read_only_clone(), + visible_rows: self.visible_rows.clone(), + gate: Arc::clone(&self.gate), + columns: PhantomData, + } + } + + /// Returns an owned read-only view of one persisted scalar column. + /// Uncommitted rows become visible to the view after `write()`. + pub fn column(&self, column: C) -> ColumnarVecColumn, C> { + self.read_only_clone().column(column) + } + + /// Returns a lazy sum of selected persisted columns. + /// Uncommitted rows become visible to the view after `write()`. + pub fn sum_columns( + &self, + name: &str, + version: Version, + columns: [C; M], + ) -> ColumnarSumVec, C> { + ColumnarSumVec::new(name, version, self.read_only_clone(), columns) + } + + pub fn reserve_pushed(&mut self, additional: usize) { + self.pushed.reserve(additional); + } + + #[inline] + fn flat_rows(&self) -> usize { + debug_assert!(self.vec.len().is_multiple_of(Self::COLUMN_COUNT)); + self.vec.len() / Self::COLUMN_COUNT + } + + #[inline] + fn push_block(vec: &mut V, rows: &[C::Row]) { + for &column in C::ALL { + for row in rows { + vec.push(column.get(row).clone()); + } + } + } + + /// Moves the logical matrix changes into the wrapped flat vector. + /// + /// Only the incomplete height block is read and rebuilt. Completed blocks + /// already consist of aligned scalar column pages and remain untouched. + fn stage_pending(&mut self) -> Result<()> { + let flat_rows = self.flat_rows(); + if self.stored_rows == flat_rows && self.pushed.is_empty() { + return Ok(()); + } + + let target_rows = self + .stored_rows + .checked_add(self.pushed.len()) + .ok_or(Error::Overflow)?; + let block_start = self.stored_rows / Self::ROWS_PER_BLOCK * Self::ROWS_PER_BLOCK; + let retained_capacity = if self.stored_rows == block_start { + 0 + } else { + (target_rows - block_start).min(Self::ROWS_PER_BLOCK) + }; + let mut retained = Vec::with_capacity(retained_capacity); + read_rows::( + &self.vec, + flat_rows, + block_start, + self.stored_rows, + &mut retained, + ); + + let mut pushed_from = 0; + if !retained.is_empty() { + let take = (Self::ROWS_PER_BLOCK - retained.len()).min(self.pushed.len()); + retained.extend_from_slice(&self.pushed[..take]); + pushed_from = take; + } + + let flat_start = block_start + .checked_mul(Self::COLUMN_COUNT) + .ok_or(Error::Overflow)?; + self.vec.truncate_if_needed_at(flat_start)?; + Self::push_block(&mut self.vec, &retained); + for block in self.pushed[pushed_from..].chunks(Self::ROWS_PER_BLOCK) { + Self::push_block(&mut self.vec, block); + } + + self.stored_rows = target_rows; + self.pushed.clear(); + debug_assert_eq!(self.vec.len(), target_rows * Self::COLUMN_COUNT); + Ok(()) + } + + fn write_pending(&mut self) -> Result { + let gate = Arc::clone(&self.gate); + let _guard = gate.write(); + self.stage_pending()?; + let written = self.vec.write()?; + self.stored_rows = self.flat_rows(); + self.visible_rows.set(self.stored_rows); + Ok(written) + } +} diff --git a/crates/vecdb/src/variants/columnar/read.rs b/crates/vecdb/src/variants/columnar/read.rs new file mode 100644 index 000000000..ce2c5e130 --- /dev/null +++ b/crates/vecdb/src/variants/columnar/read.rs @@ -0,0 +1,390 @@ +use crate::{AnyVec, READ_CHUNK_SIZE, ReadableVec, StoredVec, VecIndex, VecValue}; + +use super::{ + ColumnId, ColumnarVec, ColumnarVecColumn, ReadOnlyColumnarVec, ReadableColumnarVec, + rows_per_block, schema::validate_column, +}; + +/// Reads matrix rows from a flat page-blocked scalar vector. +pub(super) fn read_rows( + vec: &R, + rows: usize, + from: usize, + to: usize, + out: &mut Vec>, +) where + I: VecIndex, + T: VecValue, + R: ReadableVec, + C: ColumnId, +{ + let from = from.min(rows); + let to = to.min(rows); + if from >= to { + return; + } + + let per_block = rows_per_block::(); + let column_count = C::ALL.len(); + out.reserve(to - from); + let capacity = per_block.min(to - from); + let mut columns = C::from_fn(|_| Vec::with_capacity(capacity)); + let mut at = from; + while at < to { + let block = at / per_block; + let block_start = block * per_block; + let block_rows = per_block.min(rows - block_start); + let local_from = at - block_start; + let take = (to - at).min(block_rows - local_from); + let flat_block_start = block * per_block * column_count; + + for &column in C::ALL { + let values = column.get_mut(&mut columns); + values.clear(); + let start = flat_block_start + column.index() * block_rows + local_from; + vec.read_into_at(start, start + take, values); + } + out.extend((0..take).map(|index| C::from_fn(|column| column.get(&columns)[index].clone()))); + at += take; + } +} + +fn for_each_column_chunk( + vec: &R, + rows: usize, + columns: &[C], + from: usize, + to: usize, + f: &mut F, +) where + I: VecIndex, + T: VecValue, + R: ReadableVec, + C: ColumnId, + F: FnMut(C, usize, &[T]), +{ + let from = from.min(rows); + let to = to.min(rows); + if from >= to { + return; + } + + let per_block = rows_per_block::(); + let column_count = C::ALL.len(); + let mut values = Vec::with_capacity(per_block.min(to - from)); + let mut at = from; + while at < to { + let block = at / per_block; + let block_start = block * per_block; + let block_rows = per_block.min(rows - block_start); + let local_from = at - block_start; + let take = (to - at).min(block_rows - local_from); + let flat_block_start = block * per_block * column_count; + + for &column in columns { + values.clear(); + let start = flat_block_start + column.index() * block_rows + local_from; + vec.read_into_at(start, start + take, &mut values); + f(column, at, &values); + } + at += take; + } +} + +impl ReadableColumnarVec for ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + type I = V::I; + type T = V::T; + + fn for_each_column_chunk_at(&self, columns: &[C], from: usize, to: usize, f: &mut F) + where + F: FnMut(C, usize, &[V::T]), + { + for &column in columns { + validate_column(column); + } + let _guard = self.gate.read(); + for_each_column_chunk::( + &self.vec, + self.visible_rows.get(), + columns, + from, + to, + f, + ); + } +} + +impl ReadableVec> for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn cursor_chunk_size(&self) -> usize { + Self::ROWS_PER_BLOCK * READ_CHUNK_SIZE.div_ceil(Self::ROWS_PER_BLOCK) + } + + fn read_into_at(&self, from: usize, to: usize, out: &mut Vec>) { + let len = self.len(); + let from = from.min(len); + let to = to.min(len); + if from >= to { + return; + } + + if from < self.stored_rows { + read_rows::( + &self.vec, + self.flat_rows(), + from, + to.min(self.stored_rows), + out, + ); + } + if to > self.stored_rows { + let start = from.max(self.stored_rows) - self.stored_rows; + let end = to - self.stored_rows; + out.extend_from_slice(&self.pushed[start..end]); + } + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(C::Row)) { + fold_readable(self, from, to, (), |(), value| f(value)); + } + + fn fold_range_at) -> B>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> B { + fold_readable(self, from, to, init, f) + } + + fn try_fold_range_at) -> std::result::Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> std::result::Result { + try_fold_readable(self, from, to, init, f) + } +} + +impl ReadableVec> for ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn cursor_chunk_size(&self) -> usize { + let per_block = rows_per_block::(); + per_block * READ_CHUNK_SIZE.div_ceil(per_block) + } + + fn read_into_at(&self, from: usize, to: usize, out: &mut Vec>) { + let _guard = self.gate.read(); + read_rows::(&self.vec, self.visible_rows.get(), from, to, out); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(C::Row)) { + fold_readable(self, from, to, (), |(), value| f(value)); + } + + fn fold_range_at) -> B>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> B { + fold_readable(self, from, to, init, f) + } + + fn try_fold_range_at) -> std::result::Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> std::result::Result { + try_fold_readable(self, from, to, init, f) + } +} + +impl ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + pub(super) fn new(source: S, column: C) -> Self { + validate_column(column); + Self { source, column } + } +} + +fn fold_column(source: &S, column: C, from: usize, to: usize, init: B, mut f: F) -> B +where + C: ColumnId, + S: ReadableColumnarVec, + F: FnMut(B, S::T) -> B, +{ + let mut acc = Some(init); + source.for_each_column_chunk_at(&[column], from, to, &mut |_, _, values| { + for value in values { + acc = Some(f( + acc.take().expect("column fold accumulator"), + value.clone(), + )); + } + }); + acc.expect("column fold accumulator") +} + +fn try_fold_column( + source: &S, + column: C, + from: usize, + to: usize, + init: B, + mut f: F, +) -> std::result::Result +where + C: ColumnId, + S: ReadableColumnarVec, + F: FnMut(B, S::T) -> std::result::Result, +{ + let from = from.min(source.len()); + let to = to.min(source.len()); + let chunk_size = source.cursor_chunk_size().max(1); + let mut acc = Some(init); + let mut at = from; + while at < to { + let end = (at + chunk_size).min(to); + let mut error = None; + source.for_each_column_chunk_at(&[column], at, end, &mut |_, _, values| { + if error.is_some() { + return; + } + for value in values { + let current = acc.take().expect("column fold accumulator"); + match f(current, value.clone()) { + Ok(next) => acc = Some(next), + Err(err) => { + error = Some(err); + break; + } + } + } + }); + if let Some(error) = error { + return Err(error); + } + at = end; + } + Ok(acc.expect("column fold accumulator")) +} + +impl ReadableVec for ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + fn cursor_chunk_size(&self) -> usize { + self.source.cursor_chunk_size() + } + + fn read_into_at(&self, from: usize, to: usize, out: &mut Vec) { + self.source + .for_each_column_chunk_at(&[self.column], from, to, &mut |_, _, values| { + out.extend_from_slice(values) + }); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(S::T)) { + self.source + .for_each_column_chunk_at(&[self.column], from, to, &mut |_, _, values| { + for value in values { + f(value.clone()); + } + }); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + fold_column(&self.source, self.column, from, to, init, f) + } + + fn try_fold_range_at std::result::Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> std::result::Result { + try_fold_column(&self.source, self.column, from, to, init, f) + } +} + +pub(super) fn fold_readable( + vec: &R, + from: usize, + to: usize, + mut acc: B, + mut f: F, +) -> B +where + I: VecIndex, + T: VecValue, + R: ReadableVec, + F: FnMut(B, T) -> B, +{ + let from = from.min(vec.len()); + let to = to.min(vec.len()); + let chunk = vec.cursor_chunk_size().max(1); + let mut buf = Vec::with_capacity(chunk); + let mut at = from; + while at < to { + let end = (at + chunk).min(to); + buf.clear(); + vec.read_into_at(at, end, &mut buf); + for value in buf.drain(..) { + acc = f(acc, value); + } + at = end; + } + acc +} + +pub(super) fn try_fold_readable( + vec: &R, + from: usize, + to: usize, + mut acc: B, + mut f: F, +) -> std::result::Result +where + I: VecIndex, + T: VecValue, + R: ReadableVec, + F: FnMut(B, T) -> std::result::Result, +{ + let from = from.min(vec.len()); + let to = to.min(vec.len()); + let chunk = vec.cursor_chunk_size().max(1); + let mut buf = Vec::with_capacity(chunk); + let mut at = from; + while at < to { + let end = (at + chunk).min(to); + buf.clear(); + vec.read_into_at(at, end, &mut buf); + for value in buf.drain(..) { + acc = f(acc, value)?; + } + at = end; + } + Ok(acc) +} diff --git a/crates/vecdb/src/variants/columnar/schema.rs b/crates/vecdb/src/variants/columnar/schema.rs new file mode 100644 index 000000000..7af8868d4 --- /dev/null +++ b/crates/vecdb/src/variants/columnar/schema.rs @@ -0,0 +1,105 @@ +use std::fmt::Debug; + +use crate::{Error, ReadableVec, Result, VecIndex, VecValue, Version}; + +use super::{ColumnarSumVec, ColumnarVecColumn}; + +/// Typed description of a fixed column set and its logical row representation. +pub trait ColumnId: Copy + Debug + Eq + Ord + Send + Sync + 'static { + type Row: VecValue + where + T: VecValue; + + /// Bump this whenever the column meaning or physical ordering changes. + const VERSION: Version; + + /// Every valid column in physical storage order. + const ALL: &'static [Self]; + + fn index(self) -> usize; + + fn get(self, row: &Self::Row) -> &T; + + fn get_mut(self, row: &mut Self::Row) -> &mut T; + + fn from_fn(f: F) -> Self::Row + where + T: VecValue, + F: FnMut(Self) -> T; + + fn map(row: Self::Row, f: F) -> Self::Row + where + T: VecValue, + U: VecValue, + F: FnMut(T) -> U; +} + +/// Read-only access to a source that preserves typed column boundaries. +pub trait ReadableColumnarVec: ReadableVec> + Clone +where + C: ColumnId, +{ + type I: VecIndex; + type T: VecValue; + + /// Visits selected columns in the requested order within row-aligned chunks. + /// + /// `row_start` is the absolute index of the first value in `values`. + fn for_each_column_chunk_at(&self, columns: &[C], from: usize, to: usize, f: &mut F) + where + F: FnMut(C, usize, &[Self::T]); + + fn column(&self, column: C) -> ColumnarVecColumn + where + Self: Sized, + { + ColumnarVecColumn::new(self.clone(), column) + } + + fn sum_columns( + &self, + name: &str, + version: Version, + columns: [C; M], + ) -> ColumnarSumVec + where + Self: Sized, + { + ColumnarSumVec::new(name, version, self.clone(), columns) + } +} + +pub(super) fn validate_schema() -> Result<()> { + if C::ALL.is_empty() { + return Err(Error::InvalidArgument( + "ColumnarVec requires at least one column", + )); + } + for (index, &column) in C::ALL.iter().enumerate() { + if column.index() != index { + return Err(Error::InvalidArgument( + "ColumnId::ALL must contain every column in physical index order", + )); + } + } + Ok(()) +} + +pub(super) fn validate_column(column: C) { + let index = column.index(); + assert_eq!( + C::ALL.get(index), + Some(&column), + "invalid column ID at physical index {index}", + ); +} + +pub(super) fn selection_version(kind: u32, columns: &[C]) -> Version { + let mut hash = 2_166_136_261_u32 ^ kind; + for column in columns { + let index = column.index() as u64 + 1; + hash ^= index as u32 ^ (index >> 32) as u32; + hash = hash.wrapping_mul(16_777_619); + } + Version::new(kind * 1_000_000 + hash % 1_000_000 + 1) +} diff --git a/crates/vecdb/src/variants/columnar/sum.rs b/crates/vecdb/src/variants/columnar/sum.rs new file mode 100644 index 000000000..db005e8b4 --- /dev/null +++ b/crates/vecdb/src/variants/columnar/sum.rs @@ -0,0 +1,178 @@ +use std::{ops::AddAssign, sync::Arc}; + +use crate::{AnyVec, ReadOnlyClone, ReadableVec, TypedVec, Version, short_type_name}; + +use super::{ + ColumnId, ReadableColumnarVec, + read::{fold_readable, try_fold_readable}, + schema::{selection_version, validate_column}, +}; + +/// Lazy scalar sum of selected columns from any readable columnar source. +pub struct ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + name: Arc, + base_version: Version, + source: S, + columns: Box<[C]>, + selection_version: Version, +} + +impl Clone for ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + fn clone(&self) -> Self { + Self { + name: Arc::clone(&self.name), + base_version: self.base_version, + source: self.source.clone(), + columns: self.columns.clone(), + selection_version: self.selection_version, + } + } +} + +impl ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + /// Creates a lazy sum from a non-empty array of distinct column IDs. + pub fn new(name: &str, version: Version, source: S, columns: [C; M]) -> Self { + assert!(M > 0, "ColumnarSumVec requires at least one column"); + let mut columns = columns.to_vec(); + for &column in &columns { + validate_column(column); + } + columns.sort_unstable_by_key(|column| column.index()); + assert!( + columns.windows(2).all(|pair| pair[0] != pair[1]), + "ColumnarSumVec cannot sum the same column twice", + ); + let selection_version = selection_version(2, &columns); + Self { + name: Arc::from(name), + base_version: version, + source, + columns: columns.into_boxed_slice(), + selection_version, + } + } +} + +impl AnyVec for ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + fn version(&self) -> Version { + self.base_version + self.source.version() + self.selection_version + } + + fn name(&self) -> &str { + &self.name + } + + fn len(&self) -> usize { + self.source.len() + } + + fn index_type_to_string(&self) -> &'static str { + self.source.index_type_to_string() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + Vec::new() + } +} + +impl TypedVec for ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + type I = S::I; + type T = S::T; +} + +impl ReadableVec for ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, + S::T: AddAssign, +{ + fn cursor_chunk_size(&self) -> usize { + self.source.cursor_chunk_size() + } + + fn read_into_at(&self, from: usize, to: usize, out: &mut Vec) { + let from = from.min(self.len()); + let to = to.min(self.len()); + if from >= to { + return; + } + + let out_start = out.len(); + out.reserve(to - from); + let first = self.columns[0]; + self.source.for_each_column_chunk_at( + &self.columns, + from, + to, + &mut |column, row_start, values| { + let start = out_start + row_start - from; + if column == first { + debug_assert_eq!(start, out.len()); + out.extend_from_slice(values); + } else { + for (sum, value) in out[start..start + values.len()].iter_mut().zip(values) { + *sum += value.clone(); + } + } + }, + ); + } + + fn for_each_range_dyn_at(&self, from: usize, to: usize, f: &mut dyn FnMut(S::T)) { + fold_readable(self, from, to, (), |(), value| f(value)); + } + + fn fold_range_at B>(&self, from: usize, to: usize, init: B, f: F) -> B { + fold_readable(self, from, to, init, f) + } + + fn try_fold_range_at std::result::Result>( + &self, + from: usize, + to: usize, + init: B, + f: F, + ) -> std::result::Result { + try_fold_readable(self, from, to, init, f) + } +} + +impl ReadOnlyClone for ColumnarSumVec +where + C: ColumnId, + S: ReadableColumnarVec, +{ + type ReadOnly = Self; + + fn read_only_clone(&self) -> Self { + self.clone() + } +} diff --git a/crates/vecdb/src/variants/columnar/traits.rs b/crates/vecdb/src/variants/columnar/traits.rs new file mode 100644 index 000000000..6a458ba4a --- /dev/null +++ b/crates/vecdb/src/variants/columnar/traits.rs @@ -0,0 +1,344 @@ +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; + +use rawdb::{Database, Region}; + +use crate::{ + AnyStoredVec, AnyVec, Error, Header, ImportOptions, ImportableVec, ReadableBoxedVec, + ReadableCloneableVec, Result, Stamp, StoredVec, TypedVec, Version, WritableVec, + short_type_name, +}; + +use super::{ + ColumnId, ColumnarVec, ColumnarVecColumn, ReadOnlyColumnarVec, ReadableColumnarVec, + schema::selection_version, +}; + +impl ImportableVec for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn import(db: &Database, name: &str, version: Version) -> Result { + Self::import_with((db, name, version).into()) + } + + fn import_with(options: ImportOptions) -> Result { + Self::import_inner(options, false) + } + + fn forced_import(db: &Database, name: &str, version: Version) -> Result { + Self::forced_import_with((db, name, version).into()) + } + + fn forced_import_with(options: ImportOptions) -> Result { + Self::import_inner(options, true) + } +} + +impl AnyVec for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn version(&self) -> Version { + self.vec.version() + } + + fn name(&self) -> &str { + self.vec.name() + } + + fn len(&self) -> usize { + self.stored_rows + self.pushed.len() + } + + fn index_type_to_string(&self) -> &'static str { + self.vec.index_type_to_string() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::>() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::>() + } + + fn region_names(&self) -> Vec { + self.vec.region_names() + } +} + +impl AnyVec for ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn version(&self) -> Version { + self.vec.version() + } + + fn name(&self) -> &str { + self.vec.name() + } + + fn len(&self) -> usize { + self.visible_rows.get() + } + + fn index_type_to_string(&self) -> &'static str { + self.vec.index_type_to_string() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::>() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::>() + } + + fn region_names(&self) -> Vec { + self.vec.region_names() + } +} + +impl AnyVec for ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + fn version(&self) -> Version { + self.source.version() + selection_version(1, &[self.column]) + } + + fn name(&self) -> &str { + self.source.name() + } + + fn len(&self) -> usize { + self.source.len() + } + + fn index_type_to_string(&self) -> &'static str { + self.source.index_type_to_string() + } + + fn value_type_to_size_of(&self) -> usize { + size_of::() + } + + fn value_type_to_string(&self) -> &'static str { + short_type_name::() + } + + fn region_names(&self) -> Vec { + self.source.region_names() + } +} + +impl TypedVec for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + type I = V::I; + type T = C::Row; +} + +impl TypedVec for ReadOnlyColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + type I = V::I; + type T = C::Row; +} + +impl TypedVec for ColumnarVecColumn +where + C: ColumnId, + S: ReadableColumnarVec, +{ + type I = S::I; + type T = S::T; +} + +impl AnyStoredVec for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn db_path(&self) -> PathBuf { + self.vec.db_path() + } + + fn region(&self) -> &Region { + self.vec.region() + } + + fn header(&self) -> &Header { + self.vec.header() + } + + fn mut_header(&mut self) -> &mut Header { + self.vec.mut_header() + } + + fn saved_stamped_changes(&self) -> u16 { + self.vec.saved_stamped_changes() + } + + fn write(&mut self) -> Result { + self.write_pending() + } + + fn db(&self) -> Database { + self.vec.db() + } + + fn real_stored_len(&self) -> usize { + debug_assert!( + self.vec + .real_stored_len() + .is_multiple_of(Self::COLUMN_COUNT) + ); + self.vec.real_stored_len() / Self::COLUMN_COUNT + } + + fn stored_len(&self) -> usize { + self.stored_rows + } + + fn any_stamped_write_with_changes(&mut self, stamp: Stamp) -> Result<()> { + >>::stamped_write_with_changes(self, stamp) + } + + fn any_save_rollback_state(&mut self) { + >>::save_rollback_state(self) + } + + fn serialize_changes(&self) -> Result> { + if !self.pushed.is_empty() || self.stored_rows != self.flat_rows() { + return Err(Error::InvalidArgument( + "ColumnarVec changes must be staged before serialization", + )); + } + self.vec.serialize_changes() + } + + fn remove(self) -> Result<()> { + self.vec.remove() + } + + fn any_truncate_if_needed_at(&mut self, index: usize) -> Result<()> { + >>::truncate_if_needed_at(self, index) + } + + fn any_reset(&mut self) -> Result<()> { + >>::reset(self) + } +} + +impl WritableVec> for ColumnarVec +where + V: StoredVec, + C: ColumnId, +{ + fn push(&mut self, value: C::Row) { + self.pushed.push(value); + } + + fn pushed(&self) -> &[C::Row] { + &self.pushed + } + + fn truncate_if_needed_at(&mut self, index: usize) -> Result<()> { + let len = self.len(); + if index >= len { + return Ok(()); + } + if index < self.stored_rows { + self.stored_rows = index; + self.pushed.clear(); + } else { + self.pushed.truncate(index - self.stored_rows); + } + Ok(()) + } + + fn reset(&mut self) -> Result<()> { + let gate = Arc::clone(&self.gate); + let _guard = gate.write(); + self.stored_rows = 0; + self.pushed.clear(); + self.vec.reset()?; + self.visible_rows.set(0); + Ok(()) + } + + fn reset_unsaved(&mut self) { + let gate = Arc::clone(&self.gate); + let _guard = gate.write(); + self.vec.reset_unsaved(); + self.stored_rows = self.flat_rows(); + self.pushed.clear(); + self.visible_rows.set(self.stored_rows); + } + + fn is_dirty(&self) -> bool { + self.stored_rows != self.flat_rows() || !self.pushed.is_empty() || self.vec.is_dirty() + } + + fn stamped_write_with_changes(&mut self, stamp: Stamp) -> Result<()> { + let gate = Arc::clone(&self.gate); + let _guard = gate.write(); + self.stage_pending()?; + self.vec.stamped_write_with_changes(stamp)?; + self.stored_rows = self.flat_rows(); + self.visible_rows.set(self.stored_rows); + Ok(()) + } + + fn rollback(&mut self) -> Result<()> { + let gate = Arc::clone(&self.gate); + let _guard = gate.write(); + self.pushed.clear(); + self.vec.rollback()?; + self.stored_rows = self.flat_rows(); + debug_assert!(self.vec.stored_len().is_multiple_of(Self::COLUMN_COUNT)); + self.visible_rows + .set(self.vec.stored_len() / Self::COLUMN_COUNT); + Ok(()) + } + + fn find_rollback_files(&self) -> Result> { + self.vec.find_rollback_files() + } + + fn save_rollback_state(&mut self) { + self.vec.save_rollback_state(); + } +} + +impl StoredVec for ColumnarVec +where + V: StoredVec + 'static, + C: ColumnId, +{ + type ReadOnly = ReadOnlyColumnarVec; + + fn read_only_clone(&self) -> Self::ReadOnly { + ColumnarVec::read_only_clone(self) + } +} + +impl ReadableCloneableVec> for ColumnarVec +where + V: StoredVec + 'static, + C: ColumnId, +{ + fn read_only_boxed_clone(&self) -> ReadableBoxedVec> { + Box::new(self.read_only_clone()) + } +} diff --git a/crates/vecdb/src/variants/compressed/inner/read_write/mod.rs b/crates/vecdb/src/variants/compressed/inner/read_write/mod.rs index cbba11ea6..06b6b2e82 100644 --- a/crates/vecdb/src/variants/compressed/inner/read_write/mod.rs +++ b/crates/vecdb/src/variants/compressed/inner/read_write/mod.rs @@ -51,8 +51,7 @@ where /// # Warning /// /// This will DELETE all existing data on format/version errors. Use with caution. - pub fn forced_import_with(mut options: ImportOptions, format: Format) -> Result { - options.version = options.version + VERSION; + pub fn forced_import_with(options: ImportOptions, format: Format) -> Result { let res = Self::import_with(options, format); match res { Err(Error::WrongEndian) diff --git a/crates/vecdb/src/variants/mod.rs b/crates/vecdb/src/variants/mod.rs index fd5a8ac14..5dadd281a 100644 --- a/crates/vecdb/src/variants/mod.rs +++ b/crates/vecdb/src/variants/mod.rs @@ -1,4 +1,5 @@ mod cached; +mod columnar; mod compressed; mod eager; mod lazy; @@ -6,6 +7,7 @@ mod macros; mod raw; pub use cached::*; +pub use columnar::*; pub use compressed::*; pub use eager::*; pub use lazy::*; diff --git a/crates/vecdb/src/variants/raw/inner/read_write/mod.rs b/crates/vecdb/src/variants/raw/inner/read_write/mod.rs index 3ff0b7de2..bf79f099c 100644 --- a/crates/vecdb/src/variants/raw/inner/read_write/mod.rs +++ b/crates/vecdb/src/variants/raw/inner/read_write/mod.rs @@ -56,8 +56,7 @@ where /// # Warning /// /// This will DELETE all existing data on format/version errors. Use with caution. - pub fn forced_import_with(mut options: ImportOptions, format: Format) -> Result { - options.version = options.version + VERSION; + pub fn forced_import_with(options: ImportOptions, format: Format) -> Result { let res = Self::import_with(options, format); match res { Err(Error::WrongEndian) diff --git a/crates/vecdb/tests/columnar.rs b/crates/vecdb/tests/columnar.rs new file mode 100644 index 000000000..1809c2833 --- /dev/null +++ b/crates/vecdb/tests/columnar.rs @@ -0,0 +1,629 @@ +use std::{ + ops::Add, + panic::{AssertUnwindSafe, catch_unwind}, + sync::Arc, + thread, +}; + +use tempfile::tempdir; +use vecdb::{ + AnyStoredVec, AnyVec, BytesVec, ColumnId, ColumnarSumVec, ColumnarVec, Database, ImportOptions, + ImportableVec, LazyColumnarVec, PrintableIndex, ReadableColumnarVec, ReadableVec, Result, + Stamp, StoredVec, UnaryTransform, VecIndex, VecValue, Version, WritableVec, +}; + +const COLUMNS: usize = 3; +const U64S_PER_PAGE: usize = 16 * 1024 / size_of::(); + +macro_rules! column_ids { + ($name:ident, $count:literal, $version:expr, [$($column:ident),+ $(,)?]) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[repr(usize)] + enum $name { + $($column),+ + } + + impl ColumnId for $name { + type Row + = [T; $count] + where + T: VecValue; + + const VERSION: Version = $version; + const ALL: &'static [Self] = &[$(Self::$column),+]; + + fn index(self) -> usize { + self as usize + } + + fn get(self, row: &Self::Row) -> &T { + &row[self as usize] + } + + fn get_mut(self, row: &mut Self::Row) -> &mut T { + &mut row[self as usize] + } + + fn from_fn(mut f: F) -> Self::Row + where + T: VecValue, + F: FnMut(Self) -> T, + { + std::array::from_fn(|index| f(Self::ALL[index])) + } + + fn map(row: Self::Row, f: F) -> Self::Row + where + T: VecValue, + U: VecValue, + F: FnMut(T) -> U, + { + row.map(f) + } + } + }; +} + +column_ids!(TestColumn, 3, Version::ONE, [First, Second, Third]); +column_ids!(ChangedTestColumn, 3, Version::TWO, [First, Second, Third]); +column_ids!( + FiveColumn, + 5, + Version::ONE, + [First, Second, Third, Fourth, Fifth] +); + +struct Double; + +impl UnaryTransform for Double { + fn apply(value: u64) -> u64 { + value * 2 + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct CapacityIndex(usize); + +impl From for CapacityIndex { + fn from(value: usize) -> Self { + Self(value) + } +} + +impl From for usize { + fn from(value: CapacityIndex) -> Self { + value.0 + } +} + +impl Add for CapacityIndex { + type Output = Self; + + fn add(self, rhs: usize) -> Self::Output { + Self(self.0 + rhs) + } +} + +impl PrintableIndex for CapacityIndex { + fn to_string() -> &'static str { + "columnar_capacity" + } + + fn to_possible_strings() -> &'static [&'static str] { + &["columnar_capacity"] + } +} + +impl VecIndex for CapacityIndex { + const INITIAL_CAPACITY: usize = 1_200_000; +} + +fn row(index: usize) -> [u64; COLUMNS] { + [ + index as u64, + 1_000_000 + index as u64 * 3, + 9_000_000 - index as u64 * 2, + ] +} + +#[test] +fn bytes_columnar_roundtrip_and_projection() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "matrix", Version::ONE)?; + for index in 0..2_000 { + vec.push(row(index)); + } + vec.write()?; + for index in 2_000..5_000 { + vec.push(row(index)); + } + + assert_eq!(vec.collect_one_at(2_345), Some(row(2_345))); + vec.write()?; + assert_eq!(vec.region_names().len(), 1); + + let second = vec.column(TestColumn::Second); + assert_eq!(second.collect_one_at(2_345), Some(row(2_345)[1])); + assert_eq!(second.collect_range_at(4_990, 5_000).len(), 10); + drop(second); + drop(vec); + + let mut vec = V::import(&db, "matrix", Version::ONE)?; + assert_eq!(vec.len(), 5_000); + assert_eq!(vec.collect_one_at(0), Some(row(0))); + assert_eq!(vec.collect_one_at(4_999), Some(row(4_999))); + assert_eq!( + vec.column(TestColumn::Third).collect_one_at(4_321), + Some(row(4_321)[2]) + ); + + vec.truncate_if_needed_at(2_503)?; + for index in 2_503..2_777 { + vec.push(row(index)); + } + vec.write()?; + drop(vec); + + let vec = V::import(&db, "matrix", Version::ONE)?; + assert_eq!(vec.len(), 2_777); + assert_eq!(vec.collect_one_at(2_502), Some(row(2_502))); + assert_eq!(vec.collect_one_at(2_776), Some(row(2_776))); + Ok(()) +} + +#[test] +fn projection_is_isolated_from_pushed_rows_until_write() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "projection_isolation", Version::ONE)?; + for index in 0..100 { + vec.push(row(index)); + } + vec.write()?; + let projection = vec.column(TestColumn::Second); + let sum = vec.sum_columns( + "projection_isolation_sum", + Version::ONE, + [TestColumn::First, TestColumn::Second], + ); + + vec.push(row(100)); + assert_eq!(vec.len(), 101); + assert_eq!(projection.len(), 100); + assert_eq!(projection.collect_one_at(100), None); + assert_eq!(sum.len(), 100); + assert_eq!(sum.collect_one_at(100), None); + + vec.write()?; + assert_eq!(projection.len(), 101); + assert_eq!(projection.collect_one_at(100), Some(row(100)[1])); + assert_eq!(sum.len(), 101); + assert_eq!(sum.collect_one_at(100), Some(row(100)[0] + row(100)[1])); + Ok(()) +} + +#[test] +fn lazy_columnar_transform_preserves_rows_and_columns() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "lazy", Version::ONE)?; + for index in 0..5_000 { + vec.push(row(index)); + } + vec.write()?; + + let lazy = LazyColumnarVec::<_, u64, TestColumn>::transformed::( + "doubled", + Version::ONE, + vec.read_only_clone(), + ); + for index in [0, U64S_PER_PAGE - 1, U64S_PER_PAGE, 4_999] { + assert_eq!( + lazy.collect_one_at(index), + Some(row(index).map(|value| value * 2)) + ); + assert_eq!( + lazy.column(TestColumn::Second).collect_one_at(index), + Some(row(index)[1] * 2) + ); + } + + let from = U64S_PER_PAGE - 10; + let to = U64S_PER_PAGE * 2 + 10; + assert_eq!( + lazy.collect_range_at(from, to), + (from..to) + .map(|index| row(index).map(|value| value * 2)) + .collect::>() + ); + assert_eq!( + lazy.column(TestColumn::Second) + .fold_range_at(from, to, 0, u64::wrapping_add), + (from..to) + .map(|index| row(index)[1] * 2) + .fold(0, u64::wrapping_add) + ); + Ok(()) +} + +#[test] +fn columnar_sum_accepts_stored_and_lazy_sources() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "sum", Version::ONE)?; + for index in 0..5_000 { + vec.push(row(index)); + } + vec.write()?; + + let stored_sum = vec.sum_columns( + "stored_sum", + Version::ONE, + [TestColumn::Third, TestColumn::First], + ); + let source = vec.read_only_clone(); + let reordered_sum = ColumnarSumVec::new( + "reordered_sum", + Version::ONE, + source.clone(), + [TestColumn::First, TestColumn::Third], + ); + assert_eq!(stored_sum.version(), reordered_sum.version()); + let different_sum = ColumnarSumVec::new( + "different_sum", + Version::ONE, + source.clone(), + [TestColumn::First, TestColumn::Second], + ); + assert_ne!(stored_sum.version(), different_sum.version()); + + assert!( + catch_unwind(AssertUnwindSafe(|| ColumnarSumVec::new( + "empty_sum", + Version::ONE, + source.clone(), + [], + ))) + .is_err() + ); + assert!( + catch_unwind(AssertUnwindSafe(|| ColumnarSumVec::new( + "duplicate_sum", + Version::ONE, + source.clone(), + [TestColumn::First, TestColumn::First], + ))) + .is_err() + ); + + let lazy = LazyColumnarVec::<_, u64, TestColumn>::transformed::( + "doubled", + Version::ONE, + source, + ); + let lazy_sum = lazy.sum_columns( + "lazy_sum", + Version::ONE, + [TestColumn::First, TestColumn::Third], + ); + + for index in [0, U64S_PER_PAGE - 1, U64S_PER_PAGE, 4_999] { + let expected = row(index)[0] + row(index)[2]; + assert_eq!(stored_sum.collect_one_at(index), Some(expected)); + assert_eq!(lazy_sum.collect_one_at(index), Some(expected * 2)); + } + + let from = U64S_PER_PAGE - 10; + let to = U64S_PER_PAGE * 2 + 10; + let expected = (from..to) + .map(|index| row(index)[0] + row(index)[2]) + .collect::>(); + assert_eq!(stored_sum.collect_range_at(from, to), expected); + assert_eq!( + lazy_sum.collect_range_at(from, to), + expected + .into_iter() + .map(|value| value * 2) + .collect::>() + ); + Ok(()) +} + +#[test] +fn raw_data_is_column_major_within_each_page_block() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "layout", Version::ONE)?; + for index in 0..5_000 { + vec.push(row(index)); + } + vec.write()?; + + let bytes = vec.region().create_reader().read_all().to_vec(); + let stored = bytes[vecdb::HEADER_OFFSET..] + .chunks_exact(size_of::()) + .map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap())) + .collect::>(); + let mut expected = Vec::with_capacity(5_000 * COLUMNS); + for block_start in (0..5_000).step_by(U64S_PER_PAGE) { + let block_end = (block_start + U64S_PER_PAGE).min(5_000); + for column in 0..COLUMNS { + for index in block_start..block_end { + expected.push(row(index)[column]); + } + } + } + assert_eq!(stored, expected); + Ok(()) +} + +#[test] +fn column_count_is_part_of_storage_version() -> Result<()> { + type ThreeColumns = ColumnarVec, TestColumn>; + type FiveColumns = ColumnarVec, FiveColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = ThreeColumns::forced_import(&db, "column_count", Version::ONE)?; + for index in 0..5 { + vec.push(row(index)); + } + vec.write()?; + drop(vec); + + assert!(FiveColumns::import(&db, "column_count", Version::ONE).is_err()); + let vec = FiveColumns::forced_import(&db, "column_count", Version::ONE)?; + assert!(vec.is_empty()); + Ok(()) +} + +#[test] +fn column_schema_version_is_part_of_storage_version() -> Result<()> { + type Original = ColumnarVec, TestColumn>; + type Changed = ColumnarVec, ChangedTestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = Original::forced_import(&db, "column_schema", Version::ONE)?; + vec.push(row(0)); + vec.write()?; + drop(vec); + + assert!(Changed::import(&db, "column_schema", Version::ONE).is_err()); + let vec = Changed::forced_import(&db, "column_schema", Version::ONE)?; + assert!(vec.is_empty()); + Ok(()) +} + +#[test] +fn projected_try_fold_stops_at_the_first_error() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "try_fold", Version::ONE)?; + for index in 0..5_000 { + vec.push(row(index)); + } + vec.write()?; + + let mut seen = 0; + let result = vec.column(TestColumn::First).try_fold_range_at( + 0, + 5_000, + (), + |(), _| -> std::result::Result<(), ()> { + seen += 1; + if seen == 17 { Err(()) } else { Ok(()) } + }, + ); + assert_eq!(result, Err(())); + assert_eq!(seen, 17); + Ok(()) +} + +#[test] +fn reset_and_rollback_persist() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let options = ImportOptions::new(&db, "changes", Version::ONE).with_saved_stamped_changes(3); + let mut vec = V::forced_import_with(options)?; + + for index in 0..100 { + vec.push(row(index)); + } + vec.stamped_write_with_changes(Stamp::new(1))?; + for index in 100..150 { + vec.push(row(index)); + } + vec.stamped_write_with_changes(Stamp::new(2))?; + vec.rollback()?; + assert_eq!(vec.len(), 100); + assert_eq!(vec.collect_one_at(99), Some(row(99))); + + vec.stamped_write_with_changes(Stamp::new(2))?; + vec.reset()?; + vec.write()?; + drop(vec); + + let vec = V::import_with(options)?; + assert!(vec.is_empty()); + Ok(()) +} + +#[test] +fn initial_capacity_is_reserved_for_every_column() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "capacity", Version::ONE)?; + let expected = + vecdb::HEADER_OFFSET + CapacityIndex::INITIAL_CAPACITY * COLUMNS * size_of::(); + assert!(vec.region().meta().reserved() >= expected); + + for index in 0..10_000 { + vec.push(row(index)); + } + vec.write()?; + drop(vec); + + let vec = V::import(&db, "capacity", Version::ONE)?; + assert_eq!(vec.len(), 10_000); + assert_eq!(vec.collect_one_at(9_999), Some(row(9_999))); + Ok(()) +} + +#[cfg(feature = "pco")] +#[test] +fn pco_columnar_roundtrip_reads_only_selected_stream() -> Result<()> { + use vecdb::PcoVec; + + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "pco_matrix", Version::ONE)?; + for index in 0..10_000 { + vec.push(row(index)); + } + vec.write()?; + drop(vec); + + let vec = V::import(&db, "pco_matrix", Version::ONE)?; + assert_eq!(vec.collect_one_at(9_999), Some(row(9_999))); + assert_eq!( + vec.column(TestColumn::Second) + .collect_range_at(9_990, 10_000), + (9_990..10_000) + .map(|index| row(index)[1]) + .collect::>() + ); + Ok(()) +} + +#[cfg(feature = "pco")] +#[test] +fn pco_repeated_small_writes_keep_partial_pages_raw() -> Result<()> { + use vecdb::PcoVec; + + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "pco_incremental", Version::ONE)?; + for batch in 0..100 { + for index in batch * 100..(batch + 1) * 100 { + vec.push(row(index)); + } + vec.write()?; + } + assert_eq!(vec.collect_one_at(9_999), Some(row(9_999))); + + let pages_region = db.get_region(&vec.region_names()[1]).expect("pages region"); + let bytes = pages_region.create_reader().read_all().to_vec(); + let pages = bytes + .chunks_exact(16) + .map(|bytes| u32::from_le_bytes(bytes[12..16].try_into().unwrap())) + .collect::>(); + let completed_pages = vec.len() / U64S_PER_PAGE * COLUMNS; + for &values in &pages[..completed_pages] { + assert_eq!(values & (1 << 31), 0); + assert_eq!(values as usize, U64S_PER_PAGE); + } + assert!(pages.last().is_some_and(|values| values & (1 << 31) != 0)); + Ok(()) +} + +#[test] +fn concurrent_projection_reads_survive_incremental_writes() -> Result<()> { + type V = ColumnarVec, TestColumn>; + + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = V::forced_import(&db, "concurrent", Version::ONE)?; + for index in 0..1_000 { + vec.push(row(index)); + } + vec.write()?; + let projection = Arc::new(vec.column(TestColumn::Third)); + let readers = (0..4) + .map(|_| { + let projection = Arc::clone(&projection); + thread::spawn(move || { + for _ in 0..2_000 { + let len = projection.len(); + if len != 0 { + assert_eq!(projection.collect_one_at(len - 1), Some(row(len - 1)[2])); + } + thread::yield_now(); + } + }) + }) + .collect::>(); + + for batch in 10..100 { + for index in batch * 100..(batch + 1) * 100 { + vec.push(row(index)); + } + vec.write()?; + } + for reader in readers { + reader.join().expect("reader thread"); + } + assert_eq!(projection.len(), 10_000); + Ok(()) +} + +#[cfg(feature = "lz4")] +#[test] +fn lz4_columnar_roundtrip() -> Result<()> { + run_small_backend_roundtrip::>() +} + +#[cfg(feature = "zstd")] +#[test] +fn zstd_columnar_roundtrip() -> Result<()> { + run_small_backend_roundtrip::>() +} + +#[cfg(feature = "zerocopy")] +#[test] +fn zerocopy_columnar_roundtrip() -> Result<()> { + run_small_backend_roundtrip::>() +} + +fn run_small_backend_roundtrip() -> Result<()> +where + V: StoredVec + 'static, +{ + let temp = tempdir()?; + let db = Database::open(temp.path())?; + let mut vec = ColumnarVec::::forced_import(&db, "backend", Version::ONE)?; + for index in 0..4_200 { + vec.push(row(index)); + } + vec.write()?; + drop(vec); + + let vec = ColumnarVec::::import(&db, "backend", Version::ONE)?; + assert_eq!(vec.collect_one_at(4_199), Some(row(4_199))); + assert_eq!( + vec.column(TestColumn::First).collect_one_at(3_123), + Some(row(3_123)[0]) + ); + Ok(()) +} diff --git a/crates/vecdb/tests/initial_capacity.rs b/crates/vecdb/tests/initial_capacity.rs index 76a7e4476..36be44d33 100644 --- a/crates/vecdb/tests/initial_capacity.rs +++ b/crates/vecdb/tests/initial_capacity.rs @@ -64,3 +64,20 @@ fn index_initial_capacity_is_reserved_and_reused() -> vecdb::Result<()> { Ok(()) } + +#[cfg(feature = "pco")] +#[test] +fn compressed_vec_uses_index_initial_capacity() -> vecdb::Result<()> { + let temp = TempDir::new()?; + let db = Database::open(temp.path())?; + + let vec = vecdb::PcoVec::::forced_import( + &db, + "compressed_values", + Version::ONE, + )?; + let expected = (HEADER_OFFSET + 10_000 * size_of::()).next_multiple_of(PAGE_SIZE); + assert_eq!(vec.region().meta().len(), HEADER_OFFSET); + assert_eq!(vec.region().meta().reserved(), expected); + Ok(()) +} diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 5bf527d00..f1f650e51 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -9465,3 +9465,4 @@ class BrkClient(BrkClientBase): Endpoint: `GET /api.json`""" return self.get_json('/api.json') +