1use crate::column::Column;
14use crate::column_chunk::{ColumnChunk, NODE_GROUP_SIZE};
15use crate::spiller::{MultiWayStreamMerge, SpillFile, Spiller};
16use crate::version_info::VersionInfo;
17use akar_common::error::StorageError;
18use akar_common::types::Value;
19use std::collections::HashMap;
20use std::sync::Arc;
21
22#[derive(Debug, Clone)]
38pub struct NodeGroup {
39 pub columns: Vec<ColumnChunk>,
41 pub start_offset: u64,
43 pub num_nodes: u64,
45 pub version_info: Option<VersionInfo>,
47 spiller: Option<Arc<Spiller>>,
49 spill_files: Vec<SpillFile>,
51}
52
53impl NodeGroup {
54 pub fn new(num_columns: usize, start_offset: u64) -> Self {
60 let columns = (0..num_columns).map(|_| ColumnChunk::new()).collect();
61 Self {
62 columns,
63 start_offset,
64 num_nodes: 0,
65 version_info: None,
66 spiller: None,
67 spill_files: Vec::new(),
68 }
69 }
70
71 pub fn with_capacity(num_columns: usize, start_offset: u64, capacity: usize) -> Self {
73 let columns = (0..num_columns).map(|_| ColumnChunk::with_capacity(capacity)).collect();
74 Self {
75 columns,
76 start_offset,
77 num_nodes: 0,
78 version_info: None,
79 spiller: None,
80 spill_files: Vec::new(),
81 }
82 }
83
84 pub fn with_spiller(mut self, spiller: Arc<Spiller>) -> Self {
91 self.spiller = Some(spiller);
92 self
93 }
94
95 pub fn set_spiller(&mut self, spiller: Arc<Spiller>) {
97 self.spiller = Some(spiller);
98 }
99
100 pub fn enable_version_info(&mut self) {
103 if self.version_info.is_none() {
104 self.version_info = Some(VersionInfo::new(NODE_GROUP_SIZE));
105 }
106 }
107
108 pub fn append_row(&mut self, row: Vec<Value>) -> Result<(), StorageError> {
120 self.append_row_with_txn(row, None)
121 }
122
123 pub fn append_row_with_txn(&mut self, row: Vec<Value>, txn_id: Option<u64>) -> Result<(), StorageError> {
130 if row.len() != self.columns.len() {
131 return Err(StorageError::Page(format!(
132 "column count mismatch: expected {} values, got {}",
133 self.columns.len(),
134 row.len()
135 )));
136 }
137 if self.is_full() {
138 return Err(StorageError::Page("node group is already full".to_string()));
139 }
140
141 if let Some(ref spiller) = self.spiller
143 && !self.columns.is_empty()
144 && spiller.should_spill(&self.columns[0])
145 {
146 self.spill_and_clear()?;
147 }
148
149 for (col_idx, value) in row.into_iter().enumerate() {
150 self.columns[col_idx].append(value);
151 }
152 if let Some(ref vi) = self.version_info
154 && let Some(txn) = txn_id
155 {
156 vi.insert(txn, self.num_nodes as u32);
157 }
158 self.num_nodes += 1;
159 Ok(())
160 }
161
162 pub fn spill_and_clear(&mut self) -> Result<(), StorageError> {
171 let spiller = self
172 .spiller
173 .as_ref()
174 .ok_or_else(|| StorageError::Spiller("No spiller attached to NodeGroup".to_string()))?;
175
176 if self.is_empty() {
177 return Ok(());
178 }
179
180 let spill = spiller.spill_columns(&mut self.columns)?;
181 if let Some(sf) = spill {
182 self.spill_files.push(sf);
183 }
184 self.num_nodes = 0;
185 if let Some(ref vi) = self.version_info {
186 vi.reset();
187 }
188 Ok(())
189 }
190
191 pub fn restore_spilled(&mut self) -> Result<(), StorageError> {
200 if self.spill_files.is_empty() {
201 return Ok(());
202 }
203 let spiller = self
204 .spiller
205 .clone()
206 .ok_or_else(|| StorageError::Spiller("No spiller attached to NodeGroup".to_string()))?;
207 let num_cols = self.columns.len();
208
209 let mut rows: Vec<Vec<Value>> = Vec::new();
210 let files = std::mem::take(&mut self.spill_files);
211 for sf in &files {
212 let chunks = spiller.restore_columns(sf, num_cols)?;
213 let n = chunks.first().map(|c| c.num_values()).unwrap_or(0);
214 for r in 0..n {
215 let mut row = Vec::with_capacity(num_cols);
216 for c in &chunks {
217 row.push(c.get(r).cloned().unwrap_or(Value::Null));
218 }
219 rows.push(row);
220 }
221 }
222 for row in self.scan() {
223 rows.push(row);
224 }
225
226 let mut columns: Vec<ColumnChunk> = (0..num_cols).map(|_| ColumnChunk::new()).collect();
227 for row in &rows {
228 for (ci, value) in row.iter().enumerate() {
229 columns[ci].append(value.clone());
230 }
231 }
232 self.columns = columns;
233 self.num_nodes = rows.len() as u64;
234 if let Some(ref vi) = self.version_info {
238 vi.reset();
239 }
240 for sf in &files {
241 let _ = spiller.cleanup(sf);
242 }
243 Ok(())
244 }
245
246 pub fn flush_with_spiller(
257 &mut self,
258 columns: &mut [Column],
259 sort_key_column: Option<usize>,
260 dedup: bool,
261 ) -> std::io::Result<usize> {
262 if self.spill_files.is_empty() {
263 return self.flush(columns);
265 }
266
267 assert_eq!(
268 columns.len(),
269 self.columns.len(),
270 "NodeGroup::flush_with_spiller: column count mismatch"
271 );
272
273 let in_memory_rows = self.scan();
275 self.clear();
276
277 let sort_col = sort_key_column.unwrap_or(0);
279 let mut merger = MultiWayStreamMerge::new(&self.spill_files, Some(in_memory_rows), sort_col, dedup)
280 .map_err(std::io::Error::other)?;
281
282 let mut total: usize = 0;
284 while let Some(row) = merger.next_tuple() {
285 for (col_idx, value) in row.into_iter().enumerate() {
286 if col_idx < columns.len() {
287 columns[col_idx].append_value(&value)?;
288 }
289 }
290 total += 1;
291 }
292
293 if let Some(ref spiller) = self.spiller {
295 let files = std::mem::take(&mut self.spill_files);
296 for sf in &files {
297 let _ = spiller.cleanup(sf);
298 }
299 }
300
301 Ok(total)
302 }
303
304 pub fn is_full(&self) -> bool {
306 self.num_nodes as usize >= NODE_GROUP_SIZE
307 }
308
309 pub fn is_empty(&self) -> bool {
311 self.num_nodes == 0
312 }
313
314 pub fn num_columns(&self) -> usize {
316 self.columns.len()
317 }
318
319 pub fn has_spill_files(&self) -> bool {
321 !self.spill_files.is_empty()
322 }
323
324 pub fn remaining(&self) -> usize {
326 NODE_GROUP_SIZE.saturating_sub(self.num_nodes as usize)
327 }
328
329 pub fn flush(&mut self, columns: &mut [Column]) -> std::io::Result<usize> {
341 assert_eq!(
342 columns.len(),
343 self.columns.len(),
344 "NodeGroup::flush: column count mismatch"
345 );
346 let mut total = 0;
347 for (chunk, col) in self.columns.iter_mut().zip(columns.iter_mut()) {
348 let n = chunk.flush_to_column(col)?;
349 if total == 0 {
351 total = n;
352 }
353 debug_assert!(n == 0 || n == total, "inconsistent flush count");
354 }
355 self.num_nodes = 0;
356 Ok(total)
357 }
358
359 pub fn flush_copy(&self, columns: &mut [Column]) -> std::io::Result<usize> {
361 assert_eq!(
362 columns.len(),
363 self.columns.len(),
364 "NodeGroup::flush_copy: column count mismatch"
365 );
366 let mut total = 0;
367 for (chunk, col) in self.columns.iter().zip(columns.iter_mut()) {
368 let n = chunk.flush_copy_to_column(col)?;
369 if total == 0 {
370 total = n;
371 }
372 }
373 Ok(total)
374 }
375
376 pub fn scan(&self) -> Vec<Vec<Value>> {
381 let n_rows = self.num_nodes as usize;
382 let n_cols = self.columns.len();
383 let mut result = Vec::with_capacity(n_rows);
384
385 for row in 0..n_rows {
386 let mut row_data = Vec::with_capacity(n_cols);
387 for chunk in &self.columns {
388 match chunk.get(row) {
389 Some(v) => row_data.push(v.clone()),
390 None => row_data.push(Value::Null),
391 }
392 }
393 result.push(row_data);
394 }
395 result
396 }
397
398 pub fn scan_range(&self, start: usize, count: usize) -> Vec<Vec<Value>> {
402 let end = (start + count).min(self.num_nodes as usize);
403 if start >= end {
404 return Vec::new();
405 }
406 let n_cols = self.columns.len();
407 let mut result = Vec::with_capacity(end - start);
408
409 for row in start..end {
410 let mut row_data = Vec::with_capacity(n_cols);
411 for chunk in &self.columns {
412 match chunk.get(row) {
413 Some(v) => row_data.push(v.clone()),
414 None => row_data.push(Value::Null),
415 }
416 }
417 result.push(row_data);
418 }
419 result
420 }
421
422 pub fn get_value(&self, local_row: usize, col_idx: usize) -> Option<&Value> {
424 self.columns.get(col_idx).and_then(|chunk| chunk.get(local_row))
425 }
426
427 pub fn get_value_with_snapshot(
433 &self,
434 local_row: usize,
435 col_idx: usize,
436 snapshot_ts: Option<u64>,
437 commit_history: &HashMap<u64, u64>,
438 ) -> Option<&Value> {
439 if let Some(ts) = snapshot_ts
441 && !self.is_row_visible(local_row, ts, commit_history)
442 {
443 return None;
444 }
445 self.columns
447 .get(col_idx)
448 .and_then(|chunk| chunk.get_value_with_snapshot(local_row, snapshot_ts, commit_history))
449 }
450
451 pub fn get_value_owned_with_snapshot(
457 &self,
458 local_row: usize,
459 col_idx: usize,
460 snapshot_ts: Option<u64>,
461 commit_history: &HashMap<u64, u64>,
462 ) -> Option<Value> {
463 if let Some(ts) = snapshot_ts
465 && !self.is_row_visible(local_row, ts, commit_history)
466 {
467 return None;
468 }
469 self.columns
471 .get(col_idx)
472 .and_then(|chunk| chunk.get_value_owned_with_snapshot(local_row, snapshot_ts, commit_history))
473 }
474
475 pub fn is_row_visible(&self, local_row: usize, snapshot_ts: u64, commit_history: &HashMap<u64, u64>) -> bool {
478 match &self.version_info {
479 Some(vi) => vi.is_visible(local_row as u32, snapshot_ts, commit_history),
480 None => true, }
482 }
483
484 pub fn clear(&mut self) {
486 for chunk in &mut self.columns {
487 chunk.clear();
488 }
489 self.num_nodes = 0;
490 if let Some(ref vi) = self.version_info {
493 vi.reset();
494 }
495 }
496}
497
498#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::buffer_manager::BufferManagerConfig;
506 use crate::page::DEFAULT_PAGE_SIZE;
507 use akar_common::memory::MemoryManager;
508 use akar_common::types::LogicalTypeID;
509 use std::sync::{Arc, Mutex};
510
511 fn setup_columns(num_cols: usize, db_path: &std::path::Path) -> Vec<Column> {
512 let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
513 let config = BufferManagerConfig::default();
514 let bm = Arc::new(Mutex::new(crate::buffer_manager::BufferManager::new(
515 db_path.to_path_buf(),
516 mm,
517 config,
518 )));
519 (0..num_cols)
520 .map(|i| {
521 Column::new(
522 LogicalTypeID::Int64,
523 0,
524 i as u32,
525 db_path,
526 bm.clone(),
527 DEFAULT_PAGE_SIZE,
528 )
529 })
530 .collect()
531 }
532
533 #[test]
534 fn test_empty_group() {
535 let group = NodeGroup::new(3, 0);
536 assert_eq!(group.num_columns(), 3);
537 assert_eq!(group.num_nodes, 0);
538 assert!(group.is_empty());
539 assert!(!group.is_full());
540 assert_eq!(group.start_offset, 0);
541 }
542
543 #[test]
544 fn test_append_row() {
545 let mut group = NodeGroup::new(2, 100);
546 group.append_row(vec![Value::Int64(1), Value::Int64(2)]).unwrap();
547 assert_eq!(group.num_nodes, 1);
548 assert!(!group.is_empty());
549
550 group.append_row(vec![Value::Int64(3), Value::Int64(4)]).unwrap();
551 assert_eq!(group.num_nodes, 2);
552 }
553
554 #[test]
555 fn test_append_wrong_column_count() {
556 let mut group = NodeGroup::new(2, 0);
557 let result = group.append_row(vec![Value::Int64(1)]);
558 assert!(result.is_err());
559 assert!(result.unwrap_err().to_string().contains("column count mismatch"));
560 }
561
562 #[test]
563 fn test_append_when_full() {
564 let mut group = NodeGroup::with_capacity(1, 0, 5);
567 for _ in 0..10 {
568 group.append_row(vec![Value::Int64(1)]).unwrap();
569 }
570 assert!(!group.is_full());
571 assert_eq!(group.num_nodes, 10);
572 }
573
574 #[test]
575 fn test_scan() {
576 let mut group = NodeGroup::new(3, 0);
577 group
578 .append_row(vec![Value::Int64(10), Value::Int64(20), Value::Int64(30)])
579 .unwrap();
580 group
581 .append_row(vec![Value::Int64(11), Value::Int64(21), Value::Int64(31)])
582 .unwrap();
583
584 let data = group.scan();
585 assert_eq!(data.len(), 2);
586 assert_eq!(data[0][0], Value::Int64(10));
587 assert_eq!(data[0][1], Value::Int64(20));
588 assert_eq!(data[1][2], Value::Int64(31));
589 }
590
591 #[test]
592 fn test_scan_range() {
593 let mut group = NodeGroup::new(2, 0);
594 for i in 0..10 {
595 group.append_row(vec![Value::Int64(i), Value::Int64(i * 10)]).unwrap();
596 }
597
598 let slice = group.scan_range(3, 4);
599 assert_eq!(slice.len(), 4);
600 assert_eq!(slice[0][0], Value::Int64(3));
601 assert_eq!(slice[3][0], Value::Int64(6));
602 }
603
604 #[test]
605 fn test_get_value() {
606 let mut group = NodeGroup::new(2, 50);
607 group.append_row(vec![Value::Int64(100), Value::Int64(200)]).unwrap();
608
609 assert_eq!(group.get_value(0, 0), Some(&Value::Int64(100)));
610 assert_eq!(group.get_value(0, 1), Some(&Value::Int64(200)));
611 assert_eq!(group.get_value(1, 0), None);
612 }
613
614 #[test]
615 fn test_flush_to_columns() {
616 let dir = tempfile::tempdir().unwrap();
617 let mut cols = setup_columns(2, dir.path());
618 let mut group = NodeGroup::new(2, 0);
619
620 for i in 0i64..50 {
621 group.append_row(vec![Value::Int64(i), Value::Int64(i * 10)]).unwrap();
622 }
623
624 let flushed = group.flush(&mut cols).unwrap();
625 assert_eq!(flushed, 50);
626 assert_eq!(group.num_nodes, 0);
627 assert!(group.is_empty());
628
629 for i in 0i64..50 {
631 assert_eq!(cols[0].get_value(i as u64).unwrap(), Value::Int64(i));
632 assert_eq!(cols[1].get_value(i as u64).unwrap(), Value::Int64(i * 10));
633 }
634 }
635
636 #[test]
637 fn test_flush_copy_preserves_buffer() {
638 let dir = tempfile::tempdir().unwrap();
639 let mut cols = setup_columns(2, dir.path());
640 let mut group = NodeGroup::new(2, 0);
641
642 group.append_row(vec![Value::Int64(1), Value::Int64(2)]).unwrap();
643
644 let flushed = group.flush_copy(&mut cols).unwrap();
645 assert_eq!(flushed, 1);
646 assert_eq!(group.num_nodes, 1);
648 assert_eq!(cols[0].get_value(0).unwrap(), Value::Int64(1));
649 }
650
651 #[test]
652 fn test_restore_spilled_reconstructs_full_group() {
653 let dir = tempfile::tempdir().unwrap();
654 let spiller = Arc::new(crate::spiller::Spiller::new(dir.path(), 64));
655 let mut group = NodeGroup::new(2, 0);
656 group.set_spiller(spiller.clone());
657
658 for i in 0i64..20 {
660 group.append_row(vec![Value::Int64(i), Value::Int64(i * 10)]).unwrap();
661 }
662 assert!(!group.spill_files.is_empty(), "low threshold must spill");
663 assert!(group.num_nodes < 20, "spill must have evicted rows from memory");
664
665 group.restore_spilled().unwrap();
666 assert_eq!(group.num_nodes, 20, "restore must recover the full row set");
667 assert!(group.spill_files.is_empty(), "spill files cleaned up after restore");
668 for i in 0i64..20 {
669 assert_eq!(group.get_value(i as usize, 0), Some(&Value::Int64(i)));
670 assert_eq!(group.get_value(i as usize, 1), Some(&Value::Int64(i * 10)));
671 }
672 }
673
674 #[test]
675 fn test_clear() {
676 let mut group = NodeGroup::new(2, 0);
677 group.append_row(vec![Value::Int64(1), Value::Int64(2)]).unwrap();
678 group.clear();
679 assert_eq!(group.num_nodes, 0);
680 assert!(group.is_empty());
681 }
682
683 #[test]
684 fn test_clear_resets_version_info() {
685 let mut group = NodeGroup::new(2, 0);
686 group.enable_version_info();
687 group
688 .append_row_with_txn(vec![Value::Int64(1), Value::Int64(2)], Some(7))
689 .unwrap();
690 assert_eq!(group.version_info.as_ref().unwrap().num_inserters(), 1);
691
692 group.clear();
693
694 assert_eq!(group.version_info.as_ref().unwrap().num_inserters(), 0);
696 assert!(group.is_row_visible(0, 0, &HashMap::new()));
697 }
698
699 #[test]
700 fn test_spill_and_clear_resets_version_info() {
701 let dir = tempfile::tempdir().unwrap();
702 let spiller = Arc::new(crate::spiller::Spiller::new(dir.path(), 64));
703 let mut group = NodeGroup::new(2, 0);
704 group.set_spiller(spiller.clone());
705 group.enable_version_info();
706
707 for i in 0i64..20 {
710 group
711 .append_row_with_txn(vec![Value::Int64(i), Value::Int64(i * 10)], Some(7))
712 .unwrap();
713 }
714 assert!(!group.spill_files.is_empty(), "low threshold must spill");
715
716 group.restore_spilled().unwrap();
721 assert_eq!(group.num_nodes, 20);
722 let vi = group.version_info.as_ref().unwrap();
723 assert_eq!(vi.num_inserters(), 0, "stale spill records must be dropped");
724 for i in 0usize..20 {
725 assert!(
726 group.is_row_visible(i, 0, &HashMap::new()),
727 "row {i} visible by default"
728 );
729 }
730 }
731
732 #[test]
733 fn test_remaining() {
734 let mut group = NodeGroup::with_capacity(3, 0, 10);
736 assert_eq!(group.remaining(), NODE_GROUP_SIZE);
737 group
738 .append_row(vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)])
739 .unwrap();
740 assert_eq!(group.remaining(), NODE_GROUP_SIZE - 1);
741 group
742 .append_row(vec![Value::Int64(4), Value::Int64(5), Value::Int64(6)])
743 .unwrap();
744 assert_eq!(group.remaining(), NODE_GROUP_SIZE - 2);
745 }
746
747 #[test]
748 fn test_start_offset() {
749 let group = NodeGroup::new(2, 12345);
750 assert_eq!(group.start_offset, 12345);
751 }
752
753 #[test]
754 fn test_multi_column_scan() {
755 let mut group = NodeGroup::new(4, 0);
756 group
757 .append_row(vec![
758 Value::String("Alice".into()),
759 Value::Int64(30),
760 Value::Double(1.65),
761 Value::Bool(true),
762 ])
763 .unwrap();
764
765 let data = group.scan();
766 assert_eq!(data.len(), 1);
767 assert_eq!(data[0][0], Value::String("Alice".into()));
768 assert_eq!(data[0][1], Value::Int64(30));
769 assert_eq!(data[0][3], Value::Bool(true));
770 }
771
772 #[test]
773 fn test_multiple_flush_cycles() {
774 let dir = tempfile::tempdir().unwrap();
775 let mut cols = setup_columns(2, dir.path());
776 let mut group = NodeGroup::with_capacity(2, 0, 20);
777
778 for i in 0i64..15 {
780 group.append_row(vec![Value::Int64(i), Value::Int64(-i)]).unwrap();
781 }
782 assert_eq!(group.flush(&mut cols).unwrap(), 15);
783
784 for i in 15i64..30 {
786 group.append_row(vec![Value::Int64(i), Value::Int64(-i)]).unwrap();
787 }
788 assert_eq!(group.flush(&mut cols).unwrap(), 15);
789
790 assert_eq!(cols[0].num_values, 30);
792 for i in 0i64..30 {
793 assert_eq!(cols[0].get_value(i as u64).unwrap(), Value::Int64(i));
794 assert_eq!(cols[1].get_value(i as u64).unwrap(), Value::Int64(-i));
795 }
796 }
797}