1use std::collections::BTreeMap;
16
17use crate::error::{ArrayError, ArrayResult};
18use crate::schema::ArraySchema;
19use crate::tile::sparse_tile::{RowKind, SparseTile, SparseTileBuilder};
20use crate::types::cell_value::value::CellValue;
21use crate::types::coord::value::CoordValue;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum BinaryOp {
25 Add,
26 Sub,
27 Mul,
28 Div,
29}
30
31pub fn elementwise(
34 schema: &ArraySchema,
35 a: &SparseTile,
36 b: &SparseTile,
37 op: BinaryOp,
38) -> ArrayResult<SparseTile> {
39 let n_attrs = schema.attrs.len();
40 let by_coord_a = index_rows(a)?;
41 let by_coord_b = index_rows(b)?;
42 let mut keys: BTreeMap<Vec<CoordKey>, ()> = BTreeMap::new();
43 for k in by_coord_a.keys().chain(by_coord_b.keys()) {
44 keys.insert(k.clone(), ());
45 }
46
47 let mut builder = SparseTileBuilder::new(schema);
48 for key in keys.keys() {
49 let coord = decode_key(key);
50 let lhs = by_coord_a.get(key);
51 let rhs = by_coord_b.get(key);
52 let mut out = Vec::with_capacity(n_attrs);
53 for i in 0..n_attrs {
54 let l = lhs.map(|v| v[i].clone()).unwrap_or(CellValue::Null);
55 let r = rhs.map(|v| v[i].clone()).unwrap_or(CellValue::Null);
56 out.push(apply(&l, &r, op));
57 }
58 builder.push(&coord, &out)?;
59 }
60 Ok(builder.build())
61}
62
63fn apply(l: &CellValue, r: &CellValue, op: BinaryOp) -> CellValue {
64 let (lf, rf) = match (to_f64(l), to_f64(r)) {
65 (Some(a), Some(b)) => (a, b),
66 _ => return passthrough(l, r),
67 };
68 let v = match op {
69 BinaryOp::Add => lf + rf,
70 BinaryOp::Sub => lf - rf,
71 BinaryOp::Mul => lf * rf,
72 BinaryOp::Div => {
73 if rf == 0.0 {
74 return CellValue::Null;
75 }
76 lf / rf
77 }
78 };
79 if let (CellValue::Int64(_), CellValue::Int64(_)) = (l, r)
82 && v.fract() == 0.0
83 && v.is_finite()
84 {
85 return CellValue::Int64(v as i64);
86 }
87 CellValue::Float64(v)
88}
89
90fn passthrough(l: &CellValue, r: &CellValue) -> CellValue {
91 if !l.is_null() {
92 l.clone()
93 } else if !r.is_null() {
94 r.clone()
95 } else {
96 CellValue::Null
97 }
98}
99
100fn to_f64(v: &CellValue) -> Option<f64> {
101 match v {
102 CellValue::Int64(x) => Some(*x as f64),
103 CellValue::Float64(x) => Some(*x),
104 _ => None,
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
110enum CoordKey {
111 I(i64),
112 Ts(i64),
113 F(u64), S(String),
115}
116
117fn encode_key(c: &[CoordValue]) -> Vec<CoordKey> {
118 c.iter()
119 .map(|v| match v {
120 CoordValue::Int64(x) => CoordKey::I(*x),
121 CoordValue::TimestampMs(x) => CoordKey::Ts(*x),
122 CoordValue::Float64(x) => CoordKey::F(x.to_bits()),
123 CoordValue::String(s) => CoordKey::S(s.clone()),
124 })
125 .collect()
126}
127
128fn decode_key(k: &[CoordKey]) -> Vec<CoordValue> {
129 k.iter()
130 .map(|v| match v {
131 CoordKey::I(x) => CoordValue::Int64(*x),
132 CoordKey::Ts(x) => CoordValue::TimestampMs(*x),
133 CoordKey::F(b) => CoordValue::Float64(f64::from_bits(*b)),
134 CoordKey::S(s) => CoordValue::String(s.clone()),
135 })
136 .collect()
137}
138
139fn index_rows(tile: &SparseTile) -> ArrayResult<BTreeMap<Vec<CoordKey>, Vec<CellValue>>> {
142 let corrupt = |row: usize| ArrayError::SegmentCorruption {
143 detail: format!("elementwise: sparse tile row {row} is out of range"),
144 };
145 let mut out = BTreeMap::new();
146 let mut live_row = 0usize;
147 for row in 0..tile.row_count() {
148 if tile.row_kind(row)? != RowKind::Live {
149 continue;
150 }
151 let coord = tile
152 .dim_dicts
153 .iter()
154 .map(|d| {
155 d.indices
156 .get(row)
157 .and_then(|&i| d.values.get(i as usize))
158 .cloned()
159 .ok_or_else(|| corrupt(row))
160 })
161 .collect::<ArrayResult<Vec<CoordValue>>>()?;
162 let attrs = tile
163 .attr_cols
164 .iter()
165 .map(|col| col.get(live_row).cloned().ok_or_else(|| corrupt(row)))
166 .collect::<ArrayResult<Vec<CellValue>>>()?;
167 live_row += 1;
168 out.insert(encode_key(&coord), attrs);
169 }
170 Ok(out)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::schema::ArraySchemaBuilder;
177 use crate::schema::attr_spec::{AttrSpec, AttrType};
178 use crate::schema::dim_spec::{DimSpec, DimType};
179 use crate::types::domain::{Domain, DomainBound};
180
181 fn schema() -> ArraySchema {
182 ArraySchemaBuilder::new("g")
183 .dim(DimSpec::new(
184 "k",
185 DimType::Int64,
186 Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)),
187 ))
188 .attr(AttrSpec::new("v", AttrType::Int64, true))
189 .tile_extents(vec![16])
190 .build()
191 .unwrap()
192 }
193
194 fn tile(rows: &[(i64, i64)]) -> SparseTile {
195 let s = schema();
196 let mut b = SparseTileBuilder::new(&s);
197 for (k, v) in rows {
198 b.push(&[CoordValue::Int64(*k)], &[CellValue::Int64(*v)])
199 .unwrap();
200 }
201 b.build()
202 }
203
204 #[test]
205 fn add_aligned_cells() {
206 let s = schema();
207 let a = tile(&[(0, 1), (1, 2)]);
208 let b = tile(&[(0, 10), (1, 20)]);
209 let out = elementwise(&s, &a, &b, BinaryOp::Add).unwrap();
210 assert_eq!(out.nnz(), 2);
211 assert_eq!(out.attr_cols[0][0], CellValue::Int64(11));
212 assert_eq!(out.attr_cols[0][1], CellValue::Int64(22));
213 }
214
215 #[test]
216 fn outer_join_propagates_null() {
217 let s = schema();
218 let a = tile(&[(0, 1)]);
219 let b = tile(&[(1, 2)]);
220 let out = elementwise(&s, &a, &b, BinaryOp::Add).unwrap();
221 assert_eq!(out.nnz(), 2);
222 let v0 = &out.attr_cols[0][0];
225 let v1 = &out.attr_cols[0][1];
226 assert!(matches!(v0, CellValue::Int64(1) | CellValue::Int64(2)));
227 assert!(matches!(v1, CellValue::Int64(1) | CellValue::Int64(2)));
228 }
229
230 #[test]
231 fn div_by_zero_returns_null() {
232 let s = schema();
233 let a = tile(&[(0, 5)]);
234 let b = tile(&[(0, 0)]);
235 let out = elementwise(&s, &a, &b, BinaryOp::Div).unwrap();
236 assert_eq!(out.attr_cols[0][0], CellValue::Null);
237 }
238
239 #[test]
242 fn a_tombstone_row_is_skipped() {
243 use crate::tile::sparse_tile::SparseRow;
244 use nodedb_types::{OPEN_UPPER, Surrogate};
245
246 let s = schema();
247 let mut builder = SparseTileBuilder::new(&s);
248 builder
249 .push_row(SparseRow {
250 coord: &[CoordValue::Int64(0)],
251 attrs: &[],
252 surrogate: None,
253 valid_from_ms: 0,
254 valid_until_ms: OPEN_UPPER,
255 kind: RowKind::Tombstone,
256 })
257 .unwrap();
258 builder
259 .push_row(SparseRow {
260 coord: &[CoordValue::Int64(1)],
261 attrs: &[CellValue::Int64(4)],
262 surrogate: Some(Surrogate::new(1)),
263 valid_from_ms: 0,
264 valid_until_ms: OPEN_UPPER,
265 kind: RowKind::Live,
266 })
267 .unwrap();
268 let a = builder.build();
269 let b = tile(&[(1, 6)]);
270 let out = elementwise(&s, &a, &b, BinaryOp::Add).unwrap();
271 assert_eq!(out.nnz(), 1);
272 assert_eq!(out.attr_cols[0][0], CellValue::Int64(10));
273 }
274
275 #[test]
276 fn sub_and_mul() {
277 let s = schema();
278 let a = tile(&[(0, 10)]);
279 let b = tile(&[(0, 3)]);
280 let s1 = elementwise(&s, &a, &b, BinaryOp::Sub).unwrap();
281 let s2 = elementwise(&s, &a, &b, BinaryOp::Mul).unwrap();
282 assert_eq!(s1.attr_cols[0][0], CellValue::Int64(7));
283 assert_eq!(s2.attr_cols[0][0], CellValue::Int64(30));
284 }
285}