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 group_commit;
17pub mod hyperloglog;
18pub mod ice_format;
19pub mod index;
20pub mod lazy_scanner;
21pub mod local_storage;
22pub mod local_wal;
23pub mod node_group;
24pub mod npy_reader;
25pub mod page;
26pub mod page_manager;
27#[cfg(feature = "parquet")]
28pub mod parquet_reader;
29#[cfg(feature = "parquet")]
30pub mod parquet_writer;
31pub mod persistence;
32pub mod predicate;
33pub mod roaring_bitmap;
34pub mod shadow_file;
35pub mod spiller;
36pub mod stats;
37pub mod string_dictionary;
38pub mod table;
39pub mod undo_buffer;
40pub mod update_info;
41pub mod vector_index;
42pub mod version_info;
43pub mod wal;
44pub mod wal_replayer;
45
46use akar_common::error::StorageError;
47use akar_common::memory::MemoryManager;
48use akar_common::types::Value;
49use akar_vector::hnsw::DistanceMetric;
50use buffer_manager::{BufferManager, BufferManagerConfig};
51use checkpoint::checkpoint;
52use std::path::PathBuf;
53use std::sync::{Arc, Mutex};
54use wal::WAL;
55
56pub use art_index::ArtPrimaryKeyIndex;
57pub use art_key::ArtKey;
58pub use column_chunk::{ColumnChunk, NODE_GROUP_SIZE};
59pub use group_commit::{GroupCommitResult, GroupCommitStats, WalLike};
60pub use index::{HashIndex, IndexKey, OnDiskHashIndex};
61pub use local_storage::LocalStorage;
62pub use local_wal::LocalWAL;
63pub use node_group::NodeGroup;
64pub use page_manager::PageManager;
65pub use persistence::TablePersistence;
66pub use shadow_file::ShadowFile;
67pub use spiller::{MultiWayStreamMerge, SpillFile, Spiller};
68pub use string_dictionary::StringDictionary;
69pub use table::{ColumnDefinition, NodeTable, RelTable, TableCatalog};
70pub use undo_buffer::UndoBuffer;
71pub use vector_index::{VectorIndexTable, extract_f64_list_from_value};
72pub use wal::WalSink;
73pub use wal_replayer::{ReplayResult, WALReplayer};
74
75pub use wal::log_delete_record;
79pub use wal::log_insert_record;
80pub use wal::log_rel_insert_record;
81pub use wal::log_update_record;
82
83pub fn serialize_values_to_bytes(values: &[Value]) -> Vec<u8> {
87 let mut out = Vec::with_capacity(values.len() * 16);
88 for v in values {
89 out.extend_from_slice(&column::Column::serialize_value(v));
90 }
91 out
92}
93
94impl From<&akar_catalog::CatalogColumn> for ColumnDefinition {
99 fn from(c: &akar_catalog::CatalogColumn) -> Self {
100 ColumnDefinition {
101 name: c.name.clone(),
102 logical_type: c.logical_type,
103 is_primary_key: c.is_primary_key,
104 compression: c.compression,
105 }
106 }
107}
108
109#[allow(dead_code)]
111pub struct StorageManager {
112 db_path: PathBuf,
113 buffer_manager: Arc<Mutex<BufferManager>>,
114 wal: Arc<Mutex<WAL>>,
115 memory_manager: Arc<MemoryManager>,
116 page_manager: Option<Arc<PageManager>>,
118 pub(crate) table_catalog: Arc<TableCatalog>,
120 table_persistence: TablePersistence,
122 spiller: std::sync::RwLock<Option<Arc<Spiller>>>,
125 group_commit: Option<Arc<group_commit::GroupCommit<Mutex<WAL>>>>,
129}
130
131#[derive(Debug, Clone)]
133pub struct StorageInfo {
134 pub db_path: String,
135 pub page_size: usize,
136 pub total_pages: u64,
137 pub free_pages: u64,
138}
139
140#[derive(Debug, Clone)]
142pub struct BufferInfo {
143 pub total_memory: usize,
144 pub used_memory: usize,
145 pub num_pinned: usize,
146}
147
148#[derive(Debug, Clone)]
150pub struct FileInfo {
151 pub total_file_size: u64,
152 pub num_data_pages: u64,
153 pub wal_size: u64,
154}
155
156#[derive(Debug, Clone)]
158pub struct FsmInfo {
159 pub total_free_pages: u64,
160 pub num_entries: usize,
161}
162
163impl StorageManager {
164 pub fn new(db_path: PathBuf, memory_manager: Arc<MemoryManager>) -> Self {
165 let _ = std::fs::create_dir_all(&db_path);
167
168 let config = BufferManagerConfig::default();
169 let bm = BufferManager::new(db_path.clone(), memory_manager.clone(), config);
170 let wal_path = if db_path.to_string_lossy() == ":memory:" {
171 let tmp = std::env::temp_dir().join("akar-wal");
173 let _ = std::fs::create_dir_all(&tmp);
174 tmp.join("wal.log")
175 } else {
176 db_path.join("wal.log")
177 };
178 let wal = WAL::new(wal_path);
182 let fsm = Arc::new(free_space_manager::FreeSpaceManager::new());
183 let existing_pages = 0u64; let pm = PageManager::new(db_path.clone(), page::DEFAULT_PAGE_SIZE, existing_pages, fsm);
185 let table_catalog = Arc::new(TableCatalog::new());
186 if db_path.to_string_lossy() != ":memory:" {
189 table_catalog.set_db_path(db_path.clone());
190 }
191 Self {
192 db_path,
193 buffer_manager: Arc::new(Mutex::new(bm)),
194 wal: Arc::new(Mutex::new(wal)),
195 memory_manager,
196 page_manager: Some(Arc::new(pm)),
197 table_catalog,
198 table_persistence: TablePersistence::new(),
199 spiller: std::sync::RwLock::new(None),
200 group_commit: None,
201 }
202 }
203
204 pub fn set_spiller(&self, spiller: Option<Arc<Spiller>>) {
209 *self.spiller.write().unwrap() = spiller.clone();
210 let table_ids: Vec<u64> = self.table_catalog.all_node_tables().iter().map(|r| *r.key()).collect();
214 for table_id in table_ids {
215 if let Some(mut table) = self.table_catalog.get_node_table_mut(table_id) {
216 table.set_spiller(spiller.clone());
217 }
218 }
219 }
220
221 pub fn spiller(&self) -> Option<Arc<Spiller>> {
223 self.spiller.read().unwrap().clone()
224 }
225
226 pub fn set_group_commit(&mut self, config: Option<group_commit::GroupCommitConfig>) {
234 self.group_commit = config.map(|cfg| Arc::new(group_commit::GroupCommit::new(self.wal.clone(), cfg)));
235 }
236
237 pub fn group_commit(&self) -> Option<Arc<group_commit::GroupCommit<Mutex<WAL>>>> {
239 self.group_commit.clone()
240 }
241
242 pub fn open(db_path: PathBuf, memory_manager: Arc<MemoryManager>) -> Self {
248 Self::new(db_path, memory_manager)
249 }
250
251 pub fn page_manager(&self) -> Option<&Arc<PageManager>> {
253 self.page_manager.as_ref()
254 }
255
256 pub fn buffer_manager(&self) -> &Arc<Mutex<BufferManager>> {
257 &self.buffer_manager
258 }
259
260 pub fn wal(&self) -> &Arc<Mutex<WAL>> {
261 &self.wal
262 }
263
264 pub fn db_path(&self) -> &PathBuf {
265 &self.db_path
266 }
267
268 pub fn table_catalog(&self) -> Arc<TableCatalog> {
270 self.table_catalog.clone()
271 }
272
273 pub fn persist_all_tables(&self) -> Result<(), StorageError> {
278 if self.db_path.to_string_lossy() == ":memory:" {
279 return Ok(()); }
281 let page_size = self.buffer_manager.lock().unwrap().page_size();
282 self.table_persistence
283 .persist_all(&self.table_catalog, &self.db_path, &self.buffer_manager, page_size)
284 }
285
286 pub fn load_persisted_tables(&self) -> Result<usize, StorageError> {
291 if self.db_path.to_string_lossy() == ":memory:" {
292 return Ok(0); }
294 let page_size = self.buffer_manager.lock().unwrap().page_size();
295 self.table_persistence
296 .load_all(&self.table_catalog, &self.db_path, &self.buffer_manager, page_size)
297 }
298
299 pub fn drop_table_persistence(&self, table_id: u64) {
301 self.table_persistence
302 .remove(table_id, &self.db_path, &self.buffer_manager);
303 }
304
305 pub fn log_column_write(&self, table_id: u64, col_id: u32, page_id: u64, data: &[u8]) {
307 let mut wal = self.wal.lock().unwrap();
308 wal.log_column_write(table_id, col_id, page_id, data);
309 }
310
311 pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
313 let table = self.table_catalog.create_node_table(name, columns);
314 self.attach_spiller(&table);
315 table
316 }
317
318 fn attach_spiller(&self, table: &NodeTable) {
322 if let Some(spiller) = self.spiller() {
323 if let Some(mut stored) = self.table_catalog.get_node_table_mut(table.table_id) {
324 stored.set_spiller(Some(spiller));
325 }
326 }
327 }
328
329 pub fn restore_node_table(
333 &self,
334 table_id: u64,
335 name: String,
336 columns: Vec<ColumnDefinition>,
337 index_name: Option<&str>,
338 ) -> NodeTable {
339 let table = self
340 .table_catalog
341 .create_node_table_with_id(table_id, name.clone(), columns);
342
343 if let Some(index_name) = index_name {
344 let mut bm = self.buffer_manager.lock().unwrap();
346 let full_path = self.db_path.join(format!("{index_name}.art"));
347 if !bm.is_file_registered(index_name) {
348 bm.register_file(index_name, full_path);
349 }
350 drop(bm);
351
352 let _ = self.table_catalog.create_art_index(&name, index_name);
353 }
354 self.attach_spiller(&table);
355 table
356 }
357
358 pub fn restore_rel_table(
361 &self,
362 table_id: u64,
363 name: String,
364 src_table_id: u64,
365 dst_table_id: u64,
366 columns: Vec<ColumnDefinition>,
367 ) -> RelTable {
368 self.table_catalog
369 .create_rel_table_with_id(table_id, name, src_table_id, dst_table_id, columns)
370 }
371
372 pub fn create_vector_index(
374 &self,
375 name: String,
376 table_name: String,
377 column_name: String,
378 metric: DistanceMetric,
379 dimensions: u32,
380 ) -> VectorIndexTable {
381 let table = self
382 .table_catalog
383 .create_vector_index(name, table_name, column_name, metric, dimensions);
384
385 let mut bm = self.buffer_manager.lock().unwrap();
387 table.register_file(&mut bm, &self.db_path);
388
389 table
390 }
391
392 pub fn restore_vector_index(
397 &self,
398 index_id: u64,
399 name: String,
400 table_name: String,
401 column_name: String,
402 metric: DistanceMetric,
403 dimensions: u32,
404 ) -> VectorIndexTable {
405 let table =
406 self.table_catalog
407 .create_vector_index_with_id(index_id, name, table_name, column_name, metric, dimensions);
408
409 let mut bm = self.buffer_manager.lock().unwrap();
411 table.register_file(&mut bm, &self.db_path);
412
413 table
414 }
415
416 pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
418 self.table_catalog.get_vector_index_by_name(name)
419 }
420
421 pub fn get_vector_index_by_name_mut(
423 &self,
424 name: &str,
425 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
426 self.table_catalog.get_vector_index_by_name_mut(name)
427 }
428
429 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
432 self.table_catalog.create_art_index(table_name, index_name)?;
433
434 let mut bm = self
436 .buffer_manager
437 .lock()
438 .map_err(|e| StorageError::BufferManager(format!("Lock poisoned: {e}")))?;
439 let full_path = self.db_path.join(format!("{index_name}.art"));
440 let file_name = index_name.to_string();
441 if !bm.is_file_registered(&file_name) {
442 bm.register_file(&file_name, full_path);
443 }
444 drop(bm);
445
446 Ok(())
447 }
448
449 pub fn drop_art_index(&self, table_name: &str, _index_name: &str) -> Result<(), StorageError> {
451 self.table_catalog.drop_art_index(table_name)
452 }
453
454 pub fn get_art_index(&self, table_name: &str) -> Option<crate::ArtPrimaryKeyIndex> {
456 self.table_catalog.get_art_index(table_name)
457 }
458
459 pub fn create_rel_table(
461 &self,
462 name: String,
463 src_table_id: u64,
464 dst_table_id: u64,
465 columns: Vec<ColumnDefinition>,
466 ) -> RelTable {
467 self.table_catalog
468 .create_rel_table(name, src_table_id, dst_table_id, columns)
469 }
470
471 pub fn wal_size(&self) -> usize {
473 self.wal.lock().unwrap().total_size()
474 }
475
476 pub fn checkpoint(&self) -> std::io::Result<checkpoint::CheckpointResult> {
478 let mut wal = self
479 .wal
480 .lock()
481 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
482 checkpoint(&mut wal, &self.buffer_manager)
483 }
484
485 pub fn maybe_checkpoint(
496 &self,
497 threshold: i64,
498 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
499 ) -> std::io::Result<bool> {
500 if threshold == 0 {
501 return Ok(false); }
503
504 let should_checkpoint = if threshold < 0 {
505 true
507 } else {
508 self.wal_size() > threshold as usize
509 };
510
511 if should_checkpoint {
512 let _ = self.checkpoint_with_drain(drain_fn)?;
513 Ok(true)
514 } else {
515 Ok(false)
516 }
517 }
518
519 pub fn checkpoint_with_drain(
532 &self,
533 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
534 ) -> std::io::Result<crate::checkpoint::CheckpointResult> {
535 if let Some(drain) = drain_fn {
537 let drained = drain(std::time::Duration::from_secs(30));
538 if !drained {
539 tracing::warn!("Checkpoint drain timed out — proceeding with best-effort checkpoint");
540 }
541 }
542
543 self.persist_all_tables()
552 .map_err(|e| std::io::Error::other(format!("Persist tables before checkpoint failed: {e}")))?;
553
554 let mut wal = self
556 .wal
557 .lock()
558 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
559 crate::checkpoint::checkpoint(&mut wal, &self.buffer_manager)
560 }
561
562 pub fn storage_info(&self) -> StorageInfo {
564 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
565 let free_pages = 0u64; StorageInfo {
567 db_path: self.db_path.to_string_lossy().to_string(),
568 page_size: self
569 .page_manager
570 .as_ref()
571 .map(|pm| pm.page_size())
572 .unwrap_or(page::DEFAULT_PAGE_SIZE),
573 total_pages,
574 free_pages,
575 }
576 }
577
578 pub fn buffer_info(&self) -> BufferInfo {
580 let bm = self.buffer_manager.lock().unwrap();
581 let stats = bm.stats();
582 let page_size = bm.page_size();
583 BufferInfo {
584 total_memory: stats.num_frames * page_size,
585 used_memory: (stats.num_frames - (stats.num_frames - stats.pinned_frames - stats.dirty_frames)) * page_size,
586 num_pinned: stats.pinned_frames,
587 }
588 }
589
590 pub fn file_info(&self) -> FileInfo {
592 let db_path = &self.db_path;
593 let wal_path = db_path.join("wal.log");
594 let wal_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
595 let data_size = std::fs::read_dir(db_path)
596 .map(|entries| {
597 entries
598 .filter_map(|e| e.ok())
599 .filter(|e| e.path().extension().map(|x| x == "data").unwrap_or(false))
600 .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
601 .sum::<u64>()
602 })
603 .unwrap_or(0);
604 let page_size = self
605 .page_manager
606 .as_ref()
607 .map(|pm| pm.page_size())
608 .unwrap_or(page::DEFAULT_PAGE_SIZE) as u64;
609 FileInfo {
610 total_file_size: data_size + wal_size,
611 num_data_pages: data_size / page_size.max(1),
612 wal_size,
613 }
614 }
615
616 pub fn fsm_info(&self) -> FsmInfo {
618 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
619 let free_pages = 0u64; FsmInfo {
621 total_free_pages: free_pages,
622 num_entries: total_pages as usize,
623 }
624 }
625
626 pub fn commit_transaction(
648 &self,
649 local_storage: &crate::local_storage::LocalStorage,
650 shadow_file: &crate::shadow_file::ShadowFile,
651 checkpoint_threshold: i64,
652 txn_id: u64,
653 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
654 ) -> Result<(), StorageError> {
655 {
663 let mut wal = self
664 .wal
665 .lock()
666 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
667 wal.append(crate::wal::WALRecord::Commit { transaction_id: txn_id });
668 if self.group_commit.is_none() {
669 wal.flush_to_disk()
670 .map_err(|e| StorageError::Wal(format!("WAL flush failed during commit: {e}")))?;
671 }
672 }
673 if let Some(gc) = &self.group_commit {
674 gc.flush()
675 .map_err(|e| StorageError::Wal(format!("Group-commit WAL flush failed during commit: {e}")))?;
676 }
677
678 let _commit_undo_records = local_storage.flush_to_tables(&self.table_catalog, Some(txn_id))?;
687 tracing::debug!(
691 "commit_transaction: generated {} undo records for txn#{}",
692 _commit_undo_records.len(),
693 txn_id
694 );
695
696 shadow_file
698 .apply(&self.buffer_manager)
699 .map_err(|e| StorageError::ShadowFile(format!("ShadowFile apply failed during commit: {e}")))?;
700
701 if let Err(e) = self.maybe_checkpoint(checkpoint_threshold, drain_fn) {
705 tracing::warn!("Checkpoint after commit failed: {e}");
706 }
708
709 Ok(())
710 }
711
712 pub fn rollback_transaction(
724 &self,
725 local_storage: &mut crate::local_storage::LocalStorage,
726 shadow_file: &mut crate::shadow_file::ShadowFile,
727 txn_id: u64,
728 undo_records: &[akar_transaction::UndoRecord],
729 ) -> Result<(), StorageError> {
730 {
732 let mut wal = self
733 .wal
734 .lock()
735 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
736 wal.append(crate::wal::WALRecord::Rollback { transaction_id: txn_id });
737 let _ = wal.flush_to_disk();
738 }
739
740 for record in undo_records.iter().rev() {
742 if let Some(mut table) = self.table_catalog.get_node_table_mut(record.table_id) {
743 match record.undo_type {
744 akar_transaction::UndoType::Update => {
745 let values = deserialize_values_from_bytes(&record.old_data, 1);
746 if let Some(val) = values.into_iter().next() {
747 table
748 .update_cell(record.row_id, record.column as usize, val)
749 .map_err(|e| {
750 StorageError::Undo(format!(
751 "Undo failed for table {} row {}: {e}",
752 record.table_id, record.row_id
753 ))
754 })?;
755 }
756 }
757 akar_transaction::UndoType::Insert => {
758 let _ = table.delete_row(record.row_id);
760 }
761 akar_transaction::UndoType::Delete => {
762 let num_cols = table.columns.len();
764 let values = deserialize_values_from_bytes(&record.old_data, num_cols);
765 for (col_idx, val) in values.into_iter().enumerate() {
766 let _ = table.update_cell(record.row_id, col_idx, val);
767 }
768 }
769 }
770 } else if let Some(mut rel) = self.table_catalog.get_rel_table_mut(record.table_id) {
771 match record.undo_type {
774 akar_transaction::UndoType::Update => {
775 let values = deserialize_values_from_bytes(&record.old_data, 1);
776 if let Some(val) = values.into_iter().next() {
777 let _ = rel.update_cell(record.row_id as usize, record.column as usize, val);
778 }
779 }
780 akar_transaction::UndoType::Insert => {
781 let _ = rel.delete_edge(record.row_id as usize);
783 }
784 akar_transaction::UndoType::Delete => {
785 let num_cols = rel.columns.len();
787 let values = deserialize_values_from_bytes(&record.old_data, num_cols + 2);
788 let mut iter = values.into_iter();
789 let src = match iter.next() {
790 Some(Value::UInt64(v)) => v,
791 Some(Value::Int64(v)) if v >= 0 => v as u64,
792 _ => u64::MAX,
793 };
794 let dst = match iter.next() {
795 Some(Value::UInt64(v)) => v,
796 Some(Value::Int64(v)) if v >= 0 => v as u64,
797 _ => u64::MAX,
798 };
799 let props: Vec<_> = iter.collect();
800 let _ = rel.restore_deleted_edge(record.row_id as usize, src, dst, props);
801 }
802 }
803 }
804 }
805
806 local_storage.clear();
808 shadow_file.discard();
809
810 Ok(())
811 }
812
813 pub fn recover(&self) -> std::io::Result<usize> {
834 match self.load_persisted_tables() {
836 Ok(n) if n > 0 => tracing::info!("Restored {n} table(s) from durable column mirrors"),
837 Ok(_) => {}
838 Err(e) => tracing::warn!("Failed to restore tables from column mirrors: {e}"),
839 }
840
841 let mut wal = self
842 .wal
843 .lock()
844 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
845
846 wal.load_from_disk()?;
848
849 if wal.is_empty() {
850 return Ok(0); }
852
853 let mut data_records = 0usize;
854 let catalog = self.table_catalog.clone();
855
856 wal.replay(|record| replay_data_record(record, &catalog, &mut data_records))?;
858
859 if data_records > 0 {
866 self.persist_all_tables()
867 .map_err(|e| std::io::Error::other(format!("WAL recovery: persist tables failed: {e}")))?;
868 }
869
870 checkpoint(&mut wal, &self.buffer_manager)?;
874
875 Ok(data_records)
876 }
877}
878
879pub(crate) fn replay_data_record(
893 record: &crate::wal::WALRecord,
894 catalog: &Arc<TableCatalog>,
895 data_records: &mut usize,
896) -> std::io::Result<()> {
897 use crate::wal::WALRecord;
898
899 let as_u64 = |v: &Value| -> Option<u64> {
900 match v {
901 Value::UInt64(x) => Some(*x),
902 Value::Int64(x) => (*x >= 0).then_some(*x as u64),
903 _ => None,
904 }
905 };
906
907 match record {
908 WALRecord::Insert { table_id, data } => {
909 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
910 let values = deserialize_values_from_bytes(data, table.columns.len());
911 if let Err(e) = table.insert_row(values) {
912 return Err(std::io::Error::other(format!("WAL recovery insert failed: {e}")));
913 }
914 *data_records += 1;
915 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
916 let values = deserialize_values_from_bytes(data, rel.columns.len() + 2);
917 if values.len() < 2 {
918 return Err(std::io::Error::other("WAL recovery: rel insert payload too short"));
919 }
920 let (Some(src), Some(dst)) = (as_u64(&values[0]), as_u64(&values[1])) else {
921 return Err(std::io::Error::other("WAL recovery: rel insert endpoints missing"));
922 };
923 if let Err(e) = rel.insert_rel(src, dst, values[2..].to_vec()) {
924 return Err(std::io::Error::other(format!("WAL recovery rel insert failed: {e}")));
925 }
926 *data_records += 1;
927 } else {
928 tracing::debug!("WAL recovery: table {table_id} not found; skipping Insert");
929 }
930 }
931 WALRecord::Delete { table_id, row_id } => {
932 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
933 if let Err(e) = table.delete_row(*row_id) {
934 return Err(std::io::Error::other(format!("WAL recovery delete failed: {e}")));
935 }
936 *data_records += 1;
937 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
938 if let Err(e) = rel.delete_edge(*row_id as usize) {
939 return Err(std::io::Error::other(format!("WAL recovery edge delete failed: {e}")));
940 }
941 *data_records += 1;
942 } else {
943 tracing::debug!("WAL recovery: table {table_id} not found; skipping Delete");
944 }
945 }
946 WALRecord::Update {
947 table_id,
948 row_id,
949 column,
950 data,
951 } => {
952 let values = deserialize_values_from_bytes(data, 1);
953 if let Some(val) = values.into_iter().next() {
954 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
955 if let Err(e) = table.update_cell(*row_id, *column as usize, val) {
956 return Err(std::io::Error::other(format!("WAL recovery update failed: {e}")));
957 }
958 *data_records += 1;
959 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
960 if let Err(e) = rel.update_cell(*row_id as usize, *column as usize, val) {
961 return Err(std::io::Error::other(format!("WAL recovery edge update failed: {e}")));
962 }
963 *data_records += 1;
964 } else {
965 tracing::debug!("WAL recovery: table {table_id} not found; skipping Update");
966 }
967 }
968 }
969 WALRecord::LocalWALData { data } => {
970 for sub in crate::wal::decode_wal_buffer(data)? {
971 replay_data_record(&sub, catalog, data_records)?;
972 }
973 }
974 WALRecord::UpdateFsm { .. } => {
975 }
978 WALRecord::ColumnWrite { .. } => {
979 }
984 WALRecord::Commit { .. } | WALRecord::Rollback { .. } => {
985 }
990 WALRecord::Checkpoint => {
991 }
995 WALRecord::CreateTable { .. }
999 | WALRecord::DropTable { .. }
1000 | WALRecord::AlterTable { .. }
1001 | WALRecord::CreateIndex { .. }
1002 | WALRecord::DropIndex { .. }
1003 | WALRecord::CreateSequence { .. } => {
1004 }
1007 }
1008 Ok(())
1009}
1010
1011pub(crate) fn deserialize_values_from_bytes(data: &[u8], expected_count: usize) -> Vec<Value> {
1012 use crate::column::Column;
1013
1014 if data.is_empty() || expected_count == 0 {
1015 return Vec::new();
1016 }
1017
1018 let mut values = Vec::with_capacity(expected_count);
1023 let mut pos = 0usize;
1024 for _ in 0..expected_count {
1025 if pos >= data.len() {
1026 values.push(Value::Null);
1027 continue;
1028 }
1029 match Column::deserialize_value(data, &mut pos) {
1030 Ok(v) => values.push(v),
1031 Err(_) => {
1032 while values.len() < expected_count {
1034 values.push(Value::Null);
1035 }
1036 break;
1037 }
1038 }
1039 }
1040
1041 values
1042}
1043
1044#[cfg(test)]
1050mod integration_tests {
1051 use super::*;
1052 use crate::column::{Column, TAG_INT64, TAG_STRING};
1053 use crate::page::DEFAULT_PAGE_SIZE;
1054 use crate::wal::WALRecord;
1055 use akar_common::enums::CompressionType;
1056 use akar_common::types::{LogicalTypeID, Value};
1057 use std::collections::HashMap;
1058
1059 fn setup_integration() -> (StorageManager, tempfile::TempDir) {
1063 let dir = tempfile::tempdir().unwrap();
1064 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
1065 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1066 (sm, dir)
1067 }
1068
1069 #[test]
1073 fn test_table_full_persistence_cycle() {
1074 let (sm, _dir) = setup_integration();
1075
1076 let mut table = sm.create_node_table(
1078 "Person".into(),
1079 vec![
1080 ColumnDefinition {
1081 compression: akar_common::enums::CompressionType::Uncompressed,
1082 name: "name".into(),
1083 logical_type: LogicalTypeID::String,
1084 is_primary_key: true,
1085 },
1086 ColumnDefinition {
1087 compression: akar_common::enums::CompressionType::Uncompressed,
1088 name: "age".into(),
1089 logical_type: LogicalTypeID::Int64,
1090 is_primary_key: false,
1091 },
1092 ],
1093 );
1094 assert_eq!(table.table_id, 0);
1095
1096 table
1098 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1099 .unwrap();
1100 table
1101 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1102 .unwrap();
1103 table
1104 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1105 .unwrap();
1106 assert_eq!(table.num_rows, 3);
1107
1108 assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1110 assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1111
1112 let names = table.scan_column(0, 0, 3, None, &HashMap::new());
1114 assert_eq!(names.len(), 3);
1115 assert_eq!(names[0], Value::String("Alice".into()));
1116
1117 let ages = table.scan_column(1, 1, 2, None, &HashMap::new());
1118 assert_eq!(ages.len(), 2);
1119 assert_eq!(ages[0], Value::Int64(25));
1120 }
1121
1122 #[test]
1126 fn test_wal_recovery_cycle() {
1127 let dir = tempfile::tempdir().unwrap();
1128 let wal_path = dir.path().join("wal.log");
1129
1130 #[allow(unused_variables)]
1132 let (wal_records_count, column_count) = {
1133 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1134 let config = BufferManagerConfig::default();
1135 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1136 let mut wal = WAL::new(wal_path.clone());
1137
1138 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1139
1140 for i in 0i64..10 {
1142 col.append_value(&Value::Int64(i)).unwrap();
1143 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1144 }
1145 wal.append(WALRecord::Commit { transaction_id: 1 });
1146 let count = wal.len();
1147
1148 wal.flush_to_disk().unwrap();
1150
1151 {
1153 let mut bm_lock = bm.lock().unwrap();
1154 bm_lock.flush_all().unwrap();
1155 }
1156
1157 for i in 0i64..10 {
1159 let v = col.get_value(i as u64).unwrap();
1160 assert_eq!(v, Value::Int64(i), "Pre-crash data mismatch at {}", i);
1161 }
1162
1163 (count, 10)
1164 }; assert!(wal_path.exists(), "WAL file should exist after flush");
1168 let file_len = std::fs::metadata(&wal_path).unwrap().len();
1169 assert!(file_len > 0, "WAL file should have content, got {} bytes", file_len);
1170
1171 assert_eq!(
1176 wal_records_count, 11,
1177 "Expected 10 ColumnWrite + 1 Commit = 11 records, got {}",
1178 wal_records_count
1179 );
1180 assert_eq!(column_count, 10);
1181 }
1182
1183 #[test]
1187 fn test_compression_full_roundtrip() {
1188 let dir = tempfile::tempdir().unwrap();
1190 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1191 let config = BufferManagerConfig::default();
1192 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1193
1194 let mut col_int = Column::with_compression(
1195 LogicalTypeID::Int64,
1196 0,
1197 0,
1198 dir.path(),
1199 bm.clone(),
1200 DEFAULT_PAGE_SIZE,
1201 CompressionType::IntegerBitpacking,
1202 );
1203
1204 let test_values: Vec<i64> = vec![0, 1, 42, 127, 255, 65535, 1_000_000, i64::MAX, i64::MIN, -1];
1206 for v in &test_values {
1207 col_int.append_value(&Value::Int64(*v)).unwrap();
1208 }
1209
1210 for (i, expected) in test_values.iter().enumerate() {
1212 let v = col_int.get_value(i as u64).unwrap();
1213 assert_eq!(v, Value::Int64(*expected), "IntegerBitpacking mismatch at index {}", i);
1214 }
1215
1216 let mut col_float = Column::with_compression(
1218 LogicalTypeID::Double,
1219 0,
1220 1,
1221 dir.path(),
1222 bm.clone(),
1223 DEFAULT_PAGE_SIZE,
1224 CompressionType::Float,
1225 );
1226
1227 let floats: Vec<f64> = vec![1.0, std::f64::consts::PI, -2.5e10, 0.0, f64::MIN_POSITIVE, f64::MAX];
1228 for v in &floats {
1229 col_float.append_value(&Value::Double(*v)).unwrap();
1230 }
1231
1232 for (i, expected) in floats.iter().enumerate() {
1233 let v = col_float.get_value(i as u64).unwrap();
1234 match v {
1235 Value::Double(d) => assert!(
1236 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1237 "Float compression mismatch at {}: got {}, expected {}",
1238 i,
1239 d,
1240 expected
1241 ),
1242 _ => panic!("Expected Double, got {:?}", v),
1243 }
1244 }
1245
1246 col_int.flush().unwrap();
1249 col_float.flush().unwrap();
1250
1251 for (i, expected) in test_values.iter().enumerate() {
1253 let v = col_int.get_value(i as u64).unwrap();
1254 assert_eq!(
1255 v,
1256 Value::Int64(*expected),
1257 "After flush: IntegerBitpacking mismatch at {}",
1258 i
1259 );
1260 }
1261 for (i, expected) in floats.iter().enumerate() {
1262 let v = col_float.get_value(i as u64).unwrap();
1263 match v {
1264 Value::Double(d) => assert!(
1265 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1266 "After flush: Float mismatch at {}",
1267 i
1268 ),
1269 _ => panic!("Expected Double after flush"),
1270 }
1271 }
1272 }
1273
1274 #[test]
1278 fn test_multi_node_group_scan() {
1279 let (_sm, _dir) = setup_integration();
1280
1281 let mut table = NodeTable::new(
1283 1,
1284 "BigTable".into(),
1285 vec![
1286 ColumnDefinition {
1287 compression: akar_common::enums::CompressionType::Uncompressed,
1288 name: "id".into(),
1289 logical_type: LogicalTypeID::Int64,
1290 is_primary_key: false,
1291 },
1292 ColumnDefinition {
1293 compression: akar_common::enums::CompressionType::Uncompressed,
1294 name: "value".into(),
1295 logical_type: LogicalTypeID::Int64,
1296 is_primary_key: false,
1297 },
1298 ],
1299 );
1300
1301 let total_rows = NODE_GROUP_SIZE + 500;
1303 for i in 0..total_rows {
1304 table
1305 .insert_row(vec![Value::Int64(i as i64), Value::Int64((i * 2) as i64)])
1306 .unwrap();
1307 }
1308
1309 assert_eq!(table.num_rows, total_rows as u64);
1311
1312 let expected_groups = 2; assert_eq!(
1315 table.node_groups.len(),
1316 expected_groups,
1317 "Expected {} node groups for {} rows",
1318 expected_groups,
1319 total_rows
1320 );
1321
1322 assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1324 assert_eq!(table.node_groups[1].num_nodes, 500);
1325 assert_eq!(table.node_groups[0].start_offset, 0);
1326 assert_eq!(table.node_groups[1].start_offset, NODE_GROUP_SIZE as u64);
1327
1328 let row_at_boundary = (NODE_GROUP_SIZE - 1) as u64;
1331 assert_eq!(
1332 table.get_value(row_at_boundary as usize, 0),
1333 Some(&Value::Int64(row_at_boundary as i64))
1334 );
1335
1336 let row_in_group1 = NODE_GROUP_SIZE as u64;
1338 assert_eq!(
1339 table.get_value(row_in_group1 as usize, 0),
1340 Some(&Value::Int64(row_in_group1 as i64))
1341 );
1342
1343 let scanned = table.scan_column(0, 0, total_rows as u64, None, &HashMap::new());
1345 assert_eq!(scanned.len(), total_rows);
1346 assert_eq!(scanned[0], Value::Int64(0));
1347 assert_eq!(scanned[NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1348 assert_eq!(scanned[total_rows - 1], Value::Int64((total_rows - 1) as i64));
1349
1350 let scan_mid = table.scan_column(1, (NODE_GROUP_SIZE - 100) as u64, 200, None, &HashMap::new());
1352 assert_eq!(scan_mid.len(), 200);
1353 assert_eq!(scan_mid[0], Value::Int64(((NODE_GROUP_SIZE - 100) * 2) as i64));
1354 assert_eq!(scan_mid[199], Value::Int64(((NODE_GROUP_SIZE + 99) * 2) as i64));
1355
1356 let data = table.to_column_major_data();
1358 assert_eq!(data.len(), 2); assert_eq!(data[0].len(), total_rows);
1360 assert_eq!(data[1].len(), total_rows);
1361 assert_eq!(data[0][NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1362 assert_eq!(data[1][0], Value::Int64(0));
1363 assert_eq!(data[1][total_rows - 1], Value::Int64(((total_rows - 1) * 2) as i64));
1364 }
1365
1366 #[test]
1370 fn test_compressed_multi_group_with_checkpoint() {
1371 let dir = tempfile::tempdir().unwrap();
1372 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
1373
1374 let config = BufferManagerConfig::default();
1376 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1377 let wal_path = dir.path().join("wal.log");
1378 let mut wal = WAL::new(wal_path);
1379
1380 let mut col = Column::with_compression(
1381 LogicalTypeID::Int64,
1382 0,
1383 0,
1384 dir.path(),
1385 bm.clone(),
1386 DEFAULT_PAGE_SIZE,
1387 CompressionType::IntegerBitpacking,
1388 );
1389
1390 let num_values = 500;
1392 for i in 0i64..num_values {
1393 col.append_value(&Value::Int64(i)).unwrap();
1394 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1395 }
1396 wal.append(WALRecord::Commit { transaction_id: 1 });
1397
1398 for i in 0i64..num_values {
1400 let v = col.get_value(i as u64).unwrap();
1401 assert_eq!(v, Value::Int64(i), "Pre-checkpoint mismatch at {}", i);
1402 }
1403
1404 let mut bm_lock = bm.lock().unwrap();
1406 bm_lock.flush_all().unwrap();
1407 drop(bm_lock);
1408
1409 wal.flush_to_disk().unwrap();
1410
1411 for i in 0i64..num_values {
1413 let v = col.get_value(i as u64).unwrap();
1414 assert_eq!(v, Value::Int64(i), "Post-checkpoint mismatch at {}", i);
1415 }
1416
1417 assert!(
1419 col.num_pages > 1,
1420 "Expected multiple pages for {} values, got {}",
1421 num_values,
1422 col.num_pages
1423 );
1424 }
1425
1426 #[test]
1430 fn test_10k_row_stress() {
1431 let dir = tempfile::tempdir().unwrap();
1432 let mm = Arc::new(MemoryManager::new(256 * 1024 * 1024));
1433 let config = BufferManagerConfig::default();
1434 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1435
1436 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1437
1438 for i in 0i64..10_000 {
1440 col.append_value(&Value::Int64(i)).unwrap();
1441 }
1442 assert_eq!(col.num_values, 10_000);
1443
1444 for i in 0i64..10_000 {
1446 let v = col.get_value(i as u64).unwrap();
1447 assert_eq!(v, Value::Int64(i), "Stress test mismatch at {}", i);
1448 }
1449
1450 col.flush().unwrap();
1452 for i in 0i64..10_000 {
1453 let v = col.get_value(i as u64).unwrap();
1454 assert_eq!(v, Value::Int64(i), "Post-flush stress mismatch at {}", i);
1455 }
1456
1457 assert!(
1459 col.num_pages > 1,
1460 "Stress test should use multiple pages, got {}",
1461 col.num_pages
1462 );
1463 }
1464
1465 #[test]
1469 fn test_wal_recovery_insert_then_recover() {
1470 let dir = tempfile::tempdir().unwrap();
1471
1472 let _row_count = {
1474 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1475 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1476
1477 let mut table = sm.create_node_table(
1479 "Person".into(),
1480 vec![
1481 ColumnDefinition {
1482 compression: akar_common::enums::CompressionType::Uncompressed,
1483 name: "name".into(),
1484 logical_type: LogicalTypeID::String,
1485 is_primary_key: true,
1486 },
1487 ColumnDefinition {
1488 compression: akar_common::enums::CompressionType::Uncompressed,
1489 name: "age".into(),
1490 logical_type: LogicalTypeID::Int64,
1491 is_primary_key: false,
1492 },
1493 ],
1494 );
1495
1496 table
1498 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1499 .unwrap();
1500 table
1501 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1502 .unwrap();
1503 table
1504 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1505 .unwrap();
1506
1507 {
1510 sm.table_catalog.create_node_table(
1513 "Person".into(),
1514 vec![
1515 ColumnDefinition {
1516 compression: akar_common::enums::CompressionType::Uncompressed,
1517 name: "name".into(),
1518 logical_type: LogicalTypeID::String,
1519 is_primary_key: true,
1520 },
1521 ColumnDefinition {
1522 compression: akar_common::enums::CompressionType::Uncompressed,
1523 name: "age".into(),
1524 logical_type: LogicalTypeID::Int64,
1525 is_primary_key: false,
1526 },
1527 ],
1528 );
1529 }
1530
1531 let count = table.num_rows;
1532 assert_eq!(count, 3);
1533
1534 {
1536 let mut wal = sm.wal.lock().unwrap();
1537 wal.append(WALRecord::Insert {
1538 table_id: table.table_id,
1539 data: vec![
1540 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,
1541 ],
1542 });
1543 wal.append(WALRecord::Insert {
1544 table_id: table.table_id,
1545 data: vec![
1546 TAG_STRING, 3, 0, 0, 0, b'B', b'o', b'b', TAG_INT64, 25, 0, 0, 0, 0, 0, 0, 0,
1547 ],
1548 });
1549 wal.append(WALRecord::Insert {
1550 table_id: table.table_id,
1551 data: vec![
1552 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,
1553 0, 0,
1554 ],
1555 });
1556 wal.flush_to_disk().unwrap();
1557 }
1558
1559 count
1560 }; {
1565 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1566 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1567
1568 let wal_path = dir.path().join("wal.log");
1570 assert!(wal_path.exists(), "WAL file should exist for recovery");
1571
1572 sm.create_node_table(
1575 "Person".into(),
1576 vec![
1577 ColumnDefinition {
1578 compression: akar_common::enums::CompressionType::Uncompressed,
1579 name: "name".into(),
1580 logical_type: LogicalTypeID::String,
1581 is_primary_key: true,
1582 },
1583 ColumnDefinition {
1584 compression: akar_common::enums::CompressionType::Uncompressed,
1585 name: "age".into(),
1586 logical_type: LogicalTypeID::Int64,
1587 is_primary_key: false,
1588 },
1589 ],
1590 );
1591
1592 let recovered = sm.recover().unwrap();
1594 assert_eq!(recovered, 3, "Should recover 3 WAL records");
1595
1596 {
1599 let recovered_table = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1600 assert_eq!(recovered_table.num_rows, 3, "Should have recovered 3 rows");
1601 assert_eq!(recovered_table.get_value(0, 0), Some(&Value::String("Alice".into())));
1602 assert_eq!(recovered_table.get_value(1, 0), Some(&Value::String("Bob".into())));
1603 assert_eq!(recovered_table.get_value(2, 0), Some(&Value::String("Charlie".into())));
1604 assert_eq!(recovered_table.get_value(0, 1), Some(&Value::Int64(30)));
1605 assert_eq!(recovered_table.get_value(1, 1), Some(&Value::Int64(25)));
1606 assert_eq!(recovered_table.get_value(2, 1), Some(&Value::Int64(35)));
1607 }
1608
1609 {
1612 let wal = sm.wal.lock().unwrap();
1613 assert_eq!(
1614 wal.len(),
1615 1,
1616 "WAL should have only the checkpoint marker after recovery"
1617 );
1618 assert!(matches!(wal.records()[0], crate::wal::WALRecord::Checkpoint));
1619 }
1620 }
1621 }
1622
1623 #[test]
1627 fn test_wal_recovery_no_wal() {
1628 let dir = tempfile::tempdir().unwrap();
1629
1630 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1631 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1632
1633 let recovered = sm.recover().unwrap();
1635 assert_eq!(recovered, 0, "No WAL = no records recovered");
1636 }
1637
1638 #[test]
1642 fn test_wal_recovery_empty_wal() {
1643 let dir = tempfile::tempdir().unwrap();
1644
1645 let wal_path = dir.path().join("wal.log");
1647 std::fs::write(&wal_path, b"").unwrap();
1648
1649 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1650 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1651
1652 let recovered = sm.recover().unwrap();
1653 assert_eq!(recovered, 0, "Empty WAL = no records recovered");
1654 }
1655
1656 #[test]
1660 fn test_wal_load_from_disk_roundtrip() {
1661 use crate::wal::WALRecord;
1662 let dir = tempfile::tempdir().unwrap();
1663 let wal_path = dir.path().join("wal.log");
1664
1665 {
1667 let mut wal = WAL::new(wal_path.clone());
1668 wal.append(WALRecord::Insert {
1669 table_id: 42,
1670 data: vec![1, 2, 3, 4],
1671 });
1672 wal.append(WALRecord::Delete {
1673 table_id: 42,
1674 row_id: 0,
1675 });
1676 wal.append(WALRecord::Update {
1677 table_id: 42,
1678 row_id: 1,
1679 column: 2,
1680 data: vec![5, 6],
1681 });
1682 wal.append(WALRecord::ColumnWrite {
1683 table_id: 42,
1684 col_id: 0,
1685 page_id: 1,
1686 data: vec![7, 8, 9],
1687 });
1688 wal.append(WALRecord::Commit { transaction_id: 100 });
1689 wal.append(WALRecord::Rollback { transaction_id: 101 });
1690 wal.append(WALRecord::Checkpoint);
1691 wal.flush_to_disk().unwrap();
1692 }
1693
1694 {
1696 let mut wal = WAL::new(wal_path.clone());
1697 wal.load_from_disk().unwrap();
1698 assert_eq!(wal.len(), 7, "Should load 7 records from disk");
1699 assert!(wal.is_dirty());
1700
1701 match &wal.records()[0] {
1703 WALRecord::Insert { table_id, data } => {
1704 assert_eq!(*table_id, 42);
1705 assert_eq!(data, &[1, 2, 3, 4]);
1706 }
1707 _ => panic!("Expected Insert"),
1708 }
1709 match &wal.records()[1] {
1710 WALRecord::Delete { table_id, row_id } => {
1711 assert_eq!(*table_id, 42);
1712 assert_eq!(*row_id, 0);
1713 }
1714 _ => panic!("Expected Delete"),
1715 }
1716 match &wal.records()[4] {
1717 WALRecord::Commit { transaction_id } => {
1718 assert_eq!(*transaction_id, 100);
1719 }
1720 _ => panic!("Expected Commit"),
1721 }
1722 match &wal.records()[5] {
1723 WALRecord::Rollback { transaction_id } => {
1724 assert_eq!(*transaction_id, 101);
1725 }
1726 _ => panic!("Expected Rollback"),
1727 }
1728 match &wal.records()[6] {
1729 WALRecord::Checkpoint => {}
1730 _ => panic!("Expected Checkpoint"),
1731 }
1732 }
1733 }
1734
1735 #[test]
1739 fn test_commit_pipeline_local_storage_flush() {
1740 let (sm, _dir) = setup_integration();
1741
1742 let table_id;
1744 {
1745 let table = sm.table_catalog.create_node_table(
1746 "Person".into(),
1747 vec![
1748 ColumnDefinition {
1749 compression: akar_common::enums::CompressionType::Uncompressed,
1750 name: "name".into(),
1751 logical_type: LogicalTypeID::String,
1752 is_primary_key: true,
1753 },
1754 ColumnDefinition {
1755 compression: akar_common::enums::CompressionType::Uncompressed,
1756 name: "age".into(),
1757 logical_type: LogicalTypeID::Int64,
1758 is_primary_key: false,
1759 },
1760 ],
1761 );
1762 table_id = table.table_id;
1763 }
1764
1765 let mut local_storage = crate::local_storage::LocalStorage::new();
1767 {
1768 let txn_table = local_storage.get_or_create_table(table_id);
1769
1770 let mut row_bytes = Vec::new();
1772 row_bytes.push(13 );
1773 let name = "Alice";
1774 let name_bytes = name.as_bytes();
1775 row_bytes.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1776 row_bytes.extend_from_slice(name_bytes);
1777 row_bytes.push(2 );
1778 row_bytes.extend_from_slice(&30i64.to_le_bytes());
1779
1780 txn_table.insert(row_bytes);
1781 }
1782
1783 assert_eq!(local_storage.len(), 1, "Should have 1 table in local storage");
1784
1785 let shadow = crate::shadow_file::ShadowFile::new();
1787 sm.commit_transaction(
1788 &local_storage,
1789 &shadow,
1790 -1, 1, None,
1793 )
1794 .unwrap();
1795
1796 {
1798 let t = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1799 assert_eq!(t.num_rows, 1, "Should have 1 row after commit");
1800 assert_eq!(t.get_value(0, 0), Some(&Value::String("Alice".into())));
1801 assert_eq!(t.get_value(0, 1), Some(&Value::Int64(30)));
1802 }
1803
1804 {
1806 let wal = sm.wal.lock().unwrap();
1807 assert_eq!(wal.len(), 1, "WAL should have Checkpoint marker after commit");
1809 }
1810 }
1811
1812 #[test]
1816 fn test_rollback_pipeline_no_data_written() {
1817 let (sm, _dir) = setup_integration();
1818 let mut local_storage = crate::local_storage::LocalStorage::new();
1819 let mut shadow = crate::shadow_file::ShadowFile::new();
1820
1821 {
1823 let txn_table = local_storage.get_or_create_table(0);
1824 txn_table.insert(vec![2 , 42, 0, 0, 0, 0, 0, 0, 0]);
1825 }
1826
1827 assert!(!local_storage.is_empty(), "LocalStorage should have buffered data");
1828
1829 sm.rollback_transaction(&mut local_storage, &mut shadow, 1 , &[])
1831 .unwrap();
1832
1833 assert!(local_storage.is_empty(), "LocalStorage should be empty after rollback");
1835 assert!(shadow.is_empty(), "ShadowFile should be empty after rollback");
1836 }
1837
1838 #[test]
1842 fn test_commit_multiple_rows() {
1843 let (sm, _dir) = setup_integration();
1844 sm.create_node_table(
1845 "Item".into(),
1846 vec![
1847 ColumnDefinition {
1848 compression: akar_common::enums::CompressionType::Uncompressed,
1849 name: "name".into(),
1850 logical_type: LogicalTypeID::String,
1851 is_primary_key: true,
1852 },
1853 ColumnDefinition {
1854 compression: akar_common::enums::CompressionType::Uncompressed,
1855 name: "price".into(),
1856 logical_type: LogicalTypeID::Double,
1857 is_primary_key: false,
1858 },
1859 ],
1860 );
1861
1862 let mut local = crate::local_storage::LocalStorage::new();
1864 {
1865 let txn_table = local.get_or_create_table(0); let mut row = Vec::new();
1869 row.push(13);
1870 row.extend_from_slice(&6u32.to_le_bytes());
1871 row.extend_from_slice(b"Widget");
1872 row.push(11);
1873 row.extend_from_slice(&19.99f64.to_le_bytes());
1874 txn_table.insert(row);
1875
1876 let mut row = Vec::new();
1878 row.push(13);
1879 row.extend_from_slice(&6u32.to_le_bytes());
1880 row.extend_from_slice(b"Gadget");
1881 row.push(11);
1882 row.extend_from_slice(&29.99f64.to_le_bytes());
1883 txn_table.insert(row);
1884 }
1885
1886 let shadow = crate::shadow_file::ShadowFile::new();
1887 sm.commit_transaction(&local, &shadow, 0 , 2 , None)
1888 .unwrap();
1889
1890 {
1892 let t = sm.table_catalog.get_node_table_by_name("Item").unwrap();
1893 assert_eq!(t.num_rows, 2, "Should have 2 rows after commit");
1894 }
1895 }
1896 #[test]
1897 fn test_zone_map_pushdown() {
1898 use crate::column_chunk::NODE_GROUP_SIZE;
1899 use crate::table::{ColumnDefinition, NodeTable};
1900 use akar_common::types::{LogicalTypeID, Value};
1901
1902 let db_path = "test_zone_map_pushdown.db";
1903 let _ = std::fs::remove_file(db_path);
1904
1905 let mut table = NodeTable::new(
1906 0,
1907 db_path.to_string(),
1908 vec![
1909 ColumnDefinition {
1910 compression: akar_common::enums::CompressionType::Uncompressed,
1911 name: "id".into(),
1912 logical_type: LogicalTypeID::Int64,
1913 is_primary_key: true,
1914 },
1915 ColumnDefinition {
1916 compression: akar_common::enums::CompressionType::Uncompressed,
1917 name: "value".into(),
1918 logical_type: LogicalTypeID::Int64,
1919 is_primary_key: false,
1920 },
1921 ],
1922 );
1923
1924 for i in 0..NODE_GROUP_SIZE as i64 {
1926 table.insert_row(vec![Value::Int64(i), Value::Int64(i)]).unwrap();
1927 }
1928
1929 for i in 0..NODE_GROUP_SIZE as i64 {
1931 let val = i + NODE_GROUP_SIZE as i64;
1932 table.insert_row(vec![Value::Int64(val), Value::Int64(val)]).unwrap();
1933 }
1934
1935 let predicate = Some((0, ">", &Value::Int64(5000)));
1938 let data = table.to_column_major_data_with_predicate(predicate);
1939
1940 assert_eq!(
1943 data[0].len(),
1944 NODE_GROUP_SIZE,
1945 "Only the second chunk should be returned"
1946 );
1947 assert_eq!(
1948 data[0][0],
1949 Value::Int64(NODE_GROUP_SIZE as i64),
1950 "First element should be from the second chunk"
1951 );
1952
1953 let _ = std::fs::remove_file(db_path);
1954 }
1955}