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 get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
394 self.table_catalog.get_vector_index_by_name(name)
395 }
396
397 pub fn get_vector_index_by_name_mut(
399 &self,
400 name: &str,
401 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
402 self.table_catalog.get_vector_index_by_name_mut(name)
403 }
404
405 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
408 self.table_catalog.create_art_index(table_name, index_name)?;
409
410 let mut bm = self
412 .buffer_manager
413 .lock()
414 .map_err(|e| StorageError::BufferManager(format!("Lock poisoned: {e}")))?;
415 let full_path = self.db_path.join(format!("{index_name}.art"));
416 let file_name = index_name.to_string();
417 if !bm.is_file_registered(&file_name) {
418 bm.register_file(&file_name, full_path);
419 }
420 drop(bm);
421
422 Ok(())
423 }
424
425 pub fn drop_art_index(&self, table_name: &str, _index_name: &str) -> Result<(), StorageError> {
427 self.table_catalog.drop_art_index(table_name)
428 }
429
430 pub fn get_art_index(&self, table_name: &str) -> Option<crate::ArtPrimaryKeyIndex> {
432 self.table_catalog.get_art_index(table_name)
433 }
434
435 pub fn create_rel_table(
437 &self,
438 name: String,
439 src_table_id: u64,
440 dst_table_id: u64,
441 columns: Vec<ColumnDefinition>,
442 ) -> RelTable {
443 self.table_catalog
444 .create_rel_table(name, src_table_id, dst_table_id, columns)
445 }
446
447 pub fn wal_size(&self) -> usize {
449 self.wal.lock().unwrap().total_size()
450 }
451
452 pub fn checkpoint(&self) -> std::io::Result<checkpoint::CheckpointResult> {
454 let mut wal = self
455 .wal
456 .lock()
457 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
458 checkpoint(&mut wal, &self.buffer_manager)
459 }
460
461 pub fn maybe_checkpoint(
472 &self,
473 threshold: i64,
474 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
475 ) -> std::io::Result<bool> {
476 if threshold == 0 {
477 return Ok(false); }
479
480 let should_checkpoint = if threshold < 0 {
481 true
483 } else {
484 self.wal_size() > threshold as usize
485 };
486
487 if should_checkpoint {
488 let _ = self.checkpoint_with_drain(drain_fn)?;
489 Ok(true)
490 } else {
491 Ok(false)
492 }
493 }
494
495 pub fn checkpoint_with_drain(
508 &self,
509 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
510 ) -> std::io::Result<crate::checkpoint::CheckpointResult> {
511 if let Some(drain) = drain_fn {
513 let drained = drain(std::time::Duration::from_secs(30));
514 if !drained {
515 tracing::warn!("Checkpoint drain timed out — proceeding with best-effort checkpoint");
516 }
517 }
518
519 self.persist_all_tables()
528 .map_err(|e| std::io::Error::other(format!("Persist tables before checkpoint failed: {e}")))?;
529
530 let mut wal = self
532 .wal
533 .lock()
534 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
535 crate::checkpoint::checkpoint(&mut wal, &self.buffer_manager)
536 }
537
538 pub fn storage_info(&self) -> StorageInfo {
540 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
541 let free_pages = 0u64; StorageInfo {
543 db_path: self.db_path.to_string_lossy().to_string(),
544 page_size: self
545 .page_manager
546 .as_ref()
547 .map(|pm| pm.page_size())
548 .unwrap_or(page::DEFAULT_PAGE_SIZE),
549 total_pages,
550 free_pages,
551 }
552 }
553
554 pub fn buffer_info(&self) -> BufferInfo {
556 let bm = self.buffer_manager.lock().unwrap();
557 let stats = bm.stats();
558 let page_size = bm.page_size();
559 BufferInfo {
560 total_memory: stats.num_frames * page_size,
561 used_memory: (stats.num_frames - (stats.num_frames - stats.pinned_frames - stats.dirty_frames)) * page_size,
562 num_pinned: stats.pinned_frames,
563 }
564 }
565
566 pub fn file_info(&self) -> FileInfo {
568 let db_path = &self.db_path;
569 let wal_path = db_path.join("wal.log");
570 let wal_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
571 let data_size = std::fs::read_dir(db_path)
572 .map(|entries| {
573 entries
574 .filter_map(|e| e.ok())
575 .filter(|e| e.path().extension().map(|x| x == "data").unwrap_or(false))
576 .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
577 .sum::<u64>()
578 })
579 .unwrap_or(0);
580 let page_size = self
581 .page_manager
582 .as_ref()
583 .map(|pm| pm.page_size())
584 .unwrap_or(page::DEFAULT_PAGE_SIZE) as u64;
585 FileInfo {
586 total_file_size: data_size + wal_size,
587 num_data_pages: data_size / page_size.max(1),
588 wal_size,
589 }
590 }
591
592 pub fn fsm_info(&self) -> FsmInfo {
594 let total_pages = self.page_manager.as_ref().map(|pm| pm.total_pages()).unwrap_or(0);
595 let free_pages = 0u64; FsmInfo {
597 total_free_pages: free_pages,
598 num_entries: total_pages as usize,
599 }
600 }
601
602 pub fn commit_transaction(
624 &self,
625 local_storage: &crate::local_storage::LocalStorage,
626 shadow_file: &crate::shadow_file::ShadowFile,
627 checkpoint_threshold: i64,
628 txn_id: u64,
629 drain_fn: Option<&dyn Fn(std::time::Duration) -> bool>,
630 ) -> Result<(), StorageError> {
631 {
639 let mut wal = self
640 .wal
641 .lock()
642 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
643 wal.append(crate::wal::WALRecord::Commit { transaction_id: txn_id });
644 if self.group_commit.is_none() {
645 wal.flush_to_disk()
646 .map_err(|e| StorageError::Wal(format!("WAL flush failed during commit: {e}")))?;
647 }
648 }
649 if let Some(gc) = &self.group_commit {
650 gc.flush()
651 .map_err(|e| StorageError::Wal(format!("Group-commit WAL flush failed during commit: {e}")))?;
652 }
653
654 let _commit_undo_records = local_storage.flush_to_tables(&self.table_catalog, Some(txn_id))?;
663 tracing::debug!(
667 "commit_transaction: generated {} undo records for txn#{}",
668 _commit_undo_records.len(),
669 txn_id
670 );
671
672 shadow_file
674 .apply(&self.buffer_manager)
675 .map_err(|e| StorageError::ShadowFile(format!("ShadowFile apply failed during commit: {e}")))?;
676
677 if let Err(e) = self.maybe_checkpoint(checkpoint_threshold, drain_fn) {
681 tracing::warn!("Checkpoint after commit failed: {e}");
682 }
684
685 Ok(())
686 }
687
688 pub fn rollback_transaction(
700 &self,
701 local_storage: &mut crate::local_storage::LocalStorage,
702 shadow_file: &mut crate::shadow_file::ShadowFile,
703 txn_id: u64,
704 undo_records: &[akar_transaction::UndoRecord],
705 ) -> Result<(), StorageError> {
706 {
708 let mut wal = self
709 .wal
710 .lock()
711 .map_err(|e| StorageError::Wal(format!("Lock poisoned: {e}")))?;
712 wal.append(crate::wal::WALRecord::Rollback { transaction_id: txn_id });
713 let _ = wal.flush_to_disk();
714 }
715
716 for record in undo_records.iter().rev() {
718 if let Some(mut table) = self.table_catalog.get_node_table_mut(record.table_id) {
719 match record.undo_type {
720 akar_transaction::UndoType::Update => {
721 let values = deserialize_values_from_bytes(&record.old_data, 1);
722 if let Some(val) = values.into_iter().next() {
723 table
724 .update_cell(record.row_id, record.column as usize, val)
725 .map_err(|e| {
726 StorageError::Undo(format!(
727 "Undo failed for table {} row {}: {e}",
728 record.table_id, record.row_id
729 ))
730 })?;
731 }
732 }
733 akar_transaction::UndoType::Insert => {
734 let _ = table.delete_row(record.row_id);
736 }
737 akar_transaction::UndoType::Delete => {
738 let num_cols = table.columns.len();
740 let values = deserialize_values_from_bytes(&record.old_data, num_cols);
741 for (col_idx, val) in values.into_iter().enumerate() {
742 let _ = table.update_cell(record.row_id, col_idx, val);
743 }
744 }
745 }
746 } else if let Some(mut rel) = self.table_catalog.get_rel_table_mut(record.table_id) {
747 match record.undo_type {
750 akar_transaction::UndoType::Update => {
751 let values = deserialize_values_from_bytes(&record.old_data, 1);
752 if let Some(val) = values.into_iter().next() {
753 let _ = rel.update_cell(record.row_id as usize, record.column as usize, val);
754 }
755 }
756 akar_transaction::UndoType::Insert => {
757 let _ = rel.delete_edge(record.row_id as usize);
759 }
760 akar_transaction::UndoType::Delete => {
761 let num_cols = rel.columns.len();
763 let values = deserialize_values_from_bytes(&record.old_data, num_cols + 2);
764 let mut iter = values.into_iter();
765 let src = match iter.next() {
766 Some(Value::UInt64(v)) => v,
767 Some(Value::Int64(v)) if v >= 0 => v as u64,
768 _ => u64::MAX,
769 };
770 let dst = match iter.next() {
771 Some(Value::UInt64(v)) => v,
772 Some(Value::Int64(v)) if v >= 0 => v as u64,
773 _ => u64::MAX,
774 };
775 let props: Vec<_> = iter.collect();
776 let _ = rel.restore_deleted_edge(record.row_id as usize, src, dst, props);
777 }
778 }
779 }
780 }
781
782 local_storage.clear();
784 shadow_file.discard();
785
786 Ok(())
787 }
788
789 pub fn recover(&self) -> std::io::Result<usize> {
810 match self.load_persisted_tables() {
812 Ok(n) if n > 0 => tracing::info!("Restored {n} table(s) from durable column mirrors"),
813 Ok(_) => {}
814 Err(e) => tracing::warn!("Failed to restore tables from column mirrors: {e}"),
815 }
816
817 let mut wal = self
818 .wal
819 .lock()
820 .map_err(|e| std::io::Error::other(format!("Lock poisoned: {e}")))?;
821
822 wal.load_from_disk()?;
824
825 if wal.is_empty() {
826 return Ok(0); }
828
829 let mut data_records = 0usize;
830 let catalog = self.table_catalog.clone();
831
832 wal.replay(|record| replay_data_record(record, &catalog, &mut data_records))?;
834
835 if data_records > 0 {
842 self.persist_all_tables()
843 .map_err(|e| std::io::Error::other(format!("WAL recovery: persist tables failed: {e}")))?;
844 }
845
846 checkpoint(&mut wal, &self.buffer_manager)?;
850
851 Ok(data_records)
852 }
853}
854
855pub(crate) fn replay_data_record(
869 record: &crate::wal::WALRecord,
870 catalog: &Arc<TableCatalog>,
871 data_records: &mut usize,
872) -> std::io::Result<()> {
873 use crate::wal::WALRecord;
874
875 let as_u64 = |v: &Value| -> Option<u64> {
876 match v {
877 Value::UInt64(x) => Some(*x),
878 Value::Int64(x) => (*x >= 0).then_some(*x as u64),
879 _ => None,
880 }
881 };
882
883 match record {
884 WALRecord::Insert { table_id, data } => {
885 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
886 let values = deserialize_values_from_bytes(data, table.columns.len());
887 if let Err(e) = table.insert_row(values) {
888 return Err(std::io::Error::other(format!("WAL recovery insert failed: {e}")));
889 }
890 *data_records += 1;
891 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
892 let values = deserialize_values_from_bytes(data, rel.columns.len() + 2);
893 if values.len() < 2 {
894 return Err(std::io::Error::other("WAL recovery: rel insert payload too short"));
895 }
896 let (Some(src), Some(dst)) = (as_u64(&values[0]), as_u64(&values[1])) else {
897 return Err(std::io::Error::other("WAL recovery: rel insert endpoints missing"));
898 };
899 if let Err(e) = rel.insert_rel(src, dst, values[2..].to_vec()) {
900 return Err(std::io::Error::other(format!("WAL recovery rel insert failed: {e}")));
901 }
902 *data_records += 1;
903 } else {
904 tracing::debug!("WAL recovery: table {table_id} not found; skipping Insert");
905 }
906 }
907 WALRecord::Delete { table_id, row_id } => {
908 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
909 if let Err(e) = table.delete_row(*row_id) {
910 return Err(std::io::Error::other(format!("WAL recovery delete failed: {e}")));
911 }
912 *data_records += 1;
913 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
914 if let Err(e) = rel.delete_edge(*row_id as usize) {
915 return Err(std::io::Error::other(format!("WAL recovery edge delete failed: {e}")));
916 }
917 *data_records += 1;
918 } else {
919 tracing::debug!("WAL recovery: table {table_id} not found; skipping Delete");
920 }
921 }
922 WALRecord::Update {
923 table_id,
924 row_id,
925 column,
926 data,
927 } => {
928 let values = deserialize_values_from_bytes(data, 1);
929 if let Some(val) = values.into_iter().next() {
930 if let Some(mut table) = catalog.get_node_table_mut(*table_id) {
931 if let Err(e) = table.update_cell(*row_id, *column as usize, val) {
932 return Err(std::io::Error::other(format!("WAL recovery update failed: {e}")));
933 }
934 *data_records += 1;
935 } else if let Some(mut rel) = catalog.get_rel_table_mut(*table_id) {
936 if let Err(e) = rel.update_cell(*row_id as usize, *column as usize, val) {
937 return Err(std::io::Error::other(format!("WAL recovery edge update failed: {e}")));
938 }
939 *data_records += 1;
940 } else {
941 tracing::debug!("WAL recovery: table {table_id} not found; skipping Update");
942 }
943 }
944 }
945 WALRecord::LocalWALData { data } => {
946 for sub in crate::wal::decode_wal_buffer(data)? {
947 replay_data_record(&sub, catalog, data_records)?;
948 }
949 }
950 WALRecord::UpdateFsm { .. } => {
951 }
954 WALRecord::ColumnWrite { .. } => {
955 }
960 WALRecord::Commit { .. } | WALRecord::Rollback { .. } => {
961 }
966 WALRecord::Checkpoint => {
967 }
971 WALRecord::CreateTable { .. }
975 | WALRecord::DropTable { .. }
976 | WALRecord::AlterTable { .. }
977 | WALRecord::CreateIndex { .. }
978 | WALRecord::DropIndex { .. }
979 | WALRecord::CreateSequence { .. } => {
980 }
983 }
984 Ok(())
985}
986
987pub(crate) fn deserialize_values_from_bytes(data: &[u8], expected_count: usize) -> Vec<Value> {
988 use crate::column::Column;
989
990 if data.is_empty() || expected_count == 0 {
991 return Vec::new();
992 }
993
994 let mut values = Vec::with_capacity(expected_count);
999 let mut pos = 0usize;
1000 for _ in 0..expected_count {
1001 if pos >= data.len() {
1002 values.push(Value::Null);
1003 continue;
1004 }
1005 match Column::deserialize_value(data, &mut pos) {
1006 Ok(v) => values.push(v),
1007 Err(_) => {
1008 while values.len() < expected_count {
1010 values.push(Value::Null);
1011 }
1012 break;
1013 }
1014 }
1015 }
1016
1017 values
1018}
1019
1020#[cfg(test)]
1026mod integration_tests {
1027 use super::*;
1028 use crate::column::{Column, TAG_INT64, TAG_STRING};
1029 use crate::page::DEFAULT_PAGE_SIZE;
1030 use crate::wal::WALRecord;
1031 use akar_common::enums::CompressionType;
1032 use akar_common::types::{LogicalTypeID, Value};
1033 use std::collections::HashMap;
1034
1035 fn setup_integration() -> (StorageManager, tempfile::TempDir) {
1039 let dir = tempfile::tempdir().unwrap();
1040 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
1041 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1042 (sm, dir)
1043 }
1044
1045 #[test]
1049 fn test_table_full_persistence_cycle() {
1050 let (sm, _dir) = setup_integration();
1051
1052 let mut table = sm.create_node_table(
1054 "Person".into(),
1055 vec![
1056 ColumnDefinition {
1057 compression: akar_common::enums::CompressionType::Uncompressed,
1058 name: "name".into(),
1059 logical_type: LogicalTypeID::String,
1060 is_primary_key: true,
1061 },
1062 ColumnDefinition {
1063 compression: akar_common::enums::CompressionType::Uncompressed,
1064 name: "age".into(),
1065 logical_type: LogicalTypeID::Int64,
1066 is_primary_key: false,
1067 },
1068 ],
1069 );
1070 assert_eq!(table.table_id, 0);
1071
1072 table
1074 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1075 .unwrap();
1076 table
1077 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1078 .unwrap();
1079 table
1080 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1081 .unwrap();
1082 assert_eq!(table.num_rows, 3);
1083
1084 assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1086 assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1087
1088 let names = table.scan_column(0, 0, 3, None, &HashMap::new());
1090 assert_eq!(names.len(), 3);
1091 assert_eq!(names[0], Value::String("Alice".into()));
1092
1093 let ages = table.scan_column(1, 1, 2, None, &HashMap::new());
1094 assert_eq!(ages.len(), 2);
1095 assert_eq!(ages[0], Value::Int64(25));
1096 }
1097
1098 #[test]
1102 fn test_wal_recovery_cycle() {
1103 let dir = tempfile::tempdir().unwrap();
1104 let wal_path = dir.path().join("wal.log");
1105
1106 #[allow(unused_variables)]
1108 let (wal_records_count, column_count) = {
1109 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1110 let config = BufferManagerConfig::default();
1111 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1112 let mut wal = WAL::new(wal_path.clone());
1113
1114 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1115
1116 for i in 0i64..10 {
1118 col.append_value(&Value::Int64(i)).unwrap();
1119 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1120 }
1121 wal.append(WALRecord::Commit { transaction_id: 1 });
1122 let count = wal.len();
1123
1124 wal.flush_to_disk().unwrap();
1126
1127 {
1129 let mut bm_lock = bm.lock().unwrap();
1130 bm_lock.flush_all().unwrap();
1131 }
1132
1133 for i in 0i64..10 {
1135 let v = col.get_value(i as u64).unwrap();
1136 assert_eq!(v, Value::Int64(i), "Pre-crash data mismatch at {}", i);
1137 }
1138
1139 (count, 10)
1140 }; assert!(wal_path.exists(), "WAL file should exist after flush");
1144 let file_len = std::fs::metadata(&wal_path).unwrap().len();
1145 assert!(file_len > 0, "WAL file should have content, got {} bytes", file_len);
1146
1147 assert_eq!(
1152 wal_records_count, 11,
1153 "Expected 10 ColumnWrite + 1 Commit = 11 records, got {}",
1154 wal_records_count
1155 );
1156 assert_eq!(column_count, 10);
1157 }
1158
1159 #[test]
1163 fn test_compression_full_roundtrip() {
1164 let dir = tempfile::tempdir().unwrap();
1166 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1167 let config = BufferManagerConfig::default();
1168 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1169
1170 let mut col_int = Column::with_compression(
1171 LogicalTypeID::Int64,
1172 0,
1173 0,
1174 dir.path(),
1175 bm.clone(),
1176 DEFAULT_PAGE_SIZE,
1177 CompressionType::IntegerBitpacking,
1178 );
1179
1180 let test_values: Vec<i64> = vec![0, 1, 42, 127, 255, 65535, 1_000_000, i64::MAX, i64::MIN, -1];
1182 for v in &test_values {
1183 col_int.append_value(&Value::Int64(*v)).unwrap();
1184 }
1185
1186 for (i, expected) in test_values.iter().enumerate() {
1188 let v = col_int.get_value(i as u64).unwrap();
1189 assert_eq!(v, Value::Int64(*expected), "IntegerBitpacking mismatch at index {}", i);
1190 }
1191
1192 let mut col_float = Column::with_compression(
1194 LogicalTypeID::Double,
1195 0,
1196 1,
1197 dir.path(),
1198 bm.clone(),
1199 DEFAULT_PAGE_SIZE,
1200 CompressionType::Float,
1201 );
1202
1203 let floats: Vec<f64> = vec![1.0, std::f64::consts::PI, -2.5e10, 0.0, f64::MIN_POSITIVE, f64::MAX];
1204 for v in &floats {
1205 col_float.append_value(&Value::Double(*v)).unwrap();
1206 }
1207
1208 for (i, expected) in floats.iter().enumerate() {
1209 let v = col_float.get_value(i as u64).unwrap();
1210 match v {
1211 Value::Double(d) => assert!(
1212 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1213 "Float compression mismatch at {}: got {}, expected {}",
1214 i,
1215 d,
1216 expected
1217 ),
1218 _ => panic!("Expected Double, got {:?}", v),
1219 }
1220 }
1221
1222 col_int.flush().unwrap();
1225 col_float.flush().unwrap();
1226
1227 for (i, expected) in test_values.iter().enumerate() {
1229 let v = col_int.get_value(i as u64).unwrap();
1230 assert_eq!(
1231 v,
1232 Value::Int64(*expected),
1233 "After flush: IntegerBitpacking mismatch at {}",
1234 i
1235 );
1236 }
1237 for (i, expected) in floats.iter().enumerate() {
1238 let v = col_float.get_value(i as u64).unwrap();
1239 match v {
1240 Value::Double(d) => assert!(
1241 (d - expected).abs() < 1e-10 || (d / expected - 1.0).abs() < 1e-10,
1242 "After flush: Float mismatch at {}",
1243 i
1244 ),
1245 _ => panic!("Expected Double after flush"),
1246 }
1247 }
1248 }
1249
1250 #[test]
1254 fn test_multi_node_group_scan() {
1255 let (_sm, _dir) = setup_integration();
1256
1257 let mut table = NodeTable::new(
1259 1,
1260 "BigTable".into(),
1261 vec![
1262 ColumnDefinition {
1263 compression: akar_common::enums::CompressionType::Uncompressed,
1264 name: "id".into(),
1265 logical_type: LogicalTypeID::Int64,
1266 is_primary_key: false,
1267 },
1268 ColumnDefinition {
1269 compression: akar_common::enums::CompressionType::Uncompressed,
1270 name: "value".into(),
1271 logical_type: LogicalTypeID::Int64,
1272 is_primary_key: false,
1273 },
1274 ],
1275 );
1276
1277 let total_rows = NODE_GROUP_SIZE + 500;
1279 for i in 0..total_rows {
1280 table
1281 .insert_row(vec![Value::Int64(i as i64), Value::Int64((i * 2) as i64)])
1282 .unwrap();
1283 }
1284
1285 assert_eq!(table.num_rows, total_rows as u64);
1287
1288 let expected_groups = 2; assert_eq!(
1291 table.node_groups.len(),
1292 expected_groups,
1293 "Expected {} node groups for {} rows",
1294 expected_groups,
1295 total_rows
1296 );
1297
1298 assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1300 assert_eq!(table.node_groups[1].num_nodes, 500);
1301 assert_eq!(table.node_groups[0].start_offset, 0);
1302 assert_eq!(table.node_groups[1].start_offset, NODE_GROUP_SIZE as u64);
1303
1304 let row_at_boundary = (NODE_GROUP_SIZE - 1) as u64;
1307 assert_eq!(
1308 table.get_value(row_at_boundary as usize, 0),
1309 Some(&Value::Int64(row_at_boundary as i64))
1310 );
1311
1312 let row_in_group1 = NODE_GROUP_SIZE as u64;
1314 assert_eq!(
1315 table.get_value(row_in_group1 as usize, 0),
1316 Some(&Value::Int64(row_in_group1 as i64))
1317 );
1318
1319 let scanned = table.scan_column(0, 0, total_rows as u64, None, &HashMap::new());
1321 assert_eq!(scanned.len(), total_rows);
1322 assert_eq!(scanned[0], Value::Int64(0));
1323 assert_eq!(scanned[NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1324 assert_eq!(scanned[total_rows - 1], Value::Int64((total_rows - 1) as i64));
1325
1326 let scan_mid = table.scan_column(1, (NODE_GROUP_SIZE - 100) as u64, 200, None, &HashMap::new());
1328 assert_eq!(scan_mid.len(), 200);
1329 assert_eq!(scan_mid[0], Value::Int64(((NODE_GROUP_SIZE - 100) * 2) as i64));
1330 assert_eq!(scan_mid[199], Value::Int64(((NODE_GROUP_SIZE + 99) * 2) as i64));
1331
1332 let data = table.to_column_major_data();
1334 assert_eq!(data.len(), 2); assert_eq!(data[0].len(), total_rows);
1336 assert_eq!(data[1].len(), total_rows);
1337 assert_eq!(data[0][NODE_GROUP_SIZE], Value::Int64(NODE_GROUP_SIZE as i64));
1338 assert_eq!(data[1][0], Value::Int64(0));
1339 assert_eq!(data[1][total_rows - 1], Value::Int64(((total_rows - 1) * 2) as i64));
1340 }
1341
1342 #[test]
1346 fn test_compressed_multi_group_with_checkpoint() {
1347 let dir = tempfile::tempdir().unwrap();
1348 let mm = Arc::new(MemoryManager::new(128 * 1024 * 1024));
1349
1350 let config = BufferManagerConfig::default();
1352 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1353 let wal_path = dir.path().join("wal.log");
1354 let mut wal = WAL::new(wal_path);
1355
1356 let mut col = Column::with_compression(
1357 LogicalTypeID::Int64,
1358 0,
1359 0,
1360 dir.path(),
1361 bm.clone(),
1362 DEFAULT_PAGE_SIZE,
1363 CompressionType::IntegerBitpacking,
1364 );
1365
1366 let num_values = 500;
1368 for i in 0i64..num_values {
1369 col.append_value(&Value::Int64(i)).unwrap();
1370 wal.log_column_write(0, 0, 0, &i.to_le_bytes());
1371 }
1372 wal.append(WALRecord::Commit { transaction_id: 1 });
1373
1374 for i in 0i64..num_values {
1376 let v = col.get_value(i as u64).unwrap();
1377 assert_eq!(v, Value::Int64(i), "Pre-checkpoint mismatch at {}", i);
1378 }
1379
1380 let mut bm_lock = bm.lock().unwrap();
1382 bm_lock.flush_all().unwrap();
1383 drop(bm_lock);
1384
1385 wal.flush_to_disk().unwrap();
1386
1387 for i in 0i64..num_values {
1389 let v = col.get_value(i as u64).unwrap();
1390 assert_eq!(v, Value::Int64(i), "Post-checkpoint mismatch at {}", i);
1391 }
1392
1393 assert!(
1395 col.num_pages > 1,
1396 "Expected multiple pages for {} values, got {}",
1397 num_values,
1398 col.num_pages
1399 );
1400 }
1401
1402 #[test]
1406 fn test_10k_row_stress() {
1407 let dir = tempfile::tempdir().unwrap();
1408 let mm = Arc::new(MemoryManager::new(256 * 1024 * 1024));
1409 let config = BufferManagerConfig::default();
1410 let bm = Arc::new(Mutex::new(BufferManager::new(dir.path().to_path_buf(), mm, config)));
1411
1412 let mut col = Column::new(LogicalTypeID::Int64, 0, 0, dir.path(), bm.clone(), DEFAULT_PAGE_SIZE);
1413
1414 for i in 0i64..10_000 {
1416 col.append_value(&Value::Int64(i)).unwrap();
1417 }
1418 assert_eq!(col.num_values, 10_000);
1419
1420 for i in 0i64..10_000 {
1422 let v = col.get_value(i as u64).unwrap();
1423 assert_eq!(v, Value::Int64(i), "Stress test mismatch at {}", i);
1424 }
1425
1426 col.flush().unwrap();
1428 for i in 0i64..10_000 {
1429 let v = col.get_value(i as u64).unwrap();
1430 assert_eq!(v, Value::Int64(i), "Post-flush stress mismatch at {}", i);
1431 }
1432
1433 assert!(
1435 col.num_pages > 1,
1436 "Stress test should use multiple pages, got {}",
1437 col.num_pages
1438 );
1439 }
1440
1441 #[test]
1445 fn test_wal_recovery_insert_then_recover() {
1446 let dir = tempfile::tempdir().unwrap();
1447
1448 let _row_count = {
1450 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1451 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1452
1453 let mut table = sm.create_node_table(
1455 "Person".into(),
1456 vec![
1457 ColumnDefinition {
1458 compression: akar_common::enums::CompressionType::Uncompressed,
1459 name: "name".into(),
1460 logical_type: LogicalTypeID::String,
1461 is_primary_key: true,
1462 },
1463 ColumnDefinition {
1464 compression: akar_common::enums::CompressionType::Uncompressed,
1465 name: "age".into(),
1466 logical_type: LogicalTypeID::Int64,
1467 is_primary_key: false,
1468 },
1469 ],
1470 );
1471
1472 table
1474 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1475 .unwrap();
1476 table
1477 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1478 .unwrap();
1479 table
1480 .insert_row(vec![Value::String("Charlie".into()), Value::Int64(35)])
1481 .unwrap();
1482
1483 {
1486 sm.table_catalog.create_node_table(
1489 "Person".into(),
1490 vec![
1491 ColumnDefinition {
1492 compression: akar_common::enums::CompressionType::Uncompressed,
1493 name: "name".into(),
1494 logical_type: LogicalTypeID::String,
1495 is_primary_key: true,
1496 },
1497 ColumnDefinition {
1498 compression: akar_common::enums::CompressionType::Uncompressed,
1499 name: "age".into(),
1500 logical_type: LogicalTypeID::Int64,
1501 is_primary_key: false,
1502 },
1503 ],
1504 );
1505 }
1506
1507 let count = table.num_rows;
1508 assert_eq!(count, 3);
1509
1510 {
1512 let mut wal = sm.wal.lock().unwrap();
1513 wal.append(WALRecord::Insert {
1514 table_id: table.table_id,
1515 data: vec![
1516 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,
1517 ],
1518 });
1519 wal.append(WALRecord::Insert {
1520 table_id: table.table_id,
1521 data: vec![
1522 TAG_STRING, 3, 0, 0, 0, b'B', b'o', b'b', TAG_INT64, 25, 0, 0, 0, 0, 0, 0, 0,
1523 ],
1524 });
1525 wal.append(WALRecord::Insert {
1526 table_id: table.table_id,
1527 data: vec![
1528 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,
1529 0, 0,
1530 ],
1531 });
1532 wal.flush_to_disk().unwrap();
1533 }
1534
1535 count
1536 }; {
1541 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1542 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1543
1544 let wal_path = dir.path().join("wal.log");
1546 assert!(wal_path.exists(), "WAL file should exist for recovery");
1547
1548 sm.create_node_table(
1551 "Person".into(),
1552 vec![
1553 ColumnDefinition {
1554 compression: akar_common::enums::CompressionType::Uncompressed,
1555 name: "name".into(),
1556 logical_type: LogicalTypeID::String,
1557 is_primary_key: true,
1558 },
1559 ColumnDefinition {
1560 compression: akar_common::enums::CompressionType::Uncompressed,
1561 name: "age".into(),
1562 logical_type: LogicalTypeID::Int64,
1563 is_primary_key: false,
1564 },
1565 ],
1566 );
1567
1568 let recovered = sm.recover().unwrap();
1570 assert_eq!(recovered, 3, "Should recover 3 WAL records");
1571
1572 {
1575 let recovered_table = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1576 assert_eq!(recovered_table.num_rows, 3, "Should have recovered 3 rows");
1577 assert_eq!(recovered_table.get_value(0, 0), Some(&Value::String("Alice".into())));
1578 assert_eq!(recovered_table.get_value(1, 0), Some(&Value::String("Bob".into())));
1579 assert_eq!(recovered_table.get_value(2, 0), Some(&Value::String("Charlie".into())));
1580 assert_eq!(recovered_table.get_value(0, 1), Some(&Value::Int64(30)));
1581 assert_eq!(recovered_table.get_value(1, 1), Some(&Value::Int64(25)));
1582 assert_eq!(recovered_table.get_value(2, 1), Some(&Value::Int64(35)));
1583 }
1584
1585 {
1588 let wal = sm.wal.lock().unwrap();
1589 assert_eq!(
1590 wal.len(),
1591 1,
1592 "WAL should have only the checkpoint marker after recovery"
1593 );
1594 assert!(matches!(wal.records()[0], crate::wal::WALRecord::Checkpoint));
1595 }
1596 }
1597 }
1598
1599 #[test]
1603 fn test_wal_recovery_no_wal() {
1604 let dir = tempfile::tempdir().unwrap();
1605
1606 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1607 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1608
1609 let recovered = sm.recover().unwrap();
1611 assert_eq!(recovered, 0, "No WAL = no records recovered");
1612 }
1613
1614 #[test]
1618 fn test_wal_recovery_empty_wal() {
1619 let dir = tempfile::tempdir().unwrap();
1620
1621 let wal_path = dir.path().join("wal.log");
1623 std::fs::write(&wal_path, b"").unwrap();
1624
1625 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
1626 let sm = StorageManager::new(dir.path().to_path_buf(), mm);
1627
1628 let recovered = sm.recover().unwrap();
1629 assert_eq!(recovered, 0, "Empty WAL = no records recovered");
1630 }
1631
1632 #[test]
1636 fn test_wal_load_from_disk_roundtrip() {
1637 use crate::wal::WALRecord;
1638 let dir = tempfile::tempdir().unwrap();
1639 let wal_path = dir.path().join("wal.log");
1640
1641 {
1643 let mut wal = WAL::new(wal_path.clone());
1644 wal.append(WALRecord::Insert {
1645 table_id: 42,
1646 data: vec![1, 2, 3, 4],
1647 });
1648 wal.append(WALRecord::Delete {
1649 table_id: 42,
1650 row_id: 0,
1651 });
1652 wal.append(WALRecord::Update {
1653 table_id: 42,
1654 row_id: 1,
1655 column: 2,
1656 data: vec![5, 6],
1657 });
1658 wal.append(WALRecord::ColumnWrite {
1659 table_id: 42,
1660 col_id: 0,
1661 page_id: 1,
1662 data: vec![7, 8, 9],
1663 });
1664 wal.append(WALRecord::Commit { transaction_id: 100 });
1665 wal.append(WALRecord::Rollback { transaction_id: 101 });
1666 wal.append(WALRecord::Checkpoint);
1667 wal.flush_to_disk().unwrap();
1668 }
1669
1670 {
1672 let mut wal = WAL::new(wal_path.clone());
1673 wal.load_from_disk().unwrap();
1674 assert_eq!(wal.len(), 7, "Should load 7 records from disk");
1675 assert!(wal.is_dirty());
1676
1677 match &wal.records()[0] {
1679 WALRecord::Insert { table_id, data } => {
1680 assert_eq!(*table_id, 42);
1681 assert_eq!(data, &[1, 2, 3, 4]);
1682 }
1683 _ => panic!("Expected Insert"),
1684 }
1685 match &wal.records()[1] {
1686 WALRecord::Delete { table_id, row_id } => {
1687 assert_eq!(*table_id, 42);
1688 assert_eq!(*row_id, 0);
1689 }
1690 _ => panic!("Expected Delete"),
1691 }
1692 match &wal.records()[4] {
1693 WALRecord::Commit { transaction_id } => {
1694 assert_eq!(*transaction_id, 100);
1695 }
1696 _ => panic!("Expected Commit"),
1697 }
1698 match &wal.records()[5] {
1699 WALRecord::Rollback { transaction_id } => {
1700 assert_eq!(*transaction_id, 101);
1701 }
1702 _ => panic!("Expected Rollback"),
1703 }
1704 match &wal.records()[6] {
1705 WALRecord::Checkpoint => {}
1706 _ => panic!("Expected Checkpoint"),
1707 }
1708 }
1709 }
1710
1711 #[test]
1715 fn test_commit_pipeline_local_storage_flush() {
1716 let (sm, _dir) = setup_integration();
1717
1718 let table_id;
1720 {
1721 let table = sm.table_catalog.create_node_table(
1722 "Person".into(),
1723 vec![
1724 ColumnDefinition {
1725 compression: akar_common::enums::CompressionType::Uncompressed,
1726 name: "name".into(),
1727 logical_type: LogicalTypeID::String,
1728 is_primary_key: true,
1729 },
1730 ColumnDefinition {
1731 compression: akar_common::enums::CompressionType::Uncompressed,
1732 name: "age".into(),
1733 logical_type: LogicalTypeID::Int64,
1734 is_primary_key: false,
1735 },
1736 ],
1737 );
1738 table_id = table.table_id;
1739 }
1740
1741 let mut local_storage = crate::local_storage::LocalStorage::new();
1743 {
1744 let txn_table = local_storage.get_or_create_table(table_id);
1745
1746 let mut row_bytes = Vec::new();
1748 row_bytes.push(13 );
1749 let name = "Alice";
1750 let name_bytes = name.as_bytes();
1751 row_bytes.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1752 row_bytes.extend_from_slice(name_bytes);
1753 row_bytes.push(2 );
1754 row_bytes.extend_from_slice(&30i64.to_le_bytes());
1755
1756 txn_table.insert(row_bytes);
1757 }
1758
1759 assert_eq!(local_storage.len(), 1, "Should have 1 table in local storage");
1760
1761 let shadow = crate::shadow_file::ShadowFile::new();
1763 sm.commit_transaction(
1764 &local_storage,
1765 &shadow,
1766 -1, 1, None,
1769 )
1770 .unwrap();
1771
1772 {
1774 let t = sm.table_catalog.get_node_table_by_name("Person").unwrap();
1775 assert_eq!(t.num_rows, 1, "Should have 1 row after commit");
1776 assert_eq!(t.get_value(0, 0), Some(&Value::String("Alice".into())));
1777 assert_eq!(t.get_value(0, 1), Some(&Value::Int64(30)));
1778 }
1779
1780 {
1782 let wal = sm.wal.lock().unwrap();
1783 assert_eq!(wal.len(), 1, "WAL should have Checkpoint marker after commit");
1785 }
1786 }
1787
1788 #[test]
1792 fn test_rollback_pipeline_no_data_written() {
1793 let (sm, _dir) = setup_integration();
1794 let mut local_storage = crate::local_storage::LocalStorage::new();
1795 let mut shadow = crate::shadow_file::ShadowFile::new();
1796
1797 {
1799 let txn_table = local_storage.get_or_create_table(0);
1800 txn_table.insert(vec![2 , 42, 0, 0, 0, 0, 0, 0, 0]);
1801 }
1802
1803 assert!(!local_storage.is_empty(), "LocalStorage should have buffered data");
1804
1805 sm.rollback_transaction(&mut local_storage, &mut shadow, 1 , &[])
1807 .unwrap();
1808
1809 assert!(local_storage.is_empty(), "LocalStorage should be empty after rollback");
1811 assert!(shadow.is_empty(), "ShadowFile should be empty after rollback");
1812 }
1813
1814 #[test]
1818 fn test_commit_multiple_rows() {
1819 let (sm, _dir) = setup_integration();
1820 sm.create_node_table(
1821 "Item".into(),
1822 vec![
1823 ColumnDefinition {
1824 compression: akar_common::enums::CompressionType::Uncompressed,
1825 name: "name".into(),
1826 logical_type: LogicalTypeID::String,
1827 is_primary_key: true,
1828 },
1829 ColumnDefinition {
1830 compression: akar_common::enums::CompressionType::Uncompressed,
1831 name: "price".into(),
1832 logical_type: LogicalTypeID::Double,
1833 is_primary_key: false,
1834 },
1835 ],
1836 );
1837
1838 let mut local = crate::local_storage::LocalStorage::new();
1840 {
1841 let txn_table = local.get_or_create_table(0); let mut row = Vec::new();
1845 row.push(13);
1846 row.extend_from_slice(&6u32.to_le_bytes());
1847 row.extend_from_slice(b"Widget");
1848 row.push(11);
1849 row.extend_from_slice(&19.99f64.to_le_bytes());
1850 txn_table.insert(row);
1851
1852 let mut row = Vec::new();
1854 row.push(13);
1855 row.extend_from_slice(&6u32.to_le_bytes());
1856 row.extend_from_slice(b"Gadget");
1857 row.push(11);
1858 row.extend_from_slice(&29.99f64.to_le_bytes());
1859 txn_table.insert(row);
1860 }
1861
1862 let shadow = crate::shadow_file::ShadowFile::new();
1863 sm.commit_transaction(&local, &shadow, 0 , 2 , None)
1864 .unwrap();
1865
1866 {
1868 let t = sm.table_catalog.get_node_table_by_name("Item").unwrap();
1869 assert_eq!(t.num_rows, 2, "Should have 2 rows after commit");
1870 }
1871 }
1872 #[test]
1873 fn test_zone_map_pushdown() {
1874 use crate::column_chunk::NODE_GROUP_SIZE;
1875 use crate::table::{ColumnDefinition, NodeTable};
1876 use akar_common::types::{LogicalTypeID, Value};
1877
1878 let db_path = "test_zone_map_pushdown.db";
1879 let _ = std::fs::remove_file(db_path);
1880
1881 let mut table = NodeTable::new(
1882 0,
1883 db_path.to_string(),
1884 vec![
1885 ColumnDefinition {
1886 compression: akar_common::enums::CompressionType::Uncompressed,
1887 name: "id".into(),
1888 logical_type: LogicalTypeID::Int64,
1889 is_primary_key: true,
1890 },
1891 ColumnDefinition {
1892 compression: akar_common::enums::CompressionType::Uncompressed,
1893 name: "value".into(),
1894 logical_type: LogicalTypeID::Int64,
1895 is_primary_key: false,
1896 },
1897 ],
1898 );
1899
1900 for i in 0..NODE_GROUP_SIZE as i64 {
1902 table.insert_row(vec![Value::Int64(i), Value::Int64(i)]).unwrap();
1903 }
1904
1905 for i in 0..NODE_GROUP_SIZE as i64 {
1907 let val = i + NODE_GROUP_SIZE as i64;
1908 table.insert_row(vec![Value::Int64(val), Value::Int64(val)]).unwrap();
1909 }
1910
1911 let predicate = Some((0, ">", &Value::Int64(5000)));
1914 let data = table.to_column_major_data_with_predicate(predicate);
1915
1916 assert_eq!(
1919 data[0].len(),
1920 NODE_GROUP_SIZE,
1921 "Only the second chunk should be returned"
1922 );
1923 assert_eq!(
1924 data[0][0],
1925 Value::Int64(NODE_GROUP_SIZE as i64),
1926 "First element should be from the second chunk"
1927 );
1928
1929 let _ = std::fs::remove_file(db_path);
1930 }
1931}