Skip to main content

volas_core/dataframe/
computed.rs

1//! The materialized-directive (computed) column cache lifecycle on a
2//! [`DataFrame`]: recording a cached directive result, tracking staleness
3//! after an append, incrementally refreshing its tail, and invalidating it on
4//! a manual write. The cache (`DataFrame::computed`) is private frame state;
5//! these methods own its invariants.
6
7
8use crate::column::Column;
9use crate::error::{Result, VolasError};
10
11use super::{ComputedMeta, DataFrame};
12
13impl DataFrame {
14    /// Record that column `name` is a materialized directive result (valid for
15    /// all current rows).
16    pub fn set_computed(&mut self, name: &str, directive: String, lookback: usize) {
17        self.computed.insert(
18            name.to_string(),
19            ComputedMeta {
20                directive,
21                lookback,
22                valid_rows: self.height,
23                state: None,
24                origin: 0,
25            },
26        );
27    }
28
29    /// Attach (or replace) the carried recursive [`ComputedMeta::state`] for a
30    /// cached column, enabling an O(new-rows) append resume. No-op if `name` is
31    /// not a tracked computed column.
32    pub fn set_computed_state(&mut self, name: &str, state: Option<Vec<f64>>) {
33        if let Some(meta) = self.computed.get_mut(name) {
34            meta.state = state;
35        }
36    }
37
38    /// Whether any materialized directive column is stale (its `valid_rows` lags
39    /// `height` after an `append`), i.e. a bulk read would see NaN until `fulfill`.
40    pub fn has_stale_computed(&self) -> bool {
41        self.computed.values().any(|m| m.valid_rows < self.height)
42    }
43
44    /// Snapshot of the materialized-directive columns (`name`, meta).
45    pub fn computed_columns(&self) -> Vec<(String, ComputedMeta)> {
46        self.computed
47            .iter()
48            .map(|(k, v)| (k.clone(), v.clone()))
49            .collect()
50    }
51
52    /// Snapshot only stale materialized-directive columns. This keeps the live
53    /// append/fulfill path from cloning unrelated computed metadata.
54    pub fn stale_computed_columns(&self, only: Option<&str>) -> Vec<(String, ComputedMeta)> {
55        self.computed
56            .iter()
57            .filter(|(name, meta)| {
58                meta.valid_rows < self.height && only.is_none_or(|target| target == name.as_str())
59            })
60            .map(|(name, meta)| (name.clone(), meta.clone()))
61            .collect()
62    }
63
64    /// Names of all materialized-directive columns.
65    pub fn computed_names(&self) -> Vec<String> {
66        self.computed.keys().cloned().collect()
67    }
68
69    /// Overwrite a computed column's rows `[from, from + tail.len())` in place
70    /// (copy-on-write) with the recomputed `tail`, and mark it valid up to the
71    /// full height. Directive results are F64 or Bool, so both are handled.
72    /// O(tail.len()).
73    pub fn update_computed_tail(&mut self, name: &str, from: usize, tail: &Column) -> Result<()> {
74        let pos = self
75            .name_to_idx
76            .get(name)
77            .copied()
78            .ok_or_else(|| VolasError::ColumnNotFound(name.to_string()))?;
79        match (&mut self.columns[pos], tail) {
80            (Column::F64(arc), Column::F64(t)) => {
81                let buf = arc.make_mut();
82                for (i, &v) in t.iter().enumerate() {
83                    if from + i < buf.len() {
84                        buf[from + i] = v;
85                    }
86                }
87            }
88            (Column::Bool(arc, _), Column::Bool(t, _)) => {
89                let buf = arc.make_mut();
90                for (i, &v) in t.iter().enumerate() {
91                    if from + i < buf.len() {
92                        buf[from + i] = v;
93                    }
94                }
95            }
96            (col, t) => {
97                return Err(VolasError::DType(format!(
98                    "computed tail dtype {} does not match column \"{name}\" dtype {}",
99                    t.dtype(),
100                    col.dtype()
101                )))
102            }
103        }
104        if let Some(meta) = self.computed.get_mut(name) {
105            meta.valid_rows = self.height;
106        }
107        Ok(())
108    }
109
110    /// Overwrite one F64 computed value and mark the computed column current.
111    /// This avoids allocating a one-value tail column on the single-bar append path.
112    pub fn update_computed_f64_value(&mut self, name: &str, row: usize, value: f64) -> Result<()> {
113        let pos = self
114            .name_to_idx
115            .get(name)
116            .copied()
117            .ok_or_else(|| VolasError::ColumnNotFound(name.to_string()))?;
118        match &mut self.columns[pos] {
119            Column::F64(arc) => {
120                let buf = arc.make_mut();
121                if row < buf.len() {
122                    buf[row] = value;
123                }
124            }
125            col => {
126                return Err(VolasError::DType(format!(
127                    "computed scalar dtype F64 does not match column \"{name}\" dtype {}",
128                    col.dtype()
129                )))
130            }
131        }
132        if let Some(meta) = self.computed.get_mut(name) {
133            meta.valid_rows = self.height;
134        }
135        Ok(())
136    }
137
138    /// Drop the computed (cached-directive) status of the column at `col`, if any.
139    fn drop_computed_at(&mut self, col: usize) {
140        if let Some(name) = self.names.get(col) {
141            self.computed.remove(name);
142        }
143    }
144
145    /// A user write to column `col` invalidates the directive cache: `col` loses any
146    /// computed status, and every OTHER cached directive column is marked fully stale —
147    /// it may have been derived from `col`, so it is recomputed on next access (a bulk
148    /// read raises until `fulfill`, exactly like an append). Conservative (no per-column
149    /// dependency tracking), but writes are rare relative to reads.
150    pub(super) fn invalidate_computed_on_write_at(&mut self, col: usize) {
151        self.drop_computed_at(col);
152        for meta in self.computed.values_mut() {
153            meta.valid_rows = 0;
154            // The carried recursive state described row `valid_rows - 1`; resetting
155            // `valid_rows` to 0 breaks that correspondence, so drop it. A later refresh
156            // recomputes from scratch (correct) and repopulates the state.
157            meta.state = None;
158        }
159    }
160
161    /// Name-based variant for the `df[name] = value` whole-column replace path.
162    pub fn invalidate_computed_on_write(&mut self, written_name: &str) {
163        self.computed.remove(written_name);
164        for meta in self.computed.values_mut() {
165            meta.valid_rows = 0;
166            meta.state = None;
167        }
168    }
169}