1pub mod art_index;
6pub mod art_key;
7pub mod art_node;
8pub mod buffer_manager;
9pub mod checkpoint;
10pub mod column;
11pub mod column_chunk;
12pub mod compression;
13pub mod csr;
14pub mod csv_reader;
15pub mod free_space_manager;
16pub mod hyperloglog;
17pub mod ice_format;
18pub mod index;
19pub mod lazy_scanner;
20pub mod local_storage;
21pub mod local_wal;
22pub mod node_group;
23pub mod npy_reader;
24pub mod page;
25pub mod page_manager;
26#[cfg(feature = "parquet")]
27pub mod parquet_reader;
28#[cfg(feature = "parquet")]
29pub mod parquet_writer;
30pub mod persistence;
31pub mod predicate;
32pub mod roaring_bitmap;
33pub mod shadow_file;
34pub mod spiller;
35pub mod stats;
36pub mod string_dictionary;
37pub mod table;
38pub mod undo_buffer;
39pub mod update_info;
40pub mod vector_index;
41pub mod version_info;
42pub mod wal;
43pub mod wal_replayer;
44
45use akar_common::error::StorageError;
46use akar_common::memory::MemoryManager;
47use akar_common::types::Value;
48use akar_vector::hnsw::DistanceMetric;
49use buffer_manager::{BufferManager, BufferManagerConfig};
50use checkpoint::checkpoint;
51use std::path::PathBuf;
52use std::sync::{Arc, Mutex};
53use wal::WAL;
54
55pub use art_index::ArtPrimaryKeyIndex;
56pub use art_key::ArtKey;
57pub use column_chunk::{ColumnChunk, NODE_GROUP_SIZE};
58pub use index::{HashIndex, IndexKey, OnDiskHashIndex};
59pub use local_storage::LocalStorage;
60pub use local_wal::LocalWAL;
61pub use node_group::NodeGroup;
62pub use page_manager::PageManager;
63pub use persistence::TablePersistence;
64pub use shadow_file::ShadowFile;
65pub use spiller::{MultiWayStreamMerge, SpillFile, Spiller};
66pub use string_dictionary::StringDictionary;
67pub use table::{ColumnDefinition, NodeTable, RelTable, TableCatalog};
68pub use undo_buffer::UndoBuffer;
69pub use vector_index::{VectorIndexTable, extract_f64_list_from_value};
70pub use wal::WalSink;
71pub use wal_replayer::{ReplayResult, WALReplayer};
72
73pub use wal::log_delete_record;
77pub use wal::log_insert_record;
78pub use wal::log_rel_insert_record;
79pub use wal::log_update_record;
80
81pub fn serialize_values_to_bytes(values: &[Value]) -> Vec<u8> {
85 let mut out = Vec::with_capacity(values.len() * 16);
86 for v in values {
87 out.extend_from_slice(&column::Column::serialize_value(v));
88 }
89 out
90}
91
92impl From<&akar_catalog::CatalogColumn> for ColumnDefinition {
97 fn from(c: &akar_catalog::CatalogColumn) -> Self {
98 ColumnDefinition {
99 name: c.name.clone(),
100 logical_type: c.logical_type,
101 is_primary_key: c.is_primary_key,
102 compression: c.compression,
103 }
104 }
105}
106
107#[allow(dead_code)]
109pub struct StorageManager {
110 db_path: PathBuf,
111 buffer_manager: Arc<Mutex<BufferManager>>,
112 wal: Arc<Mutex<WAL>>,
113 memory_manager: Arc<MemoryManager>,
114 page_manager: Option<Arc<PageManager>>,
116 pub(crate) table_catalog: Arc<TableCatalog>,
118 table_persistence: TablePersistence,
120 spiller: std::sync::RwLock<Option<Arc<Spiller>>>,
123}
124
125#[derive(Debug, Clone)]
127pub struct StorageInfo {
128 pub db_path: String,
129 pub page_size: usize,
130 pub total_pages: u64,
131 pub free_pages: u64,
132}
133
134#[derive(Debug, Clone)]
136pub struct BufferInfo {
137 pub total_memory: usize,
138 pub used_memory: usize,
139 pub num_pinned: usize,
140}
141
142#[derive(Debug, Clone)]
144pub struct FileInfo {
145 pub total_file_size: u64,
146 pub num_data_pages: u64,
147 pub wal_size: u64,
148}
149
150#[derive(Debug, Clone)]
152pub struct FsmInfo {
153 pub total_free_pages: u64,
154 pub num_entries: usize,
155}
156
157impl StorageManager {
158 pub fn new(db_path: PathBuf, memory_manager: Arc<MemoryManager>) -> Self {
159 let _ = std::fs::create_dir_all(&db_path);
161
162 let config = BufferManagerConfig::default();
163 let bm = BufferManager::new(db_path.clone(), memory_manager.clone(), config);
164 let wal_path = if db_path.to_string_lossy() == ":memory:" {
165 let tmp = std::env::temp_dir().join("akar-wal");
167 let _ = std::fs::create_dir_all(&tmp);
168 tmp.join("wal.log")
169 } else {
170 db_path.join("wal.log")
171 };
172 let wal = WAL::new(wal_path);
176 let fsm = Arc::new(free_space_manager::FreeSpaceManager::new());
177 let existing_pages = 0u64; let pm = PageManager::new(db_path.clone(), page::DEFAULT_PAGE_SIZE, existing_pages, fsm);
179 Self {
180 db_path,
181 buffer_manager: Arc::new(Mutex::new(bm)),
182 wal: Arc::new(Mutex::new(wal)),
183 memory_manager,
184 page_manager: Some(Arc::new(pm)),
185 table_catalog: Arc::new(TableCatalog::new()),
186 table_persistence: TablePersistence::new(),
187 spiller: std::sync::RwLock::new(None),
188 }
189 }
190
191 pub fn set_spiller(&self, spiller: Option<Arc<Spiller>>) {
196 *self.spiller.write().unwrap() = spiller.clone();
197 let table_ids: Vec<u64> = self.table_catalog.all_node_tables().iter().map(|r| *r.key()).collect();
201 for table_id in table_ids {
202 if let Some(mut table) = self.table_catalog.get_node_table_mut(table_id) {
203 table.set_spiller(spiller.clone());
204 }
205 }
206 }
207
208 pub fn spiller(&self) -> Option<Arc<Spiller>> {
210 self.spiller.read().unwrap().clone()
211 }
212
213 pub fn open(db_path: PathBuf, memory_manager: Arc<MemoryManager>) -> Self {
219 Self::new(db_path, memory_manager)
220 }
221
222 pub fn page_manager(&self) -> Option<&Arc<PageManager>> {
224 self.page_manager.as_ref()
225 }
226
227 pub fn buffer_manager(&self) -> &Arc<Mutex<BufferManager>> {
228 &self.buffer_manager
229 }
230
231 pub fn wal(&self) -> &Arc<Mutex<WAL>> {
232 &self.wal
233 }
234
235 pub fn db_path(&self) -> &PathBuf {
236 &self.db_path
237 }
238
239 pub fn table_catalog(&self) -> Arc<TableCatalog> {
241 self.table_catalog.clone()
242 }
243
244 pub fn persist_all_tables(&self) -> Result<(), StorageError> {
249 if self.db_path.to_string_lossy() == ":memory:" {
250 return Ok(()); }
252 let page_size = self.buffer_manager.lock().unwrap().page_size();
253 self.table_persistence
254 .persist_all(&self.table_catalog, &self.db_path, &self.buffer_manager, page_size)
255 }
256
257 pub fn load_persisted_tables(&self) -> Result<usize, StorageError> {
262 if self.db_path.to_string_lossy() == ":memory:" {
263 return Ok(0); }
265 let page_size = self.buffer_manager.lock().unwrap().page_size();
266 self.table_persistence
267 .load_all(&self.table_catalog, &self.db_path, &self.buffer_manager, page_size)
268 }
269
270 pub fn drop_table_persistence(&self, table_id: u64) {
272 self.table_persistence
273 .remove(table_id, &self.db_path, &self.buffer_manager);
274 }
275
276 pub fn log_column_write(&self, table_id: u64, col_id: u32, page_id: u64, data: &[u8]) {
278 let mut wal = self.wal.lock().unwrap();
279 wal.log_column_write(table_id, col_id, page_id, data);
280 }
281
282 pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
284 let table = self.table_catalog.create_node_table(name, columns);
285 self.attach_spiller(&table);
286 table
287 }
288
289 fn attach_spiller(&self, table: &NodeTable) {
293 if let Some(spiller) = self.spiller() {
294 if let Some(mut stored) = self.table_catalog.get_node_table_mut(table.table_id) {
295 stored.set_spiller(Some(spiller));
296 }
297 }
298 }
299
300 pub fn restore_node_table(
304 &self,
305 table_id: u64,
306 name: String,
307 columns: Vec<ColumnDefinition>,
308 index_name: Option<&str>,
309 ) -> NodeTable {
310 let table = self
311 .table_catalog
312 .create_node_table_with_id(table_id, name.clone(), columns);
313
314 if let Some(index_name) = index_name {
315 let mut bm = self.buffer_manager.lock().unwrap();
317 let full_path = self.db_path.join(format!("{index_name}.art"));
318 if !bm.is_file_registered(index_name) {
319 bm.register_file(index_name, full_path);
320 }
321 drop(bm);
322
323 let _ = self.table_catalog.create_art_index(&name, index_name);
324 }
325 self.attach_spiller(&table);
326 table
327 }
328
329 pub fn restore_rel_table(
332 &self,
333 table_id: u64,
334 name: String,
335 src_table_id: u64,
336 dst_table_id: u64,
337 columns: Vec<ColumnDefinition>,
338 ) -> RelTable {
339 self.table_catalog
340 .create_rel_table_with_id(table_id, name, src_table_id, dst_table_id, columns)
341 }
342
343 pub fn create_vector_index(
345 &self,
346 name: String,
347 table_name: String,
348 column_name: String,
349 metric: DistanceMetric,
350 dimensions: u32,
351 ) -> VectorIndexTable {
352 let table = self
353 .table_catalog
354 .create_vector_index(name, table_name, column_name, metric, dimensions);
355
356 let mut bm = self.buffer_manager.lock().unwrap();
358 table.register_file(&mut bm, &self.db_path);
359
360 table
361 }
362
363 pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
365 self.table_catalog.get_vector_index_by_name(name)
366 }
367
368 pub fn get_vector_index_by_name_mut(
370 &self,
371 name: &str,
372 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
373 self.table_catalog.get_vector_index_by_name_mut(name)
374 }
375
376 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
379 self.table_catalog.create_art_index(table_name, index_name)?;
380
381 let mut bm = self
383 .buffer_manager
384 .lock()
385 .map_err(|e| StorageError::BufferManager(format!("Lock poisoned: {e}")))?;
386 let full_path = self.db_path.join(format!("{index_name}.art"));
387 let file_name = index_name.to_string();
388 if !bm.is_file_registered(&file_name) {
389 bm.register_file(&file_name, full_path);
390 }
391 drop(bm);
392
393 Ok(())
394 }
395
396 pub fn drop_art_index(&self, table_name: &str, _index_name: &str) -> Result<(), StorageError> {
398 self.table_catalog.drop_art_index(table_name)
399 }
400
401 pub fn get_art_index(&self, table_name: &str) -> Option<crate::ArtPrimaryKeyIndex> {
403 self.table_catalog.get_art_index(table_name)
404 }
405
406 pub fn create_rel_table(
408 &self,
409 name: String,
410 src_table_id: u64,
411 dst_table_id: u64,
412 columns: Vec<ColumnDefinition>,
413 ) -> RelTable {
414 self.table_catalog
415 .create_rel_table(name, src_table_id, dst_table_id, columns)
416 }
417
418 pub fn wal_size(&self) -> usize {
420 self.wal.lock().unwrap().total_size()
421 }
422
423 pub fn checkpoint(&self) -> std::io::Result<checkpoint::CheckpointResult> {
425 let mut wal = self
426 .wal
427 .lock()
428 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
429 checkpoint(&mut wal, &self.buffer_manager)
430 }
431
432 pub fn maybe_checkpoint(
443 &self,
444 threshold: i64,
445 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
446 ) -> std::io::Result<bool> {
447 if threshold == 0 {
448 return Ok(false); }
450
451 let should_checkpoint = if threshold < 0 {
452 true
454 } else {
455 self.wal_size() > threshold as usize
456 };
457
458 if should_checkpoint {
459 let _ = self.checkpoint_with_drain(drain_fn)?;
460 Ok(true)
461 } else {
462 Ok(false)
463 }
464 }
465
466 pub fn checkpoint_with_drain(
479 &self,
480 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
481 ) -> std::io::Result<crate::checkpoint::CheckpointResult> {
482 if let Some(drain) = drain_fn {
484 let drained = drain(std::time::Duration::from_secs(30));
485 if !drained {
486 tracing::warn!("Checkpoint drain timed out — proceeding with best-effort checkpoint");
487 }
488 }
489
490 if let Err(e) = self.persist_all_tables() {
495 tracing::warn!("Persist tables before checkpoint failed: {e}");
496 }
497
498 let mut wal = self
500 .wal
501 .lock()
502 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
503 crate::checkpoint::checkpoint(&mut wal, &self.buffer_manager)
504 }
505
506 pub fn storage_info(&self) -> StorageInfo {
508 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
509 let free_pages = 0u64; StorageInfo {
511 db_path: self.db_path.to_string_lossy().to_string(),
512 page_size: self
513 .page_manager
514 .as_ref()
515 .map(|pm| pm.page_size())
516 .unwrap_or(page::DEFAULT_PAGE_SIZE),
517 total_pages,
518 free_pages,
519 }
520 }
521
522 pub fn buffer_info(&self) -> BufferInfo {
524 let bm = self.buffer_manager.lock().unwrap();
525 let stats = bm.stats();
526 let page_size = bm.page_size();
527 BufferInfo {
528 total_memory: stats.num_frames * page_size,
529 used_memory: (stats.num_frames - (stats.num_frames - stats.pinned_frames - stats.dirty_frames)) * page_size,
530 num_pinned: stats.pinned_frames,
531 }
532 }
533
534 pub fn file_info(&self) -> FileInfo {
536 let db_path = &self.db_path;
537 let wal_path = db_path.join("wal.log");
538 let wal_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
539 let data_size = std::fs::read_dir(db_path)
540 .map(|entries| {
541 entries
542 .filter_map(|e| e.ok())
543 .filter(|e| e.path().extension().map(|x| x == "data").unwrap_or(false))
544 .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
545 .sum::<u64>()
546 })
547 .unwrap_or(0);
548 let page_size = self
549 .page_manager
550 .as_ref()
551 .map(|pm| pm.page_size())
552 .unwrap_or(page::DEFAULT_PAGE_SIZE) as u64;
553 FileInfo {
554 total_file_size: data_size + wal_size,
555 num_data_pages: data_size / page_size.max(1),
556 wal_size,
557 }
558 }
559
560 pub fn fsm_info(&self) -> FsmInfo {
562 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
563 let free_pages = 0u64; FsmInfo {
565 total_free_pages: free_pages,
566 num_entries: total_pages as usize,
567 }
568 }
569
570 pub fn commit_transaction(
592 &self,
593 local_storage: &crate::local_storage::LocalStorage,
594 shadow_file: &crate::shadow_file::ShadowFile,
595 checkpoint_threshold: i64,
596 txn_id: u64,
597 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
598 ) -> Result<(), StorageError> {
599 {
601 let mut wal = self
602 .wal
603 .lock()
604 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
605 wal.append(crate::wal::WALRecord::Commit { transaction_id: txn_id });
606 wal.flush_to_disk()
607 .map_err(|e| StorageError::Wal(format!("WAL flush failed during commit: {e}")))?;
608 }
609
610 let _commit_undo_records = local_storage.flush_to_tables(&self.table_catalog, Some(txn_id))?;
619 tracing::debug!(
623 "commit_transaction: generated {} undo records for txn#{}",
624 _commit_undo_records.len(),
625 txn_id
626 );
627
628 shadow_file
630 .apply(&self.buffer_manager)
631 .map_err(|e| StorageError::ShadowFile(format!("ShadowFile apply failed during commit: {e}")))?;
632
633 if let Err(e) = self.maybe_checkpoint(checkpoint_threshold, drain_fn) {
637 tracing::warn!("Checkpoint after commit failed: {e}");
638 }
640
641 Ok(())
642 }
643
644 pub fn rollback_transaction(
656 &self,
657 local_storage: &mut crate::local_storage::LocalStorage,
658 shadow_file: &mut crate::shadow_file::ShadowFile,
659 txn_id: u64,
660 undo_records: &[akar_transaction::UndoRecord],
661 ) -> Result<(), StorageError> {
662 {
664 let mut wal = self
665 .wal
666 .lock()
667 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
668 wal.append(crate::wal::WALRecord::Rollback { transaction_id: txn_id });
669 let _ = wal.flush_to_disk();
670 }
671
672 for record in undo_records.iter().rev() {
674 if let Some(mut table) = self.table_catalog.get_node_table_mut(record.table_id) {
675 match record.undo_type {
676 akar_transaction::UndoType::Update => {
677 let values = deserialize_values_from_bytes(&record.old_data, 1);
678 if let Some(val) = values.into_iter().next() {
679 table
680 .update_cell(record.row_id, record.column as usize, val)
681 .map_err(|e| {
682 StorageError::Undo(format!(
683 "Undo failed for table {} row {}: {e}",
684 record.table_id, record.row_id
685 ))
686 })?;
687 }
688 }
689 akar_transaction::UndoType::Insert => {
690 let _ = table.delete_row(record.row_id);
692 }
693 akar_transaction::UndoType::Delete => {
694 let num_cols = table.columns.len();
696 let values = deserialize_values_from_bytes(&record.old_data, num_cols);
697 for (col_idx, val) in values.into_iter().enumerate() {
698 let _ = table.update_cell(record.row_id, col_idx, val);
699 }
700 }
701 }
702 } else if let Some(mut rel) = self.table_catalog.get_rel_table_mut(record.table_id) {
703 match record.undo_type {
706 akar_transaction::UndoType::Update => {
707 let values = deserialize_values_from_bytes(&record.old_data, 1);
708 if let Some(val) = values.into_iter().next() {
709 let _ = rel.update_cell(record.row_id as usize, record.column as usize, val);
710 }
711 }
712 akar_transaction::UndoType::Insert => {
713 let _ = rel.delete_edge(record.row_id as usize);
715 }
716 akar_transaction::UndoType::Delete => {
717 let num_cols = rel.columns.len();
719 let values = deserialize_values_from_bytes(&record.old_data, num_cols + 2);
720 let mut iter = values.into_iter();
721 let src = match iter.next() {
722 Some(Value::UInt64(v)) => v,
723 Some(Value::Int64(v)) if v >= 0 => v as u64,
724 _ => u64::MAX,
725 };
726 let dst = match iter.next() {
727 Some(Value::UInt64(v)) => v,
728 Some(Value::Int64(v)) if v >= 0 => v as u64,
729 _ => u64::MAX,
730 };
731 let props: Vec<_> = iter.collect();
732 let _ = rel.restore_deleted_edge(record.row_id as usize, src, dst, props);
733 }
734 }
735 }
736 }
737
738 local_storage.clear();
740 shadow_file.discard();
741
742 Ok(())
743 }
744
745 pub fn recover(&self) -> std::io::Result<usize> {
766 match self.load_persisted_tables() {
768 Ok(n) if n > 0 => tracing::info!("Restored {n} table(s) from durable column mirrors"),
769 Ok(_) => {}
770 Err(e) => tracing::warn!("Failed to restore tables from column mirrors: {e}"),
771 }
772
773 let mut wal = self
774 .wal
775 .lock()
776 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
777
778 wal.load_from_disk()?;
780
781 if wal.is_empty() {
782 return Ok(0); }
784
785 let mut data_records = 0usize;
786 let catalog = self.table_catalog.clone();
787
788 wal.replay(|record| replay_data_record(record, &catalog, &mut data_records))?;
790
791 if data_records > 0 {
795 self.persist_all_tables()
796 .map_err(|e| std::io::Error::other(format!("WAL recovery: persist tables failed: {e}")))?;
797 }
798
799 if let Err(e) = checkpoint(&mut wal, &self.buffer_manager) {
801 tracing::warn!("WAL recovery: checkpoint after replay failed: {e}");
802 }
803
804 Ok(data_records)
805 }
806}
807
808pub(crate) fn replay_data_record(
822 record: &crate::wal::WALRecord,
823 catalog: &Arc<TableCatalog>,
824 data_records: &mut usize,
825) -> std::io::Result<()> {
826 use crate::wal::WALRecord;
827
828 let as_u64 = |v: &Value| -> Option<u64> {
829 match v {
830 Value::UInt64(x) => Some(*x),
831 Value::Int64(x) => (*x >= 0).then_some(*x as u64),
832 _ => None,
833 }
834 };
835
836 match record {
837 WALRecord::Insert { table_id, data } => {
838 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
839 let values = deserialize_values_from_bytes(data, table.columns.len());
840 if let Err(e) = table.insert_row(values) {
841 return Err(std::io::Error::other(format!("WAL recovery insert failed: {e}")));
842 }
843 *data_records += 1;
844 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
845 let values = deserialize_values_from_bytes(data, rel.columns.len() + 2);
846 if values.len() < 2 {
847 return Err(std::io::Error::other("WAL recovery: rel insert payload too short"));
848 }
849 let (Some(src), Some(dst)) = (as_u64(&values[0]), as_u64(&values[1])) else {
850 return Err(std::io::Error::other("WAL recovery: rel insert endpoints missing"));
851 };
852 if let Err(e) = rel.insert_rel(src, dst, values[2..].to_vec()) {
853 return Err(std::io::Error::other(format!("WAL recovery rel insert failed: {e}")));
854 }
855 *data_records += 1;
856 } else {
857 tracing::debug!("WAL recovery: table {table_id} not found; skipping Insert");
858 }
859 }
860 WALRecord::Delete { table_id, row_id } => {
861 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
862 if let Err(e) = table.delete_row(*row_id) {
863 return Err(std::io::Error::other(format!("WAL recovery delete failed: {e}")));
864 }
865 *data_records += 1;
866 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
867 if let Err(e) = rel.delete_edge(*row_id as usize) {
868 return Err(std::io::Error::other(format!("WAL recovery edge delete failed: {e}")));
869 }
870 *data_records += 1;
871 } else {
872 tracing::debug!("WAL recovery: table {table_id} not found; skipping Delete");
873 }
874 }
875 WALRecord::Update {
876 table_id,
877 row_id,
878 column,
879 data,
880 } => {
881 let values = deserialize_values_from_bytes(data, 1);
882 if let Some(val) = values.into_iter().next() {
883 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
884 if let Err(e) = table.update_cell(*row_id, *column as usize, val) {
885 return Err(std::io::Error::other(format!("WAL recovery update failed: {e}")));
886 }
887 *data_records += 1;
888 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
889 if let Err(e) = rel.update_cell(*row_id as usize, *column as usize, val) {
890 return Err(std::io::Error::other(format!("WAL recovery edge update failed: {e}")));
891 }
892 *data_records += 1;
893 } else {
894 tracing::debug!("WAL recovery: table {table_id} not found; skipping Update");
895 }
896 }
897 }
898 WALRecord::LocalWALData { data } => {
899 for sub in crate::wal::decode_wal_buffer(data)? {
900 replay_data_record(&sub, catalog, data_records)?;
901 }
902 }
903 WALRecord::UpdateFsm { .. } => {
904 }
907 WALRecord::ColumnWrite { .. } => {
908 }
913 WALRecord::Commit { .. } | WALRecord::Rollback { .. } => {
914 }
919 WALRecord::Checkpoint => {
920 }
924 WALRecord::CreateTable { .. }
928 | WALRecord::DropTable { .. }
929 | WALRecord::AlterTable { .. }
930 | WALRecord::CreateIndex { .. }
931 | WALRecord::DropIndex { .. }
932 | WALRecord::CreateSequence { .. } => {
933 }
936 }
937 Ok(())
938}
939
940pub(crate) fn deserialize_values_from_bytes(data: &[u8], expected_count: usize) -> Vec<Value> {
941 use crate::column::Column;
942
943 if data.is_empty() || expected_count == 0 {
944 return Vec::new();
945 }
946
947 let mut values = Vec::with_capacity(expected_count);
952 let mut pos = 0usize;
953 for _ in 0..expected_count {
954 if pos >= data.len() {
955 values.push(Value::Null);
956 continue;
957 }
958 match Column::deserialize_value(data, &mut pos) {
959 Ok(v) => values.push(v),
960 Err(_) => {
961 while values.len() < expected_count {
963 values.push(Value::Null);
964 }
965 break;
966 }
967 }
968 }
969
970 values
971}
972
973#[cfg(test)]
979mod integration_tests {
980 use super::*;
981 use crate::column::{Column, TAG_INT64, TAG_STRING};
982 use crate::page::DEFAULT_PAGE_SIZE;
983 use crate::wal::WALRecord;
984 use akar_common::enums::CompressionType;
985 use akar_common::types::{LogicalTypeID, Value};
986
987 fn setup_integration() -> (StorageManager, tempfile::TempDir) {
991 let dir = tempfile::tempdir().unwrap();
992 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
993 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
994 (sm, dir)
995 }
996
997 #[test]
1001 fn test_table_full_persistence_cycle() {
1002 let (sm, _dir) = setup_integration();
1003
1004 let mut table = sm.create_node_table(
1006 "Person".into(),
1007 vec![
1008 ColumnDefinition {
1009 compression: akar_common::enums::CompressionType::Uncompressed,
1010 name: "name".into(),
1011 logical_type: LogicalTypeID::String,
1012 is_primary_key: true,
1013 },
1014 ColumnDefinition {
1015 compression: akar_common::enums::CompressionType::Uncompressed,
1016 name: "age".into(),
1017 logical_type: LogicalTypeID::Int64,
1018 is_primary_key: false,
1019 },
1020 ],
1021 );
1022 assert_eq!(table.table_id, 0);
1023
1024 table
1026 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1027 .unwrap();
1028 table
1029 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1030 .unwrap();
1031 table
1032 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1033 .unwrap();
1034 assert_eq!(table.num_rows, 3);
1035
1036 assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1038 assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1039
1040 let names = table.scan_column(0, 0, 3, None, &[]);
1042 assert_eq!(names.len(), 3);
1043 assert_eq!(names[0], Value::String("Alice".into()));
1044
1045 let ages = table.scan_column(1, 1, 2, None, &[]);
1046 assert_eq!(ages.len(), 2);
1047 assert_eq!(ages[0], Value::Int64(25));
1048 }
1049
1050 #[test]
1054 fn test_wal_recovery_cycle() {
1055 let dir = tempfile::tempdir().unwrap();
1056 let wal_path = dir.path().join("wal.log");
1057
1058 #[allow(unused_variables)]
1060 let (wal_records_count, column_count) = {
1061 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1062 let config = BufferManagerConfig::default();
1063 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1064 let mut wal = WAL::new(wal_path.clone());
1065
1066 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1067
1068 for i in 0i64..10 {
1070 col.append_value(&Value::Int64(i)).unwrap();
1071 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1072 }
1073 wal.append(WALRecord::Commit { transaction_id: 1 });
1074 let count = wal.len();
1075
1076 wal.flush_to_disk().unwrap();
1078
1079 {
1081 let mut bm_lock = bm.lock().unwrap();
1082 bm_lock.flush_all().unwrap();
1083 }
1084
1085 for i in 0i64..10 {
1087 let v = col.get_value(i as u64).unwrap();
1088 assert_eq!(v, Value::Int64(i), "Pre-crash data mismatch at {}", i);
1089 }
1090
1091 (count, 10)
1092 }; assert!(wal_path.exists(), "WAL file should exist after flush");
1096 let file_len = std::fs::metadata(&wal_path).unwrap().len();
1097 assert!(file_len > 0, "WAL file should have content, got {} bytes", file_len);
1098
1099 assert_eq!(
1104 wal_records_count, 11,
1105 "Expected 10 ColumnWrite + 1 Commit = 11 records, got {}",
1106 wal_records_count
1107 );
1108 assert_eq!(column_count, 10);
1109 }
1110
1111 #[test]
1115 fn test_compression_full_roundtrip() {
1116 let dir = tempfile::tempdir().unwrap();
1118 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1119 let config = BufferManagerConfig::default();
1120 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1121
1122 let mut col_int = Column::with_compression(
1123 LogicalTypeID::Int64,
1124 0,
1125 0,
1126 dir.path(),
1127 bm.clone(),
1128 DEFAULT_PAGE_SIZE,
1129 CompressionType::IntegerBitpacking,
1130 );
1131
1132 let test_values: Vec<i64> = vec![0, 1, 42, 127, 255, 65535, 1_000_000, i64::MAX, i64::MIN, -1];
1134 for v in &test_values {
1135 col_int.append_value(&Value::Int64(*v)).unwrap();
1136 }
1137
1138 for (i, expected) in test_values.iter().enumerate() {
1140 let v = col_int.get_value(i as u64).unwrap();
1141 assert_eq!(v, Value::Int64(*expected), "IntegerBitpacking mismatch at index {}", i);
1142 }
1143
1144 let mut col_float = Column::with_compression(
1146 LogicalTypeID::Double,
1147 0,
1148 1,
1149 dir.path(),
1150 bm.clone(),
1151 DEFAULT_PAGE_SIZE,
1152 CompressionType::Float,
1153 );
1154
1155 let floats: Vec<f64> = vec![1.0, std::f64::consts::PI, -2.5e10, 0.0, f64::MIN_POSITIVE, f64::MAX];
1156 for v in &floats {
1157 col_float.append_value(&Value::Double(*v)).unwrap();
1158 }
1159
1160 for (i, expected) in floats.iter().enumerate() {
1161 let v = col_float.get_value(i as u64).unwrap();
1162 match v {
1163 Value::Double(d) => assert!(
1164 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1165 "Float compression mismatch at {}: got {}, expected {}",
1166 i,
1167 d,
1168 expected
1169 ),
1170 _ => panic!("Expected Double, got {:?}", v),
1171 }
1172 }
1173
1174 col_int.flush().unwrap();
1177 col_float.flush().unwrap();
1178
1179 for (i, expected) in test_values.iter().enumerate() {
1181 let v = col_int.get_value(i as u64).unwrap();
1182 assert_eq!(
1183 v,
1184 Value::Int64(*expected),
1185 "After flush: IntegerBitpacking mismatch at {}",
1186 i
1187 );
1188 }
1189 for (i, expected) in floats.iter().enumerate() {
1190 let v = col_float.get_value(i as u64).unwrap();
1191 match v {
1192 Value::Double(d) => assert!(
1193 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1194 "After flush: Float mismatch at {}",
1195 i
1196 ),
1197 _ => panic!("Expected Double after flush"),
1198 }
1199 }
1200 }
1201
1202 #[test]
1206 fn test_multi_node_group_scan() {
1207 let (_sm, _dir) = setup_integration();
1208
1209 let mut table = NodeTable::new(
1211 1,
1212 "BigTable".into(),
1213 vec![
1214 ColumnDefinition {
1215 compression: akar_common::enums::CompressionType::Uncompressed,
1216 name: "id".into(),
1217 logical_type: LogicalTypeID::Int64,
1218 is_primary_key: false,
1219 },
1220 ColumnDefinition {
1221 compression: akar_common::enums::CompressionType::Uncompressed,
1222 name: "value".into(),
1223 logical_type: LogicalTypeID::Int64,
1224 is_primary_key: false,
1225 },
1226 ],
1227 );
1228
1229 let total_rows = NODE_GROUP_SIZE + 500;
1231 for i in 0..total_rows {
1232 table
1233 .insert_row(vec![Value::Int64(i as i64), Value::Int64((i * 2) as i64)])
1234 .unwrap();
1235 }
1236
1237 assert_eq!(table.num_rows, total_rows as u64);
1239
1240 let expected_groups = 2; assert_eq!(
1243 table.node_groups.len(),
1244 expected_groups,
1245 "Expected {} node groups for {} rows",
1246 expected_groups,
1247 total_rows
1248 );
1249
1250 assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1252 assert_eq!(table.node_groups[1].num_nodes, 500);
1253 assert_eq!(table.node_groups[0].start_offset, 0);
1254 assert_eq!(table.node_groups[1].start_offset, NODE_GROUP_SIZE as u64);
1255
1256 let row_at_boundary = (NODE_GROUP_SIZE - 1) as u64;
1259 assert_eq!(
1260 table.get_value(row_at_boundary as usize, 0),
1261 Some(&Value::Int64(row_at_boundary as i64))
1262 );
1263
1264 let row_in_group1 = NODE_GROUP_SIZE as u64;
1266 assert_eq!(
1267 table.get_value(row_in_group1 as usize, 0),
1268 Some(&Value::Int64(row_in_group1 as i64))
1269 );
1270
1271 let scanned = table.scan_column(0, 0, total_rows as u64, None, &[]);
1273 assert_eq!(scanned.len(), total_rows);
1274 assert_eq!(scanned[0], Value::Int64(0));
1275 assert_eq!(scanned[NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1276 assert_eq!(scanned[total_rows - 1], Value::Int64((total_rows - 1) as i64));
1277
1278 let scan_mid = table.scan_column(1, (NODE_GROUP_SIZE - 100) as u64, 200, None, &[]);
1280 assert_eq!(scan_mid.len(), 200);
1281 assert_eq!(scan_mid[0], Value::Int64(((NODE_GROUP_SIZE - 100) * 2) as i64));
1282 assert_eq!(scan_mid[199], Value::Int64(((NODE_GROUP_SIZE + 99) * 2) as i64));
1283
1284 let data = table.to_column_major_data();
1286 assert_eq!(data.len(), 2); assert_eq!(data[0].len(), total_rows);
1288 assert_eq!(data[1].len(), total_rows);
1289 assert_eq!(data[0][NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1290 assert_eq!(data[1][0], Value::Int64(0));
1291 assert_eq!(data[1][total_rows - 1], Value::Int64(((total_rows - 1) * 2) as i64));
1292 }
1293
1294 #[test]
1298 fn test_compressed_multi_group_with_checkpoint() {
1299 let dir = tempfile::tempdir().unwrap();
1300 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
1301
1302 let config = BufferManagerConfig::default();
1304 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1305 let wal_path = dir.path().join("wal.log");
1306 let mut wal = WAL::new(wal_path);
1307
1308 let mut col = Column::with_compression(
1309 LogicalTypeID::Int64,
1310 0,
1311 0,
1312 dir.path(),
1313 bm.clone(),
1314 DEFAULT_PAGE_SIZE,
1315 CompressionType::IntegerBitpacking,
1316 );
1317
1318 let num_values = 500;
1320 for i in 0i64..num_values {
1321 col.append_value(&Value::Int64(i)).unwrap();
1322 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1323 }
1324 wal.append(WALRecord::Commit { transaction_id: 1 });
1325
1326 for i in 0i64..num_values {
1328 let v = col.get_value(i as u64).unwrap();
1329 assert_eq!(v, Value::Int64(i), "Pre-checkpoint mismatch at {}", i);
1330 }
1331
1332 let mut bm_lock = bm.lock().unwrap();
1334 bm_lock.flush_all().unwrap();
1335 drop(bm_lock);
1336
1337 wal.flush_to_disk().unwrap();
1338
1339 for i in 0i64..num_values {
1341 let v = col.get_value(i as u64).unwrap();
1342 assert_eq!(v, Value::Int64(i), "Post-checkpoint mismatch at {}", i);
1343 }
1344
1345 assert!(
1347 col.num_pages > 1,
1348 "Expected multiple pages for {} values, got {}",
1349 num_values,
1350 col.num_pages
1351 );
1352 }
1353
1354 #[test]
1358 fn test_10k_row_stress() {
1359 let dir = tempfile::tempdir().unwrap();
1360 let mm = Arc::new(MemoryManager::new(256 * 1024 * 1024));
1361 let config = BufferManagerConfig::default();
1362 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1363
1364 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1365
1366 for i in 0i64..10_000 {
1368 col.append_value(&Value::Int64(i)).unwrap();
1369 }
1370 assert_eq!(col.num_values, 10_000);
1371
1372 for i in 0i64..10_000 {
1374 let v = col.get_value(i as u64).unwrap();
1375 assert_eq!(v, Value::Int64(i), "Stress test mismatch at {}", i);
1376 }
1377
1378 col.flush().unwrap();
1380 for i in 0i64..10_000 {
1381 let v = col.get_value(i as u64).unwrap();
1382 assert_eq!(v, Value::Int64(i), "Post-flush stress mismatch at {}", i);
1383 }
1384
1385 assert!(
1387 col.num_pages > 1,
1388 "Stress test should use multiple pages, got {}",
1389 col.num_pages
1390 );
1391 }
1392
1393 #[test]
1397 fn test_wal_recovery_insert_then_recover() {
1398 let dir = tempfile::tempdir().unwrap();
1399
1400 let _row_count = {
1402 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1403 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1404
1405 let mut table = sm.create_node_table(
1407 "Person".into(),
1408 vec![
1409 ColumnDefinition {
1410 compression: akar_common::enums::CompressionType::Uncompressed,
1411 name: "name".into(),
1412 logical_type: LogicalTypeID::String,
1413 is_primary_key: true,
1414 },
1415 ColumnDefinition {
1416 compression: akar_common::enums::CompressionType::Uncompressed,
1417 name: "age".into(),
1418 logical_type: LogicalTypeID::Int64,
1419 is_primary_key: false,
1420 },
1421 ],
1422 );
1423
1424 table
1426 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1427 .unwrap();
1428 table
1429 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1430 .unwrap();
1431 table
1432 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1433 .unwrap();
1434
1435 {
1438 sm.table_catalog.create_node_table(
1441 "Person".into(),
1442 vec![
1443 ColumnDefinition {
1444 compression: akar_common::enums::CompressionType::Uncompressed,
1445 name: "name".into(),
1446 logical_type: LogicalTypeID::String,
1447 is_primary_key: true,
1448 },
1449 ColumnDefinition {
1450 compression: akar_common::enums::CompressionType::Uncompressed,
1451 name: "age".into(),
1452 logical_type: LogicalTypeID::Int64,
1453 is_primary_key: false,
1454 },
1455 ],
1456 );
1457 }
1458
1459 let count = table.num_rows;
1460 assert_eq!(count, 3);
1461
1462 {
1464 let mut wal = sm.wal.lock().unwrap();
1465 wal.append(WALRecord::Insert {
1466 table_id: table.table_id,
1467 data: vec![
1468 TAG_STRING, 5, 0, 0, 0, b'A', b'l', b'i', b'c', b'e', TAG_INT64, 30, 0, 0, 0, 0, 0, 0, 0,
1469 ],
1470 });
1471 wal.append(WALRecord::Insert {
1472 table_id: table.table_id,
1473 data: vec![
1474 TAG_STRING, 3, 0, 0, 0, b'B', b'o', b'b', TAG_INT64, 25, 0, 0, 0, 0, 0, 0, 0,
1475 ],
1476 });
1477 wal.append(WALRecord::Insert {
1478 table_id: table.table_id,
1479 data: vec![
1480 TAG_STRING, 7, 0, 0, 0, b'C', b'h', b'a', b'r', b'l', b'i', b'e', TAG_INT64, 35, 0, 0, 0, 0, 0,
1481 0, 0,
1482 ],
1483 });
1484 wal.flush_to_disk().unwrap();
1485 }
1486
1487 count
1488 }; {
1493 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1494 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1495
1496 let wal_path = dir.path().join("wal.log");
1498 assert!(wal_path.exists(), "WAL file should exist for recovery");
1499
1500 sm.create_node_table(
1503 "Person".into(),
1504 vec![
1505 ColumnDefinition {
1506 compression: akar_common::enums::CompressionType::Uncompressed,
1507 name: "name".into(),
1508 logical_type: LogicalTypeID::String,
1509 is_primary_key: true,
1510 },
1511 ColumnDefinition {
1512 compression: akar_common::enums::CompressionType::Uncompressed,
1513 name: "age".into(),
1514 logical_type: LogicalTypeID::Int64,
1515 is_primary_key: false,
1516 },
1517 ],
1518 );
1519
1520 let recovered = sm.recover().unwrap();
1522 assert_eq!(recovered, 3, "Should recover 3 WAL records");
1523
1524 {
1527 let recovered_table = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1528 assert_eq!(recovered_table.num_rows, 3, "Should have recovered 3 rows");
1529 assert_eq!(recovered_table.get_value(0, 0), Some(&Value::String("Alice".into())));
1530 assert_eq!(recovered_table.get_value(1, 0), Some(&Value::String("Bob".into())));
1531 assert_eq!(recovered_table.get_value(2, 0), Some(&Value::String("Charlie".into())));
1532 assert_eq!(recovered_table.get_value(0, 1), Some(&Value::Int64(30)));
1533 assert_eq!(recovered_table.get_value(1, 1), Some(&Value::Int64(25)));
1534 assert_eq!(recovered_table.get_value(2, 1), Some(&Value::Int64(35)));
1535 }
1536
1537 {
1540 let wal = sm.wal.lock().unwrap();
1541 assert_eq!(
1542 wal.len(),
1543 1,
1544 "WAL should have only the checkpoint marker after recovery"
1545 );
1546 assert!(matches!(wal.records()[0], crate::wal::WALRecord::Checkpoint));
1547 }
1548 }
1549 }
1550
1551 #[test]
1555 fn test_wal_recovery_no_wal() {
1556 let dir = tempfile::tempdir().unwrap();
1557
1558 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1559 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1560
1561 let recovered = sm.recover().unwrap();
1563 assert_eq!(recovered, 0, "No WAL = no records recovered");
1564 }
1565
1566 #[test]
1570 fn test_wal_recovery_empty_wal() {
1571 let dir = tempfile::tempdir().unwrap();
1572
1573 let wal_path = dir.path().join("wal.log");
1575 std::fs::write(&wal_path, b"").unwrap();
1576
1577 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1578 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1579
1580 let recovered = sm.recover().unwrap();
1581 assert_eq!(recovered, 0, "Empty WAL = no records recovered");
1582 }
1583
1584 #[test]
1588 fn test_wal_load_from_disk_roundtrip() {
1589 use crate::wal::WALRecord;
1590 let dir = tempfile::tempdir().unwrap();
1591 let wal_path = dir.path().join("wal.log");
1592
1593 {
1595 let mut wal = WAL::new(wal_path.clone());
1596 wal.append(WALRecord::Insert {
1597 table_id: 42,
1598 data: vec![1, 2, 3, 4],
1599 });
1600 wal.append(WALRecord::Delete {
1601 table_id: 42,
1602 row_id: 0,
1603 });
1604 wal.append(WALRecord::Update {
1605 table_id: 42,
1606 row_id: 1,
1607 column: 2,
1608 data: vec![5, 6],
1609 });
1610 wal.append(WALRecord::ColumnWrite {
1611 table_id: 42,
1612 col_id: 0,
1613 page_id: 1,
1614 data: vec![7, 8, 9],
1615 });
1616 wal.append(WALRecord::Commit { transaction_id: 100 });
1617 wal.append(WALRecord::Rollback { transaction_id: 101 });
1618 wal.append(WALRecord::Checkpoint);
1619 wal.flush_to_disk().unwrap();
1620 }
1621
1622 {
1624 let mut wal = WAL::new(wal_path.clone());
1625 wal.load_from_disk().unwrap();
1626 assert_eq!(wal.len(), 7, "Should load 7 records from disk");
1627 assert!(wal.is_dirty());
1628
1629 match &wal.records()[0] {
1631 WALRecord::Insert { table_id, data } => {
1632 assert_eq!(*table_id, 42);
1633 assert_eq!(data, &[1, 2, 3, 4]);
1634 }
1635 _ => panic!("Expected Insert"),
1636 }
1637 match &wal.records()[1] {
1638 WALRecord::Delete { table_id, row_id } => {
1639 assert_eq!(*table_id, 42);
1640 assert_eq!(*row_id, 0);
1641 }
1642 _ => panic!("Expected Delete"),
1643 }
1644 match &wal.records()[4] {
1645 WALRecord::Commit { transaction_id } => {
1646 assert_eq!(*transaction_id, 100);
1647 }
1648 _ => panic!("Expected Commit"),
1649 }
1650 match &wal.records()[5] {
1651 WALRecord::Rollback { transaction_id } => {
1652 assert_eq!(*transaction_id, 101);
1653 }
1654 _ => panic!("Expected Rollback"),
1655 }
1656 match &wal.records()[6] {
1657 WALRecord::Checkpoint => {}
1658 _ => panic!("Expected Checkpoint"),
1659 }
1660 }
1661 }
1662
1663 #[test]
1667 fn test_commit_pipeline_local_storage_flush() {
1668 let (sm, _dir) = setup_integration();
1669
1670 let table_id;
1672 {
1673 let table = sm.table_catalog.create_node_table(
1674 "Person".into(),
1675 vec![
1676 ColumnDefinition {
1677 compression: akar_common::enums::CompressionType::Uncompressed,
1678 name: "name".into(),
1679 logical_type: LogicalTypeID::String,
1680 is_primary_key: true,
1681 },
1682 ColumnDefinition {
1683 compression: akar_common::enums::CompressionType::Uncompressed,
1684 name: "age".into(),
1685 logical_type: LogicalTypeID::Int64,
1686 is_primary_key: false,
1687 },
1688 ],
1689 );
1690 table_id = table.table_id;
1691 }
1692
1693 let mut local_storage = crate::local_storage::LocalStorage::new();
1695 {
1696 let txn_table = local_storage.get_or_create_table(table_id);
1697
1698 let mut row_bytes = Vec::new();
1700 row_bytes.push(13 );
1701 let name = "Alice";
1702 let name_bytes = name.as_bytes();
1703 row_bytes.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1704 row_bytes.extend_from_slice(name_bytes);
1705 row_bytes.push(2 );
1706 row_bytes.extend_from_slice(&30i64.to_le_bytes());
1707
1708 txn_table.insert(row_bytes);
1709 }
1710
1711 assert_eq!(local_storage.len(), 1, "Should have 1 table in local storage");
1712
1713 let shadow = crate::shadow_file::ShadowFile::new();
1715 sm.commit_transaction(
1716 &local_storage,
1717 &shadow,
1718 -1, 1, None,
1721 )
1722 .unwrap();
1723
1724 {
1726 let t = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1727 assert_eq!(t.num_rows, 1, "Should have 1 row after commit");
1728 assert_eq!(t.get_value(0, 0), Some(&Value::String("Alice".into())));
1729 assert_eq!(t.get_value(0, 1), Some(&Value::Int64(30)));
1730 }
1731
1732 {
1734 let wal = sm.wal.lock().unwrap();
1735 assert_eq!(wal.len(), 1, "WAL should have Checkpoint marker after commit");
1737 }
1738 }
1739
1740 #[test]
1744 fn test_rollback_pipeline_no_data_written() {
1745 let (sm, _dir) = setup_integration();
1746 let mut local_storage = crate::local_storage::LocalStorage::new();
1747 let mut shadow = crate::shadow_file::ShadowFile::new();
1748
1749 {
1751 let txn_table = local_storage.get_or_create_table(0);
1752 txn_table.insert(vec![2 , 42, 0, 0, 0, 0, 0, 0, 0]);
1753 }
1754
1755 assert!(!local_storage.is_empty(), "LocalStorage should have buffered data");
1756
1757 sm.rollback_transaction(&mut local_storage, &mut shadow, 1 , &[])
1759 .unwrap();
1760
1761 assert!(local_storage.is_empty(), "LocalStorage should be empty after rollback");
1763 assert!(shadow.is_empty(), "ShadowFile should be empty after rollback");
1764 }
1765
1766 #[test]
1770 fn test_commit_multiple_rows() {
1771 let (sm, _dir) = setup_integration();
1772 sm.create_node_table(
1773 "Item".into(),
1774 vec![
1775 ColumnDefinition {
1776 compression: akar_common::enums::CompressionType::Uncompressed,
1777 name: "name".into(),
1778 logical_type: LogicalTypeID::String,
1779 is_primary_key: true,
1780 },
1781 ColumnDefinition {
1782 compression: akar_common::enums::CompressionType::Uncompressed,
1783 name: "price".into(),
1784 logical_type: LogicalTypeID::Double,
1785 is_primary_key: false,
1786 },
1787 ],
1788 );
1789
1790 let mut local = crate::local_storage::LocalStorage::new();
1792 {
1793 let txn_table = local.get_or_create_table(0); let mut row = Vec::new();
1797 row.push(13);
1798 row.extend_from_slice(&6u32.to_le_bytes());
1799 row.extend_from_slice(b"Widget");
1800 row.push(11);
1801 row.extend_from_slice(&19.99f64.to_le_bytes());
1802 txn_table.insert(row);
1803
1804 let mut row = Vec::new();
1806 row.push(13);
1807 row.extend_from_slice(&6u32.to_le_bytes());
1808 row.extend_from_slice(b"Gadget");
1809 row.push(11);
1810 row.extend_from_slice(&29.99f64.to_le_bytes());
1811 txn_table.insert(row);
1812 }
1813
1814 let shadow = crate::shadow_file::ShadowFile::new();
1815 sm.commit_transaction(&local, &shadow, 0 , 2 , None)
1816 .unwrap();
1817
1818 {
1820 let t = sm.table_catalog.get_node_table_by_name("Item").unwrap();
1821 assert_eq!(t.num_rows, 2, "Should have 2 rows after commit");
1822 }
1823 }
1824 #[test]
1825 fn test_zone_map_pushdown() {
1826 use crate::column_chunk::NODE_GROUP_SIZE;
1827 use crate::table::{ColumnDefinition, NodeTable};
1828 use akar_common::types::{LogicalTypeID, Value};
1829
1830 let db_path = "test_zone_map_pushdown.db";
1831 let _ = std::fs::remove_file(db_path);
1832
1833 let mut table = NodeTable::new(
1834 0,
1835 db_path.to_string(),
1836 vec![
1837 ColumnDefinition {
1838 compression: akar_common::enums::CompressionType::Uncompressed,
1839 name: "id".into(),
1840 logical_type: LogicalTypeID::Int64,
1841 is_primary_key: true,
1842 },
1843 ColumnDefinition {
1844 compression: akar_common::enums::CompressionType::Uncompressed,
1845 name: "value".into(),
1846 logical_type: LogicalTypeID::Int64,
1847 is_primary_key: false,
1848 },
1849 ],
1850 );
1851
1852 for i in 0..NODE_GROUP_SIZE as i64 {
1854 table.insert_row(vec![Value::Int64(i), Value::Int64(i)]).unwrap();
1855 }
1856
1857 for i in 0..NODE_GROUP_SIZE as i64 {
1859 let val = i + NODE_GROUP_SIZE as i64;
1860 table.insert_row(vec![Value::Int64(val), Value::Int64(val)]).unwrap();
1861 }
1862
1863 let predicate = Some((0, ">", &Value::Int64(5000)));
1866 let data = table.to_column_major_data_with_predicate(predicate);
1867
1868 assert_eq!(
1871 data[0].len(),
1872 NODE_GROUP_SIZE,
1873 "Only the second chunk should be returned"
1874 );
1875 assert_eq!(
1876 data[0][0],
1877 Value::Int64(NODE_GROUP_SIZE as i64),
1878 "First element should be from the second chunk"
1879 );
1880
1881 let _ = std::fs::remove_file(db_path);
1882 }
1883}