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