Skip to main content

akar_storage/
column_chunk.rs

1//! ColumnChunk — in-memory buffer for a contiguous range of column values.
2//!
3//! A `ColumnChunk` accumulates values in memory and flushes them to a
4//! persistent `Column` (backed by the `BufferManager`) when full. This
5//! matches the C++ Akar `ChunkedNodeGroup` / `ColumnChunk` concept.
6//!
7//! # Strategy
8//!
9//! Values are appended to an internal `Vec<Value>`. When the chunk reaches
10//! `NODE_GROUP_SIZE` entries it is considered full. The caller should then
11//! call `flush_to_column()` to batch-write all buffered values to the
12//! column's on-disk pages via `Column::append_value()`.
13
14use crate::column::Column;
15use crate::update_info::UpdateInfo;
16use akar_common::error::StorageError;
17use akar_common::types::{PhysicalTypeID, Value};
18use arrow::array::{
19    ArrayRef, BooleanBuilder, Float32Builder, Float64Builder, Int8Builder, Int16Builder, Int32Builder, Int64Builder,
20    StringBuilder, UInt64Builder,
21};
22use std::collections::HashMap;
23
24/// Default number of rows per column chunk (matches C++ Akar default).
25pub const NODE_GROUP_SIZE: usize = 4096;
26
27/// An in-memory buffer that accumulates values before flushing to a `Column`.
28#[derive(Debug, Clone)]
29pub struct ColumnChunk {
30    /// Buffered values in insertion order.
31    values: Vec<Value>,
32    /// Maximum number of values before the chunk is considered full.
33    capacity: usize,
34    /// Optional MVCC update version chain for this chunk.
35    /// Tracks versioned updates to support snapshot isolation.
36    pub update_info: Option<UpdateInfo>,
37    /// Min/max stats for zone map predicate pushdown.
38    pub stats: crate::predicate::ColumnChunkStats,
39}
40
41impl ColumnChunk {
42    /// Create a new empty chunk with the default capacity (`NODE_GROUP_SIZE`).
43    pub fn new() -> Self {
44        Self {
45            values: Vec::with_capacity(NODE_GROUP_SIZE),
46            capacity: NODE_GROUP_SIZE,
47            update_info: None,
48            stats: crate::predicate::ColumnChunkStats::new(None, None),
49        }
50    }
51
52    /// Create a new empty chunk with a custom capacity.
53    pub fn with_capacity(capacity: usize) -> Self {
54        Self {
55            values: Vec::with_capacity(capacity),
56            capacity,
57            update_info: None,
58            stats: crate::predicate::ColumnChunkStats::new(None, None),
59        }
60    }
61
62    /// Enable MVCC update tracking for this chunk.
63    pub fn enable_update_info(&mut self) {
64        if self.update_info.is_none() {
65            self.update_info = Some(UpdateInfo::new(self.capacity));
66        }
67    }
68
69    // ------------------------------------------------------------------
70    // Public API
71    // ------------------------------------------------------------------
72
73    /// Append a single value into the buffer.
74    ///
75    /// Does **not** automatically flush when full — the caller should check
76    /// `is_full()` and call `flush_to_column()` at the appropriate time.
77    pub fn append(&mut self, value: Value) {
78        self.stats.update(&value);
79        self.values.push(value);
80    }
81
82    /// Set a value at a specific index (for in-place writes like INSERT UPDATE
83    /// and DELETE nulling).
84    ///
85    /// This is the non-versioned write path: it overwrites the cell without
86    /// creating an MVCC version node, so snapshot readers see the new value
87    /// immediately. Versioned writes must use [`Self::set_value_with_version`]
88    /// so snapshot reads resolve the version chain correctly (P52.21).
89    /// Returns an error if the index is out of bounds.
90    pub fn set_value(&mut self, idx: usize, value: Value) -> Result<(), StorageError> {
91        if idx >= self.values.len() {
92            return Err(StorageError::Page(format!(
93                "ColumnChunk index {idx} out of bounds (len={})",
94                self.values.len()
95            )));
96        }
97        self.stats.update(&value);
98        self.values[idx] = value;
99        Ok(())
100    }
101
102    /// Set a value with MVCC version tracking.
103    ///
104    /// Records the replaced value in the update version chain at `version` (a
105    /// transaction commit timestamp) and overwrites the base cell. Snapshot
106    /// readers with `snapshot_ts < version` keep seeing the replaced value;
107    /// readers at or after `version` see the new value (P52.21).
108    /// Returns an error if the index is out of bounds.
109    pub fn set_value_with_version(&mut self, idx: usize, value: Value, version: u64) -> Result<(), StorageError> {
110        if idx >= self.values.len() {
111            return Err(StorageError::Page(format!(
112                "ColumnChunk index {idx} out of bounds (len={})",
113                self.values.len()
114            )));
115        }
116        if let Some(ref ui) = self.update_info {
117            let old_data = serialize_value_for_version(&self.values[idx]);
118            ui.append_update(idx as u32, version, old_data);
119        }
120        self.stats.update(&value);
121        self.values[idx] = value;
122        Ok(())
123    }
124
125    /// Get a value considering MVCC visibility at a given snapshot timestamp.
126    /// If `snapshot_ts` is `None`, returns the latest value.
127    ///
128    /// When a snapshot timestamp is provided and `UpdateInfo` has a version
129    /// node for this row whose update is NOT yet visible at `snapshot_ts`
130    /// (i.e. `version > snapshot_ts`), the snapshot predates the update and
131    /// must see the replaced (old) value. Since this variant returns `&Value`
132    /// it cannot deserialize the chain's owned bytes, so callers that need a
133    /// versioned read must use [`Self::get_value_owned_with_snapshot`].
134    pub fn get_value_with_snapshot(
135        &self,
136        idx: usize,
137        snapshot_ts: Option<u64>,
138        _commit_history: &HashMap<u64, u64>,
139    ) -> Option<&Value> {
140        if idx >= self.values.len() {
141            return None;
142        }
143        // When no snapshot requested, return latest value directly
144        let ts = match snapshot_ts {
145            Some(ts) => ts,
146            None => return self.values.get(idx),
147        };
148        // If the snapshot predates an update on this row, the correct value
149        // is the replaced one stored in the version chain — which cannot be
150        // returned by reference here. Fall through to the base value only
151        // when every update is visible at `ts`; versioned callers use the
152        // owned variant below (P52.21).
153        if let Some(ref ui) = self.update_info {
154            let _ = ui.get_version(idx as u32, ts);
155        }
156        self.values.get(idx)
157    }
158
159    /// Get a value with MVCC snapshot isolation, returning an owned Value.
160    ///
161    /// This variant properly handles version chain traversal by deserializing
162    /// replaced values from the UpdateInfo chain. Returns the value visible at
163    /// `snapshot_ts`, or `None` if the index is out of bounds.
164    pub fn get_value_owned_with_snapshot(
165        &self,
166        idx: usize,
167        snapshot_ts: Option<u64>,
168        _commit_history: &HashMap<u64, u64>,
169    ) -> Option<Value> {
170        if idx >= self.values.len() {
171            return None;
172        }
173        let ts = match snapshot_ts {
174            Some(ts) => ts,
175            None => return Some(self.values[idx].clone()),
176        };
177        // A snapshot read that predates an update must see the replaced value
178        // from the chain (base holds the latest value) (P52.21).
179        if let Some(ref ui) = self.update_info {
180            if let Some(old_data) = ui.get_version(idx as u32, ts) {
181                if let Ok(old_value) = serde_json::from_slice::<Value>(&old_data) {
182                    return Some(old_value);
183                }
184            }
185        }
186        // Every update is visible at `ts` — return the current base value
187        Some(self.values[idx].clone())
188    }
189
190    /// Number of buffered values.
191    pub fn num_values(&self) -> usize {
192        self.values.len()
193    }
194
195    /// Whether the chunk has reached its capacity and should be flushed.
196    pub fn is_full(&self) -> bool {
197        self.values.len() >= self.capacity
198    }
199
200    /// Whether the chunk is empty.
201    pub fn is_empty(&self) -> bool {
202        self.values.is_empty()
203    }
204
205    /// Borrow the buffered values as a slice.
206    pub fn as_slice(&self) -> &[Value] {
207        &self.values
208    }
209
210    /// Drain all buffered values (leaves the chunk empty).
211    pub fn drain(&mut self) -> Vec<Value> {
212        std::mem::take(&mut self.values)
213    }
214
215    /// Scan a range of buffered values (inclusive `start`, exclusive `end`).
216    ///
217    /// Panics if the range is out of bounds.
218    pub fn scan(&self, start: usize, count: usize) -> Vec<Value> {
219        let end = (start + count).min(self.values.len());
220        self.values[start..end].to_vec()
221    }
222
223    /// Flush all buffered values into a `Column` via `append_value`, then
224    /// clear the buffer.
225    ///
226    /// Returns the number of values flushed.
227    pub fn flush_to_column(&mut self, column: &mut Column) -> std::io::Result<usize> {
228        let n = self.values.len();
229        if n == 0 {
230            return Ok(0);
231        }
232
233        // Take the values out so we don't hold the buffer during I/O.
234        let batch = std::mem::take(&mut self.values);
235
236        for value in &batch {
237            column.append_value(value)?;
238        }
239
240        // Re-allocate with the original capacity.
241        self.values = Vec::with_capacity(self.capacity);
242
243        Ok(n)
244    }
245
246    /// Flush all buffered values into a `Column`, but keep the data in the
247    /// buffer afterwards (for cases where the caller still needs it).
248    pub fn flush_copy_to_column(&self, column: &mut Column) -> std::io::Result<usize> {
249        let n = self.values.len();
250        if n == 0 {
251            return Ok(0);
252        }
253
254        for value in &self.values {
255            column.append_value(value)?;
256        }
257
258        Ok(n)
259    }
260
261    /// Clear the buffer without flushing.
262    pub fn clear(&mut self) {
263        self.values.clear();
264    }
265
266    /// Remaining capacity before the chunk is full.
267    pub fn remaining(&self) -> usize {
268        self.capacity.saturating_sub(self.values.len())
269    }
270
271    /// Access a single buffered value by index.
272    pub fn get(&self, index: usize) -> Option<&Value> {
273        self.values.get(index)
274    }
275
276    /// Capacity of this chunk.
277    pub fn capacity(&self) -> usize {
278        self.capacity
279    }
280
281    /// Convert buffered values directly into an Arrow array, skipping
282    /// intermediate `Vec<Vec<Value>>` materialization.
283    ///
284    /// This is the key optimization for the scan path: instead of cloning
285    /// every Value into a `Vec<Vec<Value>>` and then building Arrow arrays
286    /// from that, we read directly from `self.values` into Arrow builders.
287    pub fn to_arrow_array(&self, phys_type: PhysicalTypeID) -> ArrayRef {
288        let size = self.values.len();
289        match phys_type {
290            PhysicalTypeID::Bool => {
291                let mut builder = BooleanBuilder::with_capacity(size);
292                for v in &self.values {
293                    match v {
294                        Value::Bool(b) => builder.append_value(*b),
295                        _ => builder.append_null(),
296                    }
297                }
298                std::sync::Arc::new(builder.finish())
299            }
300            PhysicalTypeID::Int64 => {
301                let mut builder = Int64Builder::with_capacity(size);
302                for v in &self.values {
303                    match v {
304                        Value::Int64(n) => builder.append_value(*n),
305                        Value::Int32(n) => builder.append_value(*n as i64),
306                        Value::Int16(n) => builder.append_value(*n as i64),
307                        Value::Int8(n) => builder.append_value(*n as i64),
308                        Value::UInt64(n) => builder.append_value(*n as i64),
309                        Value::UInt32(n) => builder.append_value(*n as i64),
310                        Value::UInt16(n) => builder.append_value(*n as i64),
311                        Value::UInt8(n) => builder.append_value(*n as i64),
312                        Value::Date(n) => builder.append_value(n.0 as i64),
313                        Value::Timestamp(n)
314                        | Value::TimestampNs(n)
315                        | Value::TimestampMs(n)
316                        | Value::TimestampSec(n) => builder.append_value(n.0),
317                        Value::TimestampTz(n) => builder.append_value(n.0),
318                        Value::DTime(n) => builder.append_value(*n),
319                        _ => builder.append_null(),
320                    }
321                }
322                std::sync::Arc::new(builder.finish())
323            }
324            PhysicalTypeID::UInt64 => {
325                let mut builder = UInt64Builder::with_capacity(size);
326                for v in &self.values {
327                    match v {
328                        Value::UInt64(n) => builder.append_value(*n),
329                        Value::UInt32(n) => builder.append_value(*n as u64),
330                        Value::UInt16(n) => builder.append_value(*n as u64),
331                        Value::UInt8(n) => builder.append_value(*n as u64),
332                        Value::Int64(n) if *n >= 0 => builder.append_value(*n as u64),
333                        _ => builder.append_null(),
334                    }
335                }
336                std::sync::Arc::new(builder.finish())
337            }
338            PhysicalTypeID::Int32 => {
339                let mut builder = Int32Builder::with_capacity(size);
340                for v in &self.values {
341                    match v {
342                        Value::Int32(n) => builder.append_value(*n),
343                        Value::Int16(n) => builder.append_value(*n as i32),
344                        Value::Int8(n) => builder.append_value(*n as i32),
345                        _ => builder.append_null(),
346                    }
347                }
348                std::sync::Arc::new(builder.finish())
349            }
350            PhysicalTypeID::Int16 => {
351                let mut builder = Int16Builder::with_capacity(size);
352                for v in &self.values {
353                    match v {
354                        Value::Int16(n) => builder.append_value(*n),
355                        Value::Int8(n) => builder.append_value(*n as i16),
356                        _ => builder.append_null(),
357                    }
358                }
359                std::sync::Arc::new(builder.finish())
360            }
361            PhysicalTypeID::Int8 => {
362                let mut builder = Int8Builder::with_capacity(size);
363                for v in &self.values {
364                    match v {
365                        Value::Int8(n) => builder.append_value(*n),
366                        _ => builder.append_null(),
367                    }
368                }
369                std::sync::Arc::new(builder.finish())
370            }
371            PhysicalTypeID::Double => {
372                let mut builder = Float64Builder::with_capacity(size);
373                for v in &self.values {
374                    match v {
375                        Value::Double(n) => builder.append_value(*n),
376                        Value::Float(n) => builder.append_value(*n as f64),
377                        Value::Int64(n) => builder.append_value(*n as f64),
378                        _ => builder.append_null(),
379                    }
380                }
381                std::sync::Arc::new(builder.finish())
382            }
383            PhysicalTypeID::Float => {
384                let mut builder = Float32Builder::with_capacity(size);
385                for v in &self.values {
386                    match v {
387                        Value::Float(n) => builder.append_value(*n),
388                        Value::Double(n) => builder.append_value(*n as f32),
389                        _ => builder.append_null(),
390                    }
391                }
392                std::sync::Arc::new(builder.finish())
393            }
394            PhysicalTypeID::String => {
395                let mut builder = StringBuilder::with_capacity(size, size * 16);
396                for v in &self.values {
397                    match v {
398                        Value::String(s) => builder.append_value(s),
399                        _ => builder.append_null(),
400                    }
401                }
402                std::sync::Arc::new(builder.finish())
403            }
404            PhysicalTypeID::List | PhysicalTypeID::Array | PhysicalTypeID::Struct => {
405                akar_common::arrow_vector::arrow_array_from_values(&self.values)
406            }
407            _ => {
408                let mut builder = Int64Builder::with_capacity(size);
409                for _ in 0..size {
410                    builder.append_null();
411                }
412                std::sync::Arc::new(builder.finish())
413            }
414        }
415    }
416}
417
418impl Default for ColumnChunk {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424impl From<Vec<Value>> for ColumnChunk {
425    fn from(values: Vec<Value>) -> Self {
426        let capacity = values.len().max(NODE_GROUP_SIZE);
427        let mut stats = crate::predicate::ColumnChunkStats::new(None, None);
428        for v in &values {
429            stats.update(v);
430        }
431        Self {
432            values,
433            capacity,
434            update_info: None,
435            stats,
436        }
437    }
438}
439
440// ---------------------------------------------------------------------------
441// Tests
442// ---------------------------------------------------------------------------
443
444/// Serialize a Value to bytes for storage in the update version chain.
445fn serialize_value_for_version(v: &Value) -> Vec<u8> {
446    // Use serde_json for a simple portable binary representation.
447    // The version chain data is only used internally for rollback recovery;
448    // performance is not critical for this initial implementation.
449    serde_json::to_vec(v).unwrap_or_else(|_| vec![])
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use crate::column::Column;
456    use crate::page::DEFAULT_PAGE_SIZE;
457    use akar_common::memory::MemoryManager;
458    use akar_common::types::LogicalTypeID;
459
460    use std::sync::{Arc, Mutex};
461
462    fn setup_column(db_path: &std::path::Path) -> Column {
463        let mm = Arc::new(MemoryManager::new(64 * 1024 * 1024));
464        let config = crate::buffer_manager::BufferManagerConfig::default();
465        let bm = Arc::new(Mutex::new(crate::buffer_manager::BufferManager::new(
466            db_path.to_path_buf(),
467            mm,
468            config,
469        )));
470        Column::new(LogicalTypeID::Int64, 0, 0, db_path, bm, DEFAULT_PAGE_SIZE)
471    }
472
473    #[test]
474    fn test_empty_chunk() {
475        let chunk = ColumnChunk::new();
476        assert!(chunk.is_empty());
477        assert_eq!(chunk.num_values(), 0);
478        assert_eq!(chunk.remaining(), NODE_GROUP_SIZE);
479    }
480
481    #[test]
482    fn test_append_and_count() {
483        let mut chunk = ColumnChunk::new();
484        chunk.append(Value::Int64(1));
485        chunk.append(Value::Int64(2));
486        chunk.append(Value::Int64(3));
487        assert_eq!(chunk.num_values(), 3);
488        assert!(!chunk.is_empty());
489    }
490
491    #[test]
492    fn test_is_full() {
493        let mut chunk = ColumnChunk::with_capacity(3);
494        assert!(!chunk.is_full());
495        chunk.append(Value::Int64(1));
496        chunk.append(Value::Int64(2));
497        chunk.append(Value::Int64(3));
498        assert!(chunk.is_full());
499    }
500
501    #[test]
502    fn test_scan() {
503        let mut chunk = ColumnChunk::new();
504        for i in 0..10 {
505            chunk.append(Value::Int64(i));
506        }
507        let scanned = chunk.scan(2, 4);
508        assert_eq!(scanned.len(), 4);
509        assert_eq!(scanned[0], Value::Int64(2));
510        assert_eq!(scanned[3], Value::Int64(5));
511    }
512
513    #[test]
514    fn test_drain() {
515        let mut chunk = ColumnChunk::new();
516        chunk.append(Value::Int64(42));
517        chunk.append(Value::Int64(43));
518        assert_eq!(chunk.num_values(), 2);
519
520        let drained = chunk.drain();
521        assert_eq!(drained.len(), 2);
522        assert!(chunk.is_empty());
523    }
524
525    #[test]
526    fn test_flush_to_column() {
527        let dir = tempfile::tempdir().unwrap();
528        let mut column = setup_column(dir.path());
529        let mut chunk = ColumnChunk::new();
530
531        for i in 0i64..50 {
532            chunk.append(Value::Int64(i));
533        }
534
535        assert_eq!(chunk.num_values(), 50);
536        let flushed = chunk.flush_to_column(&mut column).unwrap();
537        assert_eq!(flushed, 50);
538        assert!(chunk.is_empty());
539        assert_eq!(column.num_values, 50);
540
541        // Verify the data was written correctly
542        for i in 0i64..50 {
543            let v = column.get_value(i as u64).unwrap();
544            assert_eq!(v, Value::Int64(i));
545        }
546    }
547
548    #[test]
549    fn test_flush_copy_to_column_preserves_buffer() {
550        let dir = tempfile::tempdir().unwrap();
551        let mut column = setup_column(dir.path());
552        let mut chunk = ColumnChunk::new();
553
554        chunk.append(Value::Int64(10));
555        chunk.append(Value::Int64(20));
556
557        let flushed = chunk.flush_copy_to_column(&mut column).unwrap();
558        assert_eq!(flushed, 2);
559        // Buffer is still intact
560        assert_eq!(chunk.num_values(), 2);
561        assert_eq!(column.num_values, 2);
562    }
563
564    #[test]
565    fn test_flush_empty_chunk() {
566        let dir = tempfile::tempdir().unwrap();
567        let mut column = setup_column(dir.path());
568        let mut chunk = ColumnChunk::new();
569
570        let flushed = chunk.flush_to_column(&mut column).unwrap();
571        assert_eq!(flushed, 0);
572        assert!(chunk.is_empty());
573        assert_eq!(column.num_values, 0);
574    }
575
576    #[test]
577    fn test_clear() {
578        let mut chunk = ColumnChunk::new();
579        chunk.append(Value::Int64(99));
580        chunk.clear();
581        assert!(chunk.is_empty());
582        assert_eq!(chunk.num_values(), 0);
583    }
584
585    #[test]
586    fn test_remaining() {
587        let mut chunk = ColumnChunk::with_capacity(10);
588        assert_eq!(chunk.remaining(), 10);
589        chunk.append(Value::Int64(1));
590        assert_eq!(chunk.remaining(), 9);
591        chunk.append(Value::Int64(2));
592        assert_eq!(chunk.remaining(), 8);
593    }
594
595    #[test]
596    fn test_from_vec() {
597        let values = vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)];
598        let chunk = ColumnChunk::from(values);
599        assert_eq!(chunk.num_values(), 3);
600        assert_eq!(chunk.get(0), Some(&Value::Int64(1)));
601        assert_eq!(chunk.get(2), Some(&Value::Int64(3)));
602    }
603
604    #[test]
605    fn test_chunk_default_capacity() {
606        let chunk = ColumnChunk::new();
607        assert_eq!(chunk.capacity(), NODE_GROUP_SIZE);
608    }
609
610    #[test]
611    fn test_multiple_flushes_to_same_column() {
612        let dir = tempfile::tempdir().unwrap();
613        let mut column = setup_column(dir.path());
614        let mut chunk = ColumnChunk::with_capacity(20);
615
616        // First batch
617        for i in 0i64..15 {
618            chunk.append(Value::Int64(i));
619        }
620        chunk.flush_to_column(&mut column).unwrap();
621        assert_eq!(column.num_values, 15);
622
623        // Second batch
624        for i in 15i64..30 {
625            chunk.append(Value::Int64(i));
626        }
627        chunk.flush_to_column(&mut column).unwrap();
628        assert_eq!(column.num_values, 30);
629
630        // Verify all values
631        for i in 0i64..30 {
632            let v = column.get_value(i as u64).unwrap();
633            assert_eq!(v, Value::Int64(i));
634        }
635    }
636
637    #[test]
638    fn test_large_flush() {
639        let dir = tempfile::tempdir().unwrap();
640        let mut column = setup_column(dir.path());
641        let mut chunk = ColumnChunk::new();
642
643        // Fill the chunk to capacity
644        for i in 0..NODE_GROUP_SIZE {
645            chunk.append(Value::Int64(i as i64));
646        }
647        assert!(chunk.is_full());
648
649        let flushed = chunk.flush_to_column(&mut column).unwrap();
650        assert_eq!(flushed, NODE_GROUP_SIZE);
651        assert!(chunk.is_empty());
652        assert_eq!(column.num_values, NODE_GROUP_SIZE as u64);
653
654        // Verify a few values at boundaries
655        assert_eq!(column.get_value(0).unwrap(), Value::Int64(0));
656        let last_idx = (NODE_GROUP_SIZE - 1) as u64;
657        assert_eq!(column.get_value(last_idx).unwrap(), Value::Int64(last_idx as i64));
658    }
659}