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}