nodedb_lite/engine/array/ops/
elementwise.rs1use std::collections::HashMap;
12use std::sync::Arc;
13
14use nodedb_array::query::elementwise::{BinaryOp, elementwise};
15use nodedb_array::tile::sparse_tile::SparseTile;
16use nodedb_array::types::TileId;
17use nodedb_array::{SegmentReader, TilePayload};
18use nodedb_physical::physical_plan::ArrayBinaryOp;
19use nodedb_types::result::QueryResult;
20use nodedb_types::value::Value;
21
22use crate::engine::array::engine::ArrayEngineState;
23use crate::engine::array::ops::util::cell::cell_value_to_value;
24use crate::error::LiteError;
25use crate::runtime::now_millis_i64;
26use crate::storage::engine::StorageEngine;
27
28fn map_binary_op(op: ArrayBinaryOp) -> BinaryOp {
29 match op {
30 ArrayBinaryOp::Add => BinaryOp::Add,
31 ArrayBinaryOp::Sub => BinaryOp::Sub,
32 ArrayBinaryOp::Mul => BinaryOp::Mul,
33 ArrayBinaryOp::Div => BinaryOp::Div,
34 }
35}
36
37async fn collect_tiles_for_array<S: StorageEngine>(
40 array_state: &Arc<tokio::sync::Mutex<ArrayEngineState>>,
41 storage: &Arc<S>,
42 name: &str,
43 system_as_of: i64,
44) -> Result<HashMap<u64, Vec<SparseTile>>, LiteError> {
45 let (seg_ids, schema) = {
46 let state = array_state.lock().await;
47 let arr = state
48 .arrays
49 .get(name)
50 .ok_or_else(|| LiteError::BadRequest {
51 detail: format!("array '{name}' not found"),
52 })?;
53 let seg_ids: Vec<u64> = arr.manifest.segments.iter().map(|s| s.id).collect();
54 (seg_ids, arr.schema.clone())
55 };
56
57 let mut prefix_tiles: HashMap<u64, Vec<SparseTile>> = HashMap::new();
58
59 for seg_id in &seg_ids {
60 let bytes = crate::engine::array::segments::load_segment(storage, name, *seg_id).await?;
61 let reader = SegmentReader::open(&bytes).map_err(|e| LiteError::Storage {
62 detail: format!("open segment {seg_id}: {e}"),
63 })?;
64 for idx in 0..reader.tile_count() {
65 let entry_tile_id: TileId = reader.tiles()[idx].tile_id;
66 if entry_tile_id.system_from_ms > system_as_of {
67 continue;
68 }
69 let payload = reader.read_tile(idx).map_err(|e| LiteError::Storage {
70 detail: format!("read_tile seg {seg_id} idx {idx}: {e}"),
71 })?;
72 if let TilePayload::Sparse(tile) = payload {
73 prefix_tiles
74 .entry(entry_tile_id.hilbert_prefix)
75 .or_default()
76 .push(tile);
77 }
78 }
79 }
80
81 {
83 let mut state = array_state.lock().await;
84 let arr = state
85 .arrays
86 .get_mut(name)
87 .ok_or_else(|| LiteError::BadRequest {
88 detail: format!("array '{name}' not found"),
89 })?;
90 let mem_tiles = arr
91 .memtable
92 .drain_all_tiles_read_only(system_as_of, &schema)
93 .map_err(|e| LiteError::Storage {
94 detail: format!("memtable drain '{name}': {e}"),
95 })?;
96 for (tile_id, tile) in mem_tiles {
97 prefix_tiles
98 .entry(tile_id.hilbert_prefix)
99 .or_default()
100 .push(tile);
101 }
102 }
103
104 Ok(prefix_tiles)
105}
106
107pub async fn elementwise_op<S: StorageEngine>(
109 array_state: &Arc<tokio::sync::Mutex<ArrayEngineState>>,
110 storage: &Arc<S>,
111 left_name: &str,
112 right_name: &str,
113 op: ArrayBinaryOp,
114) -> Result<QueryResult, LiteError> {
115 let system_as_of = now_millis_i64();
116 let binary_op = map_binary_op(op);
117
118 let schema = {
119 let state = array_state.lock().await;
120 let arr = state
121 .arrays
122 .get(left_name)
123 .ok_or_else(|| LiteError::BadRequest {
124 detail: format!("array '{left_name}' not found"),
125 })?;
126 arr.schema.clone()
127 };
128
129 let left_tiles = collect_tiles_for_array(array_state, storage, left_name, system_as_of).await?;
130 let right_tiles =
131 collect_tiles_for_array(array_state, storage, right_name, system_as_of).await?;
132
133 let mut all_prefixes: std::collections::HashSet<u64> = std::collections::HashSet::new();
135 all_prefixes.extend(left_tiles.keys().copied());
136 all_prefixes.extend(right_tiles.keys().copied());
137
138 let columns = vec![
139 "attrs".to_string(),
140 "valid_from_ms".to_string(),
141 "valid_until_ms".to_string(),
142 ];
143 let mut rows: Vec<Vec<Value>> = Vec::new();
144
145 let empty_tile = nodedb_array::tile::sparse_tile::SparseTileBuilder::new(&schema).build();
146
147 for prefix in all_prefixes {
148 let left_set = left_tiles.get(&prefix);
149 let right_set = right_tiles.get(&prefix);
150
151 let left_merged = merge_prefix_tiles(left_set, &schema, binary_op)?;
153 let right_merged = merge_prefix_tiles(right_set, &schema, binary_op)?;
154
155 let l = left_merged.as_ref().unwrap_or(&empty_tile);
156 let r = right_merged.as_ref().unwrap_or(&empty_tile);
157
158 let out = elementwise(&schema, l, r, binary_op).map_err(|e| LiteError::Storage {
159 detail: format!("elementwise prefix {prefix}: {e}"),
160 })?;
161
162 let n = out.nnz() as usize;
163 let attr_count = out.attr_cols.len();
164 for row in 0..n {
165 let attrs: Vec<Value> = (0..attr_count)
166 .map(|ai| {
167 out.attr_cols
168 .get(ai)
169 .and_then(|col| col.get(row))
170 .map(|cv| cell_value_to_value(cv.clone()))
171 .unwrap_or(Value::Null)
172 })
173 .collect();
174 rows.push(vec![
175 Value::Array(attrs),
176 Value::Integer(0),
177 Value::Integer(i64::MAX),
178 ]);
179 }
180 }
181
182 Ok(QueryResult {
183 columns,
184 rows,
185 rows_affected: 0,
186 command: None,
187 })
188}
189
190fn merge_prefix_tiles(
194 tiles: Option<&Vec<SparseTile>>,
195 schema: &nodedb_array::schema::ArraySchema,
196 op: BinaryOp,
197) -> Result<Option<SparseTile>, LiteError> {
198 let tiles = match tiles {
199 Some(t) if !t.is_empty() => t,
200 _ => return Ok(None),
201 };
202 let mut acc = tiles[0].clone();
203 for tile in &tiles[1..] {
204 acc = elementwise(schema, &acc, tile, op).map_err(|e| LiteError::Storage {
205 detail: format!("merge prefix tiles: {e}"),
206 })?;
207 }
208 Ok(Some(acc))
209}