1use std::collections::{BTreeMap, BTreeSet, HashMap};
40
41use improv_core_model::{CategoryId, Coordinate, ItemId, MeasureId, Model, Value, ValueType};
42use improv_engine::session::Edit;
43use improv_engine::{encode_coord, CellValue};
44
45use crate::pgoutput::{Datum, Message, Relation, Tuple, Txn};
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Agg {
50 Sum,
52 Avg,
54 Min,
56 Max,
58 Count,
60 CountDistinct,
62}
63
64impl Agg {
65 pub fn parse(s: &str) -> Option<Agg> {
67 Some(match s {
68 "sum" => Agg::Sum,
69 "avg" => Agg::Avg,
70 "min" => Agg::Min,
71 "max" => Agg::Max,
72 "count" => Agg::Count,
73 "count_distinct" => Agg::CountDistinct,
74 _ => return None,
75 })
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub struct Binding {
82 pub measure: MeasureId,
84 pub value_col: String,
86 pub dims: Vec<(CategoryId, String)>,
88 pub agg: Option<Agg>,
91}
92
93type Touched = BTreeMap<(MeasureId, Coordinate), Option<Value>>;
95
96type Items = HashMap<CategoryId, HashMap<String, ItemId>>;
98
99pub type FoldRow = (Vec<Option<String>>, Option<String>, i64, i64);
102
103#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct Snap {
107 xmin: u64,
108 xmax: u64,
109 xip: BTreeSet<u64>,
110}
111
112impl Snap {
113 pub fn parse(s: &str) -> Option<Snap> {
115 let mut it = s.split(':');
116 let xmin = it.next()?.parse().ok()?;
117 let xmax = it.next()?.parse().ok()?;
118 let xip = match it.next()? {
119 "" => BTreeSet::new(),
120 l => l
121 .split(',')
122 .map(|x| x.parse().ok())
123 .collect::<Option<_>>()?,
124 };
125 it.next().is_none().then_some(Snap { xmin, xmax, xip })
126 }
127
128 pub fn sees(&self, xid: u32) -> bool {
131 let d = i64::from(xid.wrapping_sub(self.xmax as u32) as i32);
132 let full = self.xmax as i64 + d;
133 full < self.xmin as i64 || (full < self.xmax as i64 && !self.xip.contains(&(full as u64)))
134 }
135}
136
137pub fn seed_sql(rel: &str, dim_cols: &[String], value_col: &str, agg: Agg) -> String {
145 let nulls = vec!["NULL"; dim_cols.len()].join(", ");
146 format!(
147 "({}) UNION ALL SELECT {nulls}, NULL, 0, 0, pg_current_snapshot()::text",
148 group_sql(rel, dim_cols, value_col, agg)
149 )
150}
151
152fn group_sql(rel: &str, dim_cols: &[String], value_col: &str, agg: Agg) -> String {
153 let dims: Vec<String> = dim_cols
154 .iter()
155 .map(|c| format!("{}::text", crate::quote_ident(c)))
156 .collect();
157 let by: Vec<String> = (1..=dims.len()).map(|i| i.to_string()).collect();
158 let (dims, by) = (dims.join(", "), by.join(", "));
159 let v = crate::quote_ident(value_col);
160 match agg {
161 Agg::Count => {
162 format!(
163 "SELECT {dims}, NULL::text, count({v}), count(*), NULL::text FROM {rel} \
164 GROUP BY {by}"
165 )
166 }
167 Agg::Sum | Agg::Avg => format!(
168 "SELECT {dims}, coalesce(improv_special, sum({v})::text), count({v}), count(*), \
169 NULL::text \
170 FROM (SELECT *, CASE WHEN {v}::text IN ('Infinity', '-Infinity', 'NaN') \
171 THEN {v}::text END AS improv_special FROM {rel}) t \
172 GROUP BY {by}, improv_special"
173 ),
174 Agg::Min | Agg::Max | Agg::CountDistinct => format!(
175 "SELECT {dims}, {v}::text, count({v}), count(*), NULL::text FROM {rel} \
176 GROUP BY {by}, {}",
177 dim_cols.len() + 1
178 ),
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct Rebuild(pub &'static str);
185
186#[derive(Debug, Clone, Default)]
188pub struct Mapper {
189 bindings: HashMap<u32, Vec<Binding>>,
191 items: Items,
193 folds: HashMap<MeasureId, HashMap<Coordinate, Fold>>,
195 seeded: HashMap<MeasureId, Snap>,
197}
198
199impl Mapper {
200 pub fn new(model: &Model, bindings: HashMap<u32, Vec<Binding>>) -> Self {
204 let mut items: Items = HashMap::new();
205 for i in model.items.values() {
206 items
207 .entry(i.category)
208 .or_default()
209 .insert(i.name.0.clone(), i.id);
210 }
211 Mapper {
212 bindings,
213 items,
214 folds: HashMap::new(),
215 seeded: HashMap::new(),
216 }
217 }
218
219 pub fn aggregated(&self) -> Vec<(u32, Binding)> {
222 let mut v: Vec<(u32, Binding)> = self
223 .bindings
224 .iter()
225 .flat_map(|(r, bs)| bs.iter().map(|b| (*r, b.clone())))
226 .filter(|(_, b)| b.agg.is_some())
227 .collect();
228 v.sort_by_key(|(_, b)| b.measure);
229 v
230 }
231
232 pub fn binds(&self, oid: u32) -> bool {
234 self.bindings.contains_key(&oid)
235 }
236
237 pub fn unknown(
242 &self,
243 rels: &HashMap<u32, Relation>,
244 txns: &[Txn],
245 ) -> Vec<(CategoryId, String)> {
246 let mut out = BTreeSet::new();
247 for change in txns.iter().flat_map(|t| &t.changes) {
248 let (rel, tuples) = match change {
249 Message::Insert { rel, new } => (rel, [None, Some(new)]),
250 Message::Update { rel, old, new } => (rel, [old.as_ref(), Some(new)]),
251 Message::Delete { rel, old } => (rel, [Some(old), None]),
252 _ => continue,
253 };
254 let (Some(bs), Some(r)) = (self.bindings.get(rel), rels.get(rel)) else {
255 continue;
256 };
257 for (b, t) in bs
258 .iter()
259 .flat_map(|b| tuples.iter().flatten().map(move |t| (b, t)))
260 {
261 for (cat, col) in &b.dims {
262 let Ok(Some(name)) = dim_name(r, col, t) else {
263 continue;
264 };
265 if !self.items.get(cat).is_some_and(|m| m.contains_key(&name)) {
266 out.insert((*cat, name));
267 }
268 }
269 }
270 }
271 out.into_iter().collect()
272 }
273
274 pub fn add_item(&mut self, cat: CategoryId, name: &str, id: ItemId) {
276 self.items
277 .entry(cat)
278 .or_default()
279 .insert(name.to_string(), id);
280 }
281
282 pub fn seed(
288 &mut self,
289 model: &mut Model,
290 measure: MeasureId,
291 oid: u32,
292 rows: &[FoldRow],
293 snap: Snap,
294 ) -> Result<(), Rebuild> {
295 let b = self
296 .bindings
297 .values()
298 .flatten()
299 .find(|b| b.measure == measure)
300 .ok_or(Rebuild("unknown binding"))?;
301 let agg = b.agg.ok_or(Rebuild("not an aggregated binding"))?;
302 let vt = value_type(model, measure)?;
303 if unfoldable(agg, vt) {
304 return Ok(());
305 }
306 self.seeded.insert(measure, snap);
307 let folds = self.folds.entry(measure).or_default();
308 for (dims, v, n, all) in rows {
309 let names: Vec<Option<&str>> = dims.iter().map(Option::as_deref).collect();
310 let Some(c) = coord_of(&self.items, b, &names)? else {
311 continue;
312 };
313 let f = folds.entry(c).or_default();
314 f.rows += all - n;
315 let ok = match v {
316 None => f.add(&Part::Count, *n),
317 Some(s) => match part_of(agg, vt, oid, s)? {
318 Part::Total(x) if x.is_finite() => {
320 f.add(&Part::Total(x), 1) && f.add(&Part::Count, n - 1)
321 }
322 p => f.add(&p, *n),
323 },
324 };
325 if !ok {
326 return Err(Rebuild("a fold went out of step"));
327 }
328 }
329 model.inputs.retain(|(m, _), _| *m != measure);
330 for (c, f) in folds.iter() {
331 if let Some(v) = f.cell(agg, oid) {
332 model.set_input(measure, c.clone(), v);
333 }
334 }
335 Ok(())
336 }
337
338 pub fn map(
347 &mut self,
348 model: &mut Model,
349 rels: &HashMap<u32, Relation>,
350 txns: &[Txn],
351 replay_until: u64,
352 ) -> Result<Vec<Edit>, Rebuild> {
353 let mut touched = BTreeMap::new();
355 for txn in txns {
356 let replay = txn.end_lsn <= replay_until;
357 for change in &txn.changes {
358 self.change(model, rels, change, (replay, txn.xid), &mut touched)?;
359 }
360 }
361 let cell = |v: Option<&Value>| v.and_then(CellValue::from_model_value);
363 Ok(touched
364 .into_iter()
365 .filter_map(|((measure, c), before): (_, Option<Value>)| {
366 let now = cell(model.inputs.get(&(measure, c.clone())));
367 (now != cell(before.as_ref())).then(|| Edit {
368 value: now,
369 coord: encode_coord(&c),
370 measure,
371 })
372 })
373 .collect())
374 }
375
376 fn change(
377 &mut self,
378 model: &mut Model,
379 rels: &HashMap<u32, Relation>,
380 change: &Message,
381 (replay, xid): (bool, u32),
382 touched: &mut Touched,
383 ) -> Result<(), Rebuild> {
384 let (rel, old, new) = match change {
387 Message::Insert { rel, new } => (*rel, None, Some(new)),
388 Message::Update { rel, old, new } => (*rel, Some(old.as_ref()), Some(new)),
389 Message::Delete { rel, old } => (*rel, Some(Some(old)), None),
390 Message::Truncate { rels, .. } => {
391 for r in rels {
392 self.truncate(model, *r, (replay, xid), touched)?;
393 }
394 return Ok(());
395 }
396 _ => return Ok(()),
397 };
398 let Some(bindings) = self.bindings.get(&rel) else {
399 return Ok(());
400 };
401 let r = rels.get(&rel).ok_or(Rebuild("undescribed relation"))?;
402 for b in bindings {
403 if b.agg.is_none() {
404 row(&self.items, model, r, b, (old, new), replay, touched)?;
405 } else if !self.loaded(model, b, xid)? {
406 let folds = self.folds.entry(b.measure).or_default();
407 fold_row(&self.items, folds, model, r, b, (old, new), touched)?;
408 }
409 }
410 Ok(())
411 }
412
413 fn loaded(&self, model: &Model, b: &Binding, xid: u32) -> Result<bool, Rebuild> {
416 match self.seeded.get(&b.measure) {
417 Some(s) => Ok(s.sees(xid)),
418 None if b.agg.is_some_and(|a| unfoldable(a, vt_of(model, b))) => {
419 Err(Rebuild("min or max of text"))
420 }
421 None => Err(Rebuild("fold not seeded")),
422 }
423 }
424
425 fn truncate(
428 &mut self,
429 model: &mut Model,
430 rel: u32,
431 (replay, xid): (bool, u32),
432 touched: &mut Touched,
433 ) -> Result<(), Rebuild> {
434 for b in self.bindings.get(&rel).into_iter().flatten() {
435 if b.agg.is_some() {
436 if self.loaded(model, b, xid)? {
437 continue;
438 }
439 self.folds.remove(&b.measure);
440 } else if replay {
441 return Err(Rebuild("truncate"));
444 }
445 let gone: Vec<(MeasureId, Coordinate)> = model
446 .inputs
447 .keys()
448 .filter(|(m, _)| *m == b.measure)
449 .cloned()
450 .collect();
451 for k in gone {
452 let before = model.inputs.remove(&k);
453 touched.entry(k).or_insert(before);
454 }
455 }
456 Ok(())
457 }
458}
459
460fn vt_of(model: &Model, b: &Binding) -> ValueType {
461 model
462 .measures
463 .get(&b.measure)
464 .map_or(ValueType::Number, |m| m.value_type)
465}
466
467fn value_type(model: &Model, measure: MeasureId) -> Result<ValueType, Rebuild> {
468 Ok(model
469 .measures
470 .get(&measure)
471 .ok_or(Rebuild("unknown measure"))?
472 .value_type)
473}
474
475fn row(
478 items: &Items,
479 model: &mut Model,
480 r: &Relation,
481 b: &Binding,
482 (old, new): (Option<Option<&Tuple>>, Option<&Tuple>),
483 replay: bool,
484 touched: &mut Touched,
485) -> Result<(), Rebuild> {
486 let vt = value_type(model, b.measure)?;
487 let keyed = || {
492 let keys: BTreeSet<&str> = r
493 .columns
494 .iter()
495 .filter(|c| c.key)
496 .map(|c| c.name.as_str())
497 .collect();
498 keys == b.dims.iter().map(|(_, c)| c.as_str()).collect()
499 };
500 let old = match old {
501 None => None,
502 Some(Some(t)) if r.replica_identity == b'f' => Some((t, true)),
503 Some(t) if r.replica_identity != b'f' && keyed() => {
504 Some((t.or(new).ok_or(Rebuild("no row"))?, false))
505 }
506 Some(_) => return Err(Rebuild("old row not replicated")),
507 };
508 if let Some((t, value_known)) = old {
509 if let Some(c) = coord(items, r, b, t)? {
510 if !value_known || value(r, b, t, vt)?.is_some() {
513 let k = (b.measure, c);
514 let before = model.inputs.remove(&k);
515 touched.entry(k).or_insert(before);
516 }
517 }
518 }
519 if let Some(t) = new {
520 if let (Some(c), Some(v)) = (coord(items, r, b, t)?, value(r, b, t, vt)?) {
521 let k = (b.measure, c);
522 if !replay && model.inputs.contains_key(&k) {
523 return Err(Rebuild("two rows on one coordinate"));
524 }
525 let before = model.inputs.insert(k.clone(), v);
526 touched.entry(k).or_insert(before);
527 }
528 }
529 Ok(())
530}
531
532fn fold_row(
536 items: &Items,
537 folds: &mut HashMap<Coordinate, Fold>,
538 model: &mut Model,
539 r: &Relation,
540 b: &Binding,
541 (old, new): (Option<Option<&Tuple>>, Option<&Tuple>),
542 touched: &mut Touched,
543) -> Result<(), Rebuild> {
544 let agg = b.agg.ok_or(Rebuild("not an aggregated binding"))?;
545 let vt = value_type(model, b.measure)?;
546 let old = match old {
549 None => None,
550 Some(Some(t)) if r.replica_identity == b'f' => Some(t),
551 Some(_) => return Err(Rebuild("old row not replicated")),
552 };
553 for (t, k) in [(old, -1), (new, 1)] {
554 let Some(t) = t else { continue };
555 let Some(c) = coord(items, r, b, t)? else {
556 continue;
557 };
558 let p = part(agg, r, b, t, vt)?;
559 let f = folds.entry(c.clone()).or_default();
560 if !f.add(&p, k) {
561 return Err(Rebuild("a fold went out of step"));
562 }
563 let cell = f.cell(agg, value_oid(r, b));
564 if f.rows == 0 {
565 folds.remove(&c);
566 }
567 let key = (b.measure, c);
568 let before = match cell {
569 Some(v) => model.inputs.insert(key.clone(), v),
570 None => model.inputs.remove(&key),
571 };
572 touched.entry(key).or_insert(before);
573 }
574 Ok(())
575}
576
577fn unfoldable(agg: Agg, vt: ValueType) -> bool {
582 matches!(agg, Agg::Min | Agg::Max) && vt == ValueType::Text
583}
584
585fn coord(
588 items: &Items,
589 r: &Relation,
590 b: &Binding,
591 t: &Tuple,
592) -> Result<Option<Coordinate>, Rebuild> {
593 let mut names = Vec::with_capacity(b.dims.len());
594 for (_, col) in &b.dims {
595 names.push(dim_name(r, col, t)?);
596 }
597 let names: Vec<Option<&str>> = names.iter().map(Option::as_deref).collect();
598 coord_of(items, b, &names)
599}
600
601fn dim_name(r: &Relation, col: &str, t: &Tuple) -> Result<Option<String>, Rebuild> {
606 let s = match r.get(t, col) {
607 None => return Err(Rebuild("bound column missing")),
608 Some(Datum::Null) => return Ok(None),
609 Some(Datum::Unchanged) => return Err(Rebuild("unchanged TOAST value")),
610 Some(Datum::Text(s)) => s,
611 };
612 let oid = r
613 .columns
614 .iter()
615 .find(|c| c.name == col)
616 .map_or(0, |c| c.type_oid);
617 Ok(Some(match (oid, s.as_str()) {
618 (BOOL, "t") => "true".into(),
619 (BOOL, "f") => "false".into(),
620 (BPCHAR, s) => s.trim_end_matches(' ').into(),
621 (_, s) => s.into(),
622 }))
623}
624
625fn coord_of(
628 items: &Items,
629 b: &Binding,
630 names: &[Option<&str>],
631) -> Result<Option<Coordinate>, Rebuild> {
632 let mut pairs = Vec::with_capacity(b.dims.len());
633 for ((cat, _), name) in b.dims.iter().zip(names) {
634 let Some(name) = name else { return Ok(None) };
635 let item = items
636 .get(cat)
637 .and_then(|m| m.get(*name))
638 .ok_or(Rebuild("new item"))?;
639 pairs.push((*cat, *item));
640 }
641 Ok(Some(Coordinate::from_pairs(pairs)))
642}
643
644const BPCHAR: u32 = 1042;
646const BOOL: u32 = 16;
647const FLOAT4: u32 = 700;
648const FLOAT8: u32 = 701;
649const NUMERIC: u32 = 1700;
650
651fn text<'t>(r: &Relation, b: &Binding, t: &'t Tuple) -> Result<Option<&'t str>, Rebuild> {
653 match r.get(t, &b.value_col) {
654 None => Err(Rebuild("bound column missing")),
655 Some(Datum::Null) => Ok(None),
656 Some(Datum::Unchanged) => Err(Rebuild("unchanged TOAST value")),
657 Some(Datum::Text(s)) => Ok(Some(s)),
658 }
659}
660
661fn value_oid(r: &Relation, b: &Binding) -> u32 {
663 r.columns
664 .iter()
665 .find(|c| c.name == b.value_col)
666 .map_or(0, |c| c.type_oid)
667}
668
669fn value(r: &Relation, b: &Binding, t: &Tuple, vt: ValueType) -> Result<Option<Value>, Rebuild> {
671 let Some(s) = text(r, b, t)? else {
672 return Ok(None);
673 };
674 parse(vt, s, value_oid(r, b) == BPCHAR).map(Some)
675}
676
677fn parse(vt: ValueType, s: &str, bpchar: bool) -> Result<Value, Rebuild> {
679 let v = match vt {
680 ValueType::Number => s.parse().ok().map(Value::Number),
681 ValueType::Boolean => match s {
683 "t" | "true" => Some(Value::Boolean(true)),
684 "f" | "false" => Some(Value::Boolean(false)),
685 _ => None,
686 },
687 ValueType::Text => Some(Value::Text(
688 if bpchar { s.trim_end_matches(' ') } else { s }.to_string(),
689 )),
690 ValueType::DateTime => datetime(s).map(Value::DateTime),
691 ValueType::Enum => None,
692 };
693 v.ok_or(Rebuild("value the loader would not read"))
694}
695
696fn datetime(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
701 use chrono::{DateTime, NaiveDate, NaiveDateTime};
702 let t = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%#z")
703 .map(|d| d.naive_utc())
704 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f"))
705 .or_else(|_| {
706 NaiveDate::parse_from_str(s, "%Y-%m-%d").map(|d| d.and_time(Default::default()))
707 })
708 .ok()?
709 .and_utc();
710 let us = t.timestamp_micros();
711 let ms = us / 1000 + (us % 1000) / 500;
712 DateTime::from_timestamp_millis(ms)
713}
714
715#[derive(Debug, Clone, Default, PartialEq)]
717struct Fold {
718 rows: i64,
721 n: i64,
723 total: Total,
725 values: BTreeMap<Key, i64>,
728}
729
730#[derive(Debug, Clone, PartialEq)]
732enum Part {
733 Null,
735 Count,
737 Total(Num),
739 Key(Key),
741}
742
743impl Fold {
744 fn add(&mut self, p: &Part, k: i64) -> bool {
747 self.rows += k;
748 match p {
749 Part::Null => return self.rows >= 0,
750 Part::Count => {}
751 Part::Total(x) => self.total.add(*x, k),
752 Part::Key(key) => {
753 let c = self.values.entry(key.clone()).or_default();
754 *c += k;
755 let now = *c;
756 if now == 0 {
757 self.values.remove(key);
758 } else if now < 0 {
759 return false;
760 }
761 }
762 }
763 self.n += k;
764 self.rows >= 0 && self.n >= 0
765 }
766
767 fn cell(&self, agg: Agg, oid: u32) -> Option<Value> {
770 if self.rows <= 0 {
771 return None;
772 }
773 let num = |x: f64| Some(Value::Number(x));
774 match agg {
775 Agg::Count => num(self.n as f64),
776 Agg::CountDistinct => num(self.values.len() as f64),
777 _ if self.n <= 0 => None,
779 Agg::Sum => num(self.total.value()),
780 Agg::Avg => num(self.total.avg(self.n, matches!(oid, FLOAT4 | FLOAT8))),
781 Agg::Min => self.values.keys().next().and_then(Key::value),
782 Agg::Max => self.values.keys().next_back().and_then(Key::value),
783 }
784 }
785}
786
787fn part_of(agg: Agg, vt: ValueType, oid: u32, s: &str) -> Result<Part, Rebuild> {
790 Ok(match agg {
791 Agg::Count => Part::Count,
792 Agg::CountDistinct => Part::Key(Key::Text(distinct_text(oid, s))),
793 Agg::Sum | Agg::Avg => Part::Total(Num::parse(s)?),
794 Agg::Min | Agg::Max => Part::Key(Key::order(parse(vt, s, oid == BPCHAR)?)?),
795 })
796}
797
798fn part(agg: Agg, r: &Relation, b: &Binding, t: &Tuple, vt: ValueType) -> Result<Part, Rebuild> {
800 match text(r, b, t)? {
801 None => Ok(Part::Null),
802 Some(s) => part_of(agg, vt, value_oid(r, b), s),
803 }
804}
805
806fn distinct_text(oid: u32, s: &str) -> String {
814 match oid {
815 BPCHAR => s.trim_end_matches(' ').to_string(),
816 BOOL => s.get(..1).unwrap_or(s).to_string(),
817 NUMERIC if s.contains('.') => s.trim_end_matches('0').trim_end_matches('.').to_string(),
818 FLOAT4 | FLOAT8 if s == "-0" => "0".to_string(),
819 _ => s.to_string(),
820 }
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
825enum Key {
826 Num(i64),
828 Bool(bool),
829 Date(i64),
831 Text(String),
833}
834
835impl Key {
836 fn order(v: Value) -> Result<Key, Rebuild> {
838 Ok(match v {
839 Value::Number(x) => Key::Num(ord_bits(x + 0.0)),
841 Value::Boolean(b) => Key::Bool(b),
842 Value::DateTime(d) => Key::Date(d.timestamp_millis()),
843 _ => return Err(Rebuild("min or max of text")),
845 })
846 }
847
848 fn value(&self) -> Option<Value> {
849 Some(match self {
850 Key::Num(k) => Value::Number(f64::from_bits(ord_bits_inverse(*k))),
851 Key::Bool(b) => Value::Boolean(*b),
852 Key::Date(ms) => Value::DateTime(chrono::DateTime::from_timestamp_millis(*ms)?),
853 Key::Text(s) => Value::Text(s.clone()),
854 })
855 }
856}
857
858fn ord_bits(x: f64) -> i64 {
860 let b = x.to_bits() as i64;
861 b ^ ((((b >> 63) as u64) >> 1) as i64)
862}
863
864fn ord_bits_inverse(k: i64) -> u64 {
866 (k ^ ((((k >> 63) as u64) >> 1) as i64)) as u64
867}
868
869#[derive(Debug, Clone, Copy, PartialEq)]
872enum Num {
873 Nanos(i128, u8),
875 Float(f64),
876}
877
878const NANOS: i128 = 1_000_000_000;
879
880impl Num {
881 fn is_finite(self) -> bool {
882 match self {
883 Num::Nanos(..) => true,
884 Num::Float(f) => f.is_finite(),
885 }
886 }
887
888 fn parse(s: &str) -> Result<Num, Rebuild> {
889 if let Some(n) = nanos(s) {
890 let scale = s.split_once('.').map_or(0, |(_, f)| f.len());
891 return Ok(Num::Nanos(n, scale as u8));
892 }
893 s.parse()
894 .map(Num::Float)
895 .map_err(|_| Rebuild("value the loader would not read"))
896 }
897}
898
899fn nanos(s: &str) -> Option<i128> {
902 let (neg, body) = match s.strip_prefix('-') {
903 Some(b) => (true, b),
904 None => (false, s.strip_prefix('+').unwrap_or(s)),
905 };
906 let (int, frac) = body.split_once('.').unwrap_or((body, ""));
907 let digits = |d: &str| d.bytes().all(|c| c.is_ascii_digit());
908 if (int.is_empty() && frac.is_empty())
909 || int.len() > 20
910 || frac.len() > 9
911 || !digits(int)
912 || !digits(frac)
913 {
914 return None;
915 }
916 let int: i128 = if int.is_empty() { 0 } else { int.parse().ok()? };
917 let x = int * NANOS + format!("{frac:0<9}").parse::<i128>().ok()?;
918 Some(if neg { -x } else { x })
919}
920
921fn numeric_div(nanos: i128, scale: i32, n: i64) -> f64 {
927 fn weight(int: i128, frac9: i128) -> (i32, i128) {
930 if int > 0 {
931 let w = (int.ilog10() / 4) as i32;
932 (w, int / 10_000i128.pow(w as u32))
933 } else {
934 let f12 = frac9 * 1000;
935 [
936 (-1, f12 / 100_000_000),
937 (-2, f12 / 10_000 % 10_000),
938 (-3, f12 % 10_000),
939 ]
940 .into_iter()
941 .find(|(_, g)| *g > 0)
942 .unwrap_or((0, 0))
943 }
944 }
945 let a = nanos.unsigned_abs() as i128;
946 let (w1, d1) = weight(a / NANOS, a % NANOS);
947 let (w2, d2) = weight(i128::from(n), 0);
948 let qweight = w1 - w2 - i32::from(d1 <= d2);
949 let rscale = (16 - 4 * qweight).max(scale).clamp(0, 1000);
950 let d = NANOS * i128::from(n);
951 let (mut int, mut rem) = (a / d, a % d);
952 let mut digits = Vec::with_capacity(rscale as usize);
953 for _ in 0..rscale {
954 rem *= 10;
955 digits.push((rem / d) as u8);
956 rem %= d;
957 }
958 if rem * 10 / d >= 5 {
959 let mut carry = true;
960 for x in digits.iter_mut().rev() {
961 *x = (*x + 1) % 10;
962 if *x != 0 {
963 carry = false;
964 break;
965 }
966 }
967 if carry {
968 int += 1;
969 }
970 }
971 let frac: String = digits.iter().map(|x| char::from(b'0' + x)).collect();
974 let sign = if nanos < 0 { "-" } else { "" };
975 format!("{sign}{int}.{frac}0").parse().unwrap_or(f64::NAN)
976}
977
978#[derive(Debug, Clone, Default, PartialEq)]
988struct Total {
989 nanos: i128,
990 scales: [i64; 10],
996 float: f64,
997 floats: i64,
999 special: [i64; 3],
1001}
1002
1003impl Total {
1004 fn add(&mut self, x: Num, k: i64) {
1005 match x {
1006 Num::Nanos(n, scale) => {
1007 self.nanos += n * i128::from(k);
1008 self.scales[usize::from(scale)] += k;
1009 }
1010 Num::Float(f) if f.is_nan() => self.special[2] += k,
1011 Num::Float(f) if f == f64::INFINITY => self.special[0] += k,
1012 Num::Float(f) if f == f64::NEG_INFINITY => self.special[1] += k,
1013 Num::Float(f) => {
1014 self.float += f * k as f64;
1015 self.floats += k;
1016 }
1017 }
1018 }
1019
1020 fn avg(&self, n: i64, float: bool) -> f64 {
1023 let special = self.special.iter().any(|k| *k > 0);
1024 if float || special || self.floats > 0 {
1025 return self.value() / n as f64;
1026 }
1027 let scale = self.scales.iter().rposition(|k| *k > 0).unwrap_or(0);
1028 numeric_div(self.nanos, scale as i32, n)
1029 }
1030
1031 fn value(&self) -> f64 {
1032 match self.special {
1033 [_, _, nan] if nan > 0 => f64::NAN,
1034 [p, m, _] if p > 0 && m > 0 => f64::NAN,
1035 [p, _, _] if p > 0 => f64::INFINITY,
1036 [_, m, _] if m > 0 => f64::NEG_INFINITY,
1037 _ => {
1038 let (q, r) = (self.nanos / NANOS, (self.nanos % NANOS).abs());
1039 let sign = if self.nanos < 0 && q == 0 { "-" } else { "" };
1040 let exact: f64 = format!("{sign}{q}.{r:09}").parse().unwrap_or(f64::NAN);
1042 exact + self.float
1043 }
1044 }
1045 }
1046}
1047
1048#[cfg(test)]
1049#[cfg_attr(coverage_nightly, coverage(off))]
1050mod tests {
1051 use super::*;
1052 use crate::pgoutput::Column;
1053 use crate::{build_model, Catalog, CategoryRow, ItemRow, MeasureRow};
1054 use improv_engine::CoordKey;
1055
1056 const F: u32 = 7;
1057 const OTHER: u32 = 8;
1058
1059 fn item(id: u32, category_id: u32, name: &str) -> ItemRow {
1060 ItemRow {
1061 id,
1062 category_id,
1063 name: name.into(),
1064 }
1065 }
1066
1067 fn catalog() -> Catalog {
1069 Catalog {
1070 categories: vec![
1071 CategoryRow {
1072 id: 1,
1073 name: "Time".into(),
1074 },
1075 CategoryRow {
1076 id: 2,
1077 name: "Product".into(),
1078 },
1079 ],
1080 items: vec![item(11, 1, "Q1"), item(12, 1, "Q2"), item(21, 2, "P1")],
1081 measures: vec![MeasureRow::input(5, "Price", vec![1, 2])],
1082 lambdas: vec![],
1083 }
1084 }
1085
1086 fn model() -> Model {
1088 let mut m = build_model(&catalog()).unwrap();
1089 m.set_input(MeasureId(5), coordinate(11, 21), Value::Number(1.0));
1090 m
1091 }
1092
1093 fn coordinate(q: u32, p: u32) -> Coordinate {
1094 Coordinate::from_pairs([(CategoryId(1), ItemId(q)), (CategoryId(2), ItemId(p))])
1095 }
1096
1097 fn key(q: u32, p: u32) -> CoordKey {
1098 vec![(1, q), (2, p)]
1099 }
1100
1101 fn rels(identity: u8, keys: &[&str]) -> HashMap<u32, Relation> {
1103 let col = |n: &str| Column {
1104 key: keys.contains(&n),
1105 name: n.into(),
1106 type_oid: 25,
1107 type_mod: -1,
1108 };
1109 let f = Relation {
1110 id: F,
1111 namespace: "public".into(),
1112 name: "f".into(),
1113 replica_identity: identity,
1114 columns: ["quarter", "product", "price", "note"]
1115 .into_iter()
1116 .map(col)
1117 .collect(),
1118 };
1119 HashMap::from([(F, f)])
1120 }
1121
1122 fn mapper(m: &Model, agg: Option<Agg>) -> Mapper {
1123 let b = Binding {
1124 measure: MeasureId(5),
1125 value_col: "price".into(),
1126 dims: vec![
1127 (CategoryId(1), "quarter".into()),
1128 (CategoryId(2), "product".into()),
1129 ],
1130 agg,
1131 };
1132 Mapper::new(m, HashMap::from([(F, vec![b])]))
1133 }
1134
1135 fn row(q: Option<&str>, p: Option<&str>, v: Option<&str>) -> Tuple {
1136 [q, p, v, Some("x")]
1137 .into_iter()
1138 .map(|d| d.map_or(Datum::Null, |s| Datum::Text(s.into())))
1139 .collect()
1140 }
1141
1142 fn full(q: &str, v: &str) -> Tuple {
1143 row(Some(q), Some("P1"), Some(v))
1144 }
1145
1146 type Out = Result<Vec<(CoordKey, Option<f64>)>, Rebuild>;
1147
1148 fn run(changes: Vec<Message>) -> Out {
1150 run_with(rels(b'f', &[]), None, changes)
1151 }
1152
1153 fn txn(changes: Vec<Message>) -> Vec<Txn> {
1154 vec![Txn {
1155 xid: 1,
1156 end_lsn: 10,
1157 changes,
1158 }]
1159 }
1160
1161 fn run_with(rels: HashMap<u32, Relation>, agg: Option<Agg>, changes: Vec<Message>) -> Out {
1162 let mut m = model();
1163 let edits = mapper(&m, agg).map(&mut m, &rels, &txn(changes), 0)?;
1164 assert!(edits.iter().all(|e| e.measure == MeasureId(5)));
1165 Ok(edits
1166 .into_iter()
1167 .map(|e| (e.coord, e.value.map(|v| v.as_num().expect("a number"))))
1168 .collect())
1169 }
1170
1171 fn ins(t: Tuple) -> Message {
1172 Message::Insert { rel: F, new: t }
1173 }
1174
1175 fn upd(old: Option<Tuple>, new: Tuple) -> Message {
1176 Message::Update { rel: F, old, new }
1177 }
1178
1179 fn del(t: Tuple) -> Message {
1180 Message::Delete { rel: F, old: t }
1181 }
1182
1183 #[test]
1184 fn insert_update_delete_map_to_set_and_remove() {
1185 assert_eq!(
1186 run(vec![ins(full("Q2", "3"))]),
1187 Ok(vec![(key(12, 21), Some(3.0))])
1188 );
1189 assert_eq!(
1190 run(vec![upd(Some(full("Q1", "1")), full("Q1", "4.5"))]),
1191 Ok(vec![(key(11, 21), Some(4.5))])
1192 );
1193 assert_eq!(
1194 run(vec![del(full("Q1", "1"))]),
1195 Ok(vec![(key(11, 21), None)])
1196 );
1197 }
1198
1199 #[test]
1200 fn a_dimension_change_moves_the_cell() {
1201 assert_eq!(
1202 run(vec![upd(Some(full("Q1", "1")), full("Q2", "1"))]),
1203 Ok(vec![(key(11, 21), None), (key(12, 21), Some(1.0))])
1204 );
1205 }
1206
1207 #[test]
1208 fn a_batch_yields_each_cells_final_value() {
1209 assert_eq!(
1210 run(vec![
1211 ins(full("Q2", "3")),
1212 upd(Some(full("Q2", "3")), full("Q2", "5")),
1213 del(full("Q1", "1")),
1214 ins(full("Q1", "2")),
1215 ]),
1216 Ok(vec![(key(11, 21), Some(2.0)), (key(12, 21), Some(5.0))])
1217 );
1218 assert_eq!(
1220 run(vec![ins(full("Q2", "3")), del(full("Q2", "3"))]),
1221 Ok(vec![])
1222 );
1223 assert_eq!(
1224 run(vec![
1225 upd(Some(full("Q1", "1")), full("Q1", "2")),
1226 upd(Some(full("Q1", "2")), full("Q1", "1")),
1227 ]),
1228 Ok(vec![])
1229 );
1230 }
1231
1232 #[test]
1233 fn nulls_follow_the_loader() {
1234 assert_eq!(run(vec![ins(row(None, Some("P1"), Some("3")))]), Ok(vec![]));
1236 assert_eq!(
1237 run(vec![ins(row(Some("Q2"), Some("P1"), None))]),
1238 Ok(vec![])
1239 );
1240 assert_eq!(
1241 run(vec![upd(
1242 Some(full("Q1", "1")),
1243 row(Some("Q1"), Some("P1"), None)
1244 )]),
1245 Ok(vec![(key(11, 21), None)])
1246 );
1247 assert_eq!(
1248 run(vec![upd(
1249 Some(full("Q1", "1")),
1250 row(None, Some("P1"), Some("1"))
1251 )]),
1252 Ok(vec![(key(11, 21), None)])
1253 );
1254 assert_eq!(
1256 run(vec![del(row(Some("Q1"), Some("P1"), None))]),
1257 Ok(vec![])
1258 );
1259 assert_eq!(
1260 run(vec![upd(
1261 Some(row(Some("Q2"), Some("P1"), None)),
1262 full("Q2", "7")
1263 )]),
1264 Ok(vec![(key(12, 21), Some(7.0))])
1265 );
1266 }
1267
1268 #[test]
1269 fn unbound_relations_and_columns_are_ignored() {
1270 let mut m = model();
1271 let mut r = rels(b'f', &[]);
1272 let mut other = r[&F].clone();
1273 other.id = OTHER;
1274 r.insert(OTHER, other);
1275 let changes = txn(vec![
1276 Message::Insert {
1277 rel: OTHER,
1278 new: full("Q2", "3"),
1279 },
1280 Message::Truncate {
1281 options: 0,
1282 rels: vec![OTHER],
1283 },
1284 upd(Some(full("Q1", "1")), full("Q1", "1")),
1286 ]);
1287 let edits = mapper(&m, None).map(&mut m, &r, &changes, 0).unwrap();
1288 assert!(edits.is_empty());
1289 }
1290
1291 #[test]
1292 fn what_cannot_be_applied_cell_by_cell_rebuilds() {
1293 assert_eq!(run(vec![ins(full("Q3", "1"))]), Err(Rebuild("new item")));
1294 assert_eq!(
1295 run(vec![ins(full("Q1", "2"))]),
1296 Err(Rebuild("two rows on one coordinate"))
1297 );
1298 assert_eq!(
1299 run(vec![ins(full("Q2", "abc"))]),
1300 Err(Rebuild("value the loader would not read"))
1301 );
1302 let mut t = full("Q2", "1");
1303 t[2] = Datum::Unchanged;
1304 assert_eq!(run(vec![ins(t)]), Err(Rebuild("unchanged TOAST value")));
1305 let mut no_price = rels(b'f', &[]);
1306 no_price.get_mut(&F).unwrap().columns[2].name = "cost".into();
1307 assert_eq!(
1308 run_with(no_price, None, vec![ins(full("Q2", "1"))]),
1309 Err(Rebuild("bound column missing"))
1310 );
1311 let mut m = model();
1312 let changes = txn(vec![ins(full("Q2", "1"))]);
1313 assert_eq!(
1314 mapper(&m, None)
1315 .map(&mut m, &HashMap::new(), &changes, 0)
1316 .err(),
1317 Some(Rebuild("undescribed relation"))
1318 );
1319 let mut gone = model();
1320 let mut m = mapper(&gone, None);
1321 gone.measures.clear();
1322 assert_eq!(
1323 m.map(&mut gone, &rels(b'f', &[]), &changes, 0).err(),
1324 Some(Rebuild("unknown measure"))
1325 );
1326 }
1327
1328 fn typed(vt: ValueType, type_oid: u32, price: &str) -> Result<Option<CellValue>, Rebuild> {
1331 let mut m = model();
1332 m.measures.get_mut(&MeasureId(5)).unwrap().value_type = vt;
1333 let mut r = rels(b'f', &[]);
1334 r.get_mut(&F).unwrap().columns[2].type_oid = type_oid;
1335 let edits = mapper(&m, None).map(&mut m, &r, &txn(vec![ins(full("Q2", price))]), 0)?;
1336 assert_eq!(edits.len(), 1);
1337 assert_eq!(edits[0].coord, key(12, 21));
1338 Ok(edits[0].value.clone())
1339 }
1340
1341 #[test]
1342 fn typed_bindings_read_values_as_the_loader_does() {
1343 use ValueType::{Boolean, DateTime, Enum, Text};
1344 let ok = |v| Ok(Some(v));
1345 assert_eq!(typed(Boolean, 16, "t"), ok(CellValue::Bool(true)));
1346 assert_eq!(typed(Boolean, 16, "f"), ok(CellValue::Bool(false)));
1347 assert_eq!(typed(Text, 25, "a b "), ok(CellValue::Text("a b ".into())));
1348 assert_eq!(
1350 typed(Text, BPCHAR, "ab "),
1351 ok(CellValue::Text("ab".into()))
1352 );
1353 let day = 19_723 * 86_400_000; assert_eq!(
1355 typed(DateTime, 1082, "2024-01-01"),
1356 ok(CellValue::Date(day))
1357 );
1358 assert_eq!(
1359 typed(DateTime, 1114, "2024-01-01 00:00:01.5"),
1360 ok(CellValue::Date(day + 1500))
1361 );
1362 assert_eq!(
1363 typed(DateTime, 1184, "2024-01-01 05:30:00.0005+05:30"),
1364 ok(CellValue::Date(day + 1))
1365 );
1366 assert_eq!(
1367 typed(DateTime, 1184, "1969-12-31 23:59:59.9985+00"),
1368 ok(CellValue::Date(-2))
1369 );
1370 let no = Err(Rebuild("value the loader would not read"));
1371 for (vt, s) in [(Boolean, "yes"), (DateTime, "infinity"), (Enum, "1")] {
1372 assert_eq!(typed(vt, 25, s), no, "{vt:?} {s}");
1373 }
1374 }
1375
1376 #[test]
1377 fn non_finite_values_parse_like_float8() {
1378 assert_eq!(
1379 run(vec![ins(full("Q2", "-Infinity"))]),
1380 Ok(vec![(key(12, 21), Some(f64::NEG_INFINITY))])
1381 );
1382 }
1383
1384 #[test]
1385 fn a_key_identity_works_only_when_the_key_is_the_dimensions() {
1386 let pk = || rels(b'd', &["quarter", "product"]);
1387 let key_only = row(Some("Q1"), Some("P1"), None);
1389 assert_eq!(
1390 run_with(pk(), None, vec![del(key_only.clone())]),
1391 Ok(vec![(key(11, 21), None)])
1392 );
1393 assert_eq!(
1394 run_with(pk(), None, vec![upd(None, full("Q1", "9"))]),
1395 Ok(vec![(key(11, 21), Some(9.0))])
1396 );
1397 assert_eq!(
1398 run_with(pk(), None, vec![upd(Some(key_only), full("Q2", "9"))]),
1399 Ok(vec![(key(11, 21), None), (key(12, 21), Some(9.0))])
1400 );
1401 let id_key = || rels(b'd', &["note"]);
1403 assert_eq!(
1404 run_with(id_key(), None, vec![ins(full("Q2", "1"))]),
1405 Ok(vec![(key(12, 21), Some(1.0))])
1406 );
1407 assert_eq!(
1408 run_with(id_key(), None, vec![upd(None, full("Q1", "9"))]),
1409 Err(Rebuild("old row not replicated"))
1410 );
1411 assert_eq!(
1412 run_with(id_key(), None, vec![del(full("Q1", "1"))]),
1413 Err(Rebuild("old row not replicated"))
1414 );
1415 }
1416
1417 #[test]
1418 fn replayed_transactions_converge() {
1419 let mut m = model();
1422 let mut mp = mapper(&m, None);
1423 let r = rels(b'f', &[]);
1424 let replayed = vec![
1425 Txn {
1426 xid: 1,
1427 end_lsn: 10,
1428 changes: vec![ins(full("Q1", "1"))],
1429 },
1430 Txn {
1431 xid: 2,
1432 end_lsn: 20,
1433 changes: vec![upd(Some(full("Q1", "1")), full("Q1", "2"))],
1434 },
1435 ];
1436 let edits = mp.map(&mut m, &r, &replayed, 20).unwrap();
1437 assert_eq!(
1438 (edits.len(), edits[0].value.clone()),
1439 (1, Some(CellValue::num(2.0)))
1440 );
1441 assert!(mp.map(&mut m, &r, &replayed, 20).unwrap().is_empty());
1442 let later = txn(vec![ins(full("Q1", "3"))]);
1443 assert_eq!(
1444 mp.map(&mut m, &r, &later, 9).err(),
1445 Some(Rebuild("two rows on one coordinate"))
1446 );
1447 }
1448
1449 #[test]
1450 fn new_dimension_values_are_listed_then_minted() {
1451 let mut m = model();
1452 let mut mp = mapper(&m, None);
1453 let r = rels(b'f', &[]);
1454 let batch = txn(vec![
1455 ins(full("Q3", "1")),
1456 ins(row(Some("Q4"), None, Some("1"))),
1457 upd(
1458 Some(full("Q1", "1")),
1459 row(Some("Q1"), Some("P2"), Some("2")),
1460 ),
1461 del(full("Q3", "1")),
1462 ins(full("Q1", "9")),
1463 ]);
1464 assert_eq!(
1465 mp.unknown(&r, &batch),
1466 [
1467 (CategoryId(1), "Q3".to_string()),
1468 (CategoryId(1), "Q4".into()),
1469 (CategoryId(2), "P2".into())
1470 ]
1471 );
1472 assert_eq!(
1473 mp.map(&mut m, &r, &batch, 0).err(),
1474 Some(Rebuild("new item"))
1475 );
1476 let mut m = model();
1477 for (cat, name, id) in [(1, "Q3", 31), (1, "Q4", 32), (2, "P2", 33)] {
1478 m.add_item(ItemId(id), CategoryId(cat), name);
1479 mp.add_item(CategoryId(cat), name, ItemId(id));
1480 }
1481 assert!(mp.unknown(&r, &batch).is_empty());
1482 let edits = mp.map(&mut m, &r, &batch, 0).unwrap();
1483 let got: Vec<_> = edits
1484 .iter()
1485 .map(|e| (e.coord.clone(), e.value.clone()))
1486 .collect();
1487 assert_eq!(
1488 got,
1489 [
1490 (key(11, 21), Some(CellValue::num(9.0))),
1491 (key(11, 33), Some(CellValue::num(2.0))),
1492 ]
1493 );
1494 }
1495
1496 #[test]
1497 fn dimension_names_read_as_the_loader_casts_them() {
1498 let mut r = rels(b'f', &[]);
1499 r.get_mut(&F).unwrap().columns[0].type_oid = BOOL;
1500 r.get_mut(&F).unwrap().columns[1].type_oid = BPCHAR;
1501 let f = &r[&F];
1502 let t = row(Some("t"), Some("P1 "), Some("1"));
1503 assert_eq!(dim_name(f, "quarter", &t), Ok(Some("true".into())));
1504 assert_eq!(dim_name(f, "product", &t), Ok(Some("P1".into())));
1505 let t = row(Some("f"), None, Some("1"));
1506 assert_eq!(dim_name(f, "quarter", &t), Ok(Some("false".into())));
1507 assert_eq!(dim_name(f, "product", &t), Ok(None));
1508 assert_eq!(
1509 dim_name(f, "nope", &t),
1510 Err(Rebuild("bound column missing"))
1511 );
1512 }
1513
1514 #[test]
1515 fn truncate_removes_the_relations_cells() {
1516 let truncate = || Message::Truncate {
1517 options: 0,
1518 rels: vec![OTHER, F],
1519 };
1520 assert_eq!(run(vec![truncate()]), Ok(vec![(key(11, 21), None)]));
1521 assert_eq!(
1523 run(vec![truncate(), ins(full("Q2", "3"))]),
1524 Ok(vec![(key(11, 21), None), (key(12, 21), Some(3.0))])
1525 );
1526 let seed = [("Q1", Some("1")), ("Q1", Some("2"))];
1528 assert_eq!(
1529 fold(Agg::Count, &seed, vec![truncate(), ins(full("Q1", "5"))]),
1530 nums(&[("Q1", 1.0)])
1531 );
1532 }
1533
1534 type Rows = Vec<(String, Option<String>)>;
1536
1537 fn rows_of(rows: &[(&str, Option<&str>)]) -> Rows {
1538 rows.iter()
1539 .map(|(q, v)| (q.to_string(), v.map(str::to_string)))
1540 .collect()
1541 }
1542
1543 fn seed_rows(agg: Agg, rows: &Rows) -> Vec<FoldRow> {
1545 let mut g: BTreeMap<(String, Option<String>), (i64, i64, f64)> = BTreeMap::new();
1547 for (q, v) in rows {
1548 let special = |s: &String| s.parse::<f64>().is_ok_and(|x| !x.is_finite());
1549 let group = match agg {
1550 Agg::Count => None,
1551 Agg::Sum | Agg::Avg => v.clone().filter(special),
1552 _ => v.clone(),
1553 };
1554 let e = g.entry((q.clone(), group)).or_default();
1555 e.1 += 1;
1556 if let Some(s) = v {
1557 e.0 += 1;
1558 e.2 += s.parse::<f64>().unwrap_or(0.0);
1559 }
1560 }
1561 g.into_iter()
1562 .map(|((q, group), (n, all, sum))| {
1563 let v = match agg {
1564 Agg::Count => None,
1565 Agg::Sum | Agg::Avg => group.or((n > 0).then(|| sum.to_string())),
1566 _ => group,
1567 };
1568 (vec![Some(q), Some("P1".into())], v, n, all)
1569 })
1570 .collect()
1571 }
1572
1573 fn expected(agg: Agg, rows: &Rows) -> BTreeMap<String, Value> {
1575 let mut by: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1576 for (q, v) in rows {
1577 let present = by.entry(q).or_default();
1578 present.extend(v.as_deref());
1579 }
1580 by.into_iter()
1581 .filter_map(|(q, present)| {
1582 let xs = || present.iter().map(|s| s.parse::<f64>().unwrap());
1583 let n = present.len() as f64;
1584 let v = match agg {
1585 Agg::Count => Some(n),
1586 Agg::CountDistinct => {
1587 Some(present.iter().collect::<BTreeSet<_>>().len() as f64)
1588 }
1589 _ if present.is_empty() => None,
1590 Agg::Sum => Some(xs().sum()),
1591 Agg::Avg => Some(xs().sum::<f64>() / n),
1592 Agg::Min => xs().min_by(f64::total_cmp),
1594 Agg::Max => xs().max_by(f64::total_cmp),
1595 };
1596 v.map(|v| (q.to_string(), Value::Number(v)))
1597 })
1598 .collect()
1599 }
1600
1601 fn quarter(c: &Coordinate) -> String {
1602 match c.dims[&CategoryId(1)] {
1603 ItemId(11) => "Q1".into(),
1604 _ => "Q2".into(),
1605 }
1606 }
1607
1608 fn cells(m: &Model) -> BTreeMap<String, Value> {
1610 m.inputs
1611 .iter()
1612 .map(|((_, c), v)| (quarter(c), v.clone()))
1613 .collect()
1614 }
1615
1616 fn nums(pairs: &[(&str, f64)]) -> Result<BTreeMap<String, Value>, Rebuild> {
1617 Ok(pairs
1618 .iter()
1619 .map(|(q, x)| (q.to_string(), Value::Number(*x)))
1620 .collect())
1621 }
1622
1623 fn fold(
1626 agg: Agg,
1627 seed: &[(&str, Option<&str>)],
1628 changes: Vec<Message>,
1629 ) -> Result<BTreeMap<String, Value>, Rebuild> {
1630 let rows = rows_of(seed);
1631 let mut m = model();
1632 let mut mp = mapper(&m, Some(agg));
1633 mp.seed(
1634 &mut m,
1635 MeasureId(5),
1636 25,
1637 &seed_rows(agg, &rows),
1638 Snap::default(),
1639 )?;
1640 assert_eq!(cells(&m), expected(agg, &rows), "{agg:?} seed");
1641 mp.map(&mut m, &rels(b'f', &[]), &txn(changes), 0)?;
1642 Ok(cells(&m))
1643 }
1644
1645 #[test]
1646 fn aggregated_bindings_fold_rows_incrementally() {
1647 assert_eq!(
1649 fold(
1650 Agg::Sum,
1651 &[("Q1", Some("1")), ("Q1", Some("2"))],
1652 vec![
1653 ins(full("Q1", "4")),
1654 del(full("Q1", "1")),
1655 upd(Some(full("Q1", "2")), full("Q2", "2")),
1656 ]
1657 ),
1658 nums(&[("Q1", 4.0), ("Q2", 2.0)])
1659 );
1660 let null_q2 = || ins(row(Some("Q2"), Some("P1"), None));
1662 assert_eq!(
1663 fold(
1664 Agg::Count,
1665 &[("Q1", Some("1"))],
1666 vec![null_q2(), ins(full("Q1", "1"))]
1667 ),
1668 nums(&[("Q1", 2.0), ("Q2", 0.0)])
1669 );
1670 assert_eq!(fold(Agg::Sum, &[], vec![null_q2()]), nums(&[]));
1672 assert_eq!(
1673 fold(
1674 Agg::Avg,
1675 &[("Q1", Some("1")), ("Q1", Some("2"))],
1676 vec![ins(full("Q1", "6"))]
1677 ),
1678 nums(&[("Q1", 3.0)])
1679 );
1680 let threes = [("Q1", Some("3")), ("Q1", Some("3")), ("Q1", Some("5"))];
1682 assert_eq!(
1683 fold(Agg::Min, &threes, vec![del(full("Q1", "3"))]),
1684 nums(&[("Q1", 3.0)])
1685 );
1686 assert_eq!(
1687 fold(
1688 Agg::Min,
1689 &threes,
1690 vec![del(full("Q1", "3")), del(full("Q1", "3"))]
1691 ),
1692 nums(&[("Q1", 5.0)])
1693 );
1694 assert_eq!(
1695 fold(Agg::Max, &threes, vec![del(full("Q1", "5"))]),
1696 nums(&[("Q1", 3.0)])
1697 );
1698 let shas = [("Q1", Some("a1")), ("Q1", Some("a1")), ("Q1", Some("a2"))];
1700 assert_eq!(
1701 fold(Agg::CountDistinct, &shas, vec![del(full("Q1", "a1"))]),
1702 nums(&[("Q1", 2.0)])
1703 );
1704 assert_eq!(
1705 fold(
1706 Agg::CountDistinct,
1707 &shas,
1708 vec![del(full("Q1", "a1")), del(full("Q1", "a1"))]
1709 ),
1710 nums(&[("Q1", 1.0)])
1711 );
1712 assert_eq!(
1714 fold(Agg::Count, &[("Q1", Some("1"))], vec![del(full("Q1", "1"))]),
1715 nums(&[])
1716 );
1717 }
1718
1719 #[test]
1720 fn what_a_fold_cannot_apply_rebuilds() {
1721 let step = Some(Rebuild("a fold went out of step"));
1722 assert_eq!(fold(Agg::Sum, &[], vec![del(full("Q1", "1"))]).err(), step);
1723 assert_eq!(
1724 fold(
1725 Agg::CountDistinct,
1726 &[("Q1", Some("a1"))],
1727 vec![del(full("Q1", "a2"))]
1728 )
1729 .err(),
1730 step
1731 );
1732 assert_eq!(
1733 fold(Agg::Sum, &[], vec![ins(full("Q1", "x"))]).err(),
1734 Some(Rebuild("value the loader would not read"))
1735 );
1736 assert_eq!(
1737 fold(Agg::Count, &[("Q9", Some("1"))], vec![]).err(),
1738 Some(Rebuild("new item"))
1739 );
1740 let mut m = model();
1742 let mut mp = mapper(&m, Some(Agg::Sum));
1743 assert_eq!(
1744 mp.map(
1745 &mut m,
1746 &rels(b'f', &[]),
1747 &txn(vec![ins(full("Q2", "1"))]),
1748 0
1749 )
1750 .err(),
1751 Some(Rebuild("fold not seeded"))
1752 );
1753 mp.seed(&mut m, MeasureId(5), 25, &[], Snap::default())
1754 .unwrap();
1755 let pk = rels(b'd', &["quarter", "product"]);
1756 assert_eq!(
1757 mp.map(&mut m, &pk, &txn(vec![del(full("Q1", "1"))]), 0)
1758 .err(),
1759 Some(Rebuild("old row not replicated"))
1760 );
1761 assert!(mp
1762 .map(&mut m, &pk, &txn(vec![ins(full("Q2", "1"))]), 0)
1763 .is_ok());
1764 let mut m = model();
1766 m.measures.get_mut(&MeasureId(5)).unwrap().value_type = ValueType::Text;
1767 let mut mp = mapper(&m, Some(Agg::Max));
1768 let b = seed_rows(Agg::Max, &rows_of(&[("Q1", Some("b"))]));
1769 mp.seed(&mut m, MeasureId(5), 25, &b, Snap::default())
1770 .unwrap();
1771 assert_eq!(
1772 mp.map(
1773 &mut m,
1774 &rels(b'f', &[]),
1775 &txn(vec![ins(full("Q1", "a"))]),
1776 0
1777 )
1778 .err(),
1779 Some(Rebuild("min or max of text"))
1780 );
1781 assert_eq!(
1782 mp.seed(&mut m, MeasureId(6), 25, &[], Snap::default()),
1783 Err(Rebuild("unknown binding"))
1784 );
1785 let mut plain = mapper(&m, None);
1786 assert_eq!(
1787 plain.seed(&mut m, MeasureId(5), 25, &[], Snap::default()),
1788 Err(Rebuild("not an aggregated binding"))
1789 );
1790 }
1791
1792 #[test]
1793 fn loaded_changes_to_aggregated_bindings_are_skipped() {
1794 let mut m = model();
1795 let mut mp = mapper(&m, Some(Agg::Sum));
1796 let b = seed_rows(Agg::Sum, &rows_of(&[("Q1", Some("1")), ("Q1", Some("4"))]));
1797 mp.seed(&mut m, MeasureId(5), 25, &b, Snap::parse("4:6:4").unwrap())
1799 .unwrap();
1800 let t = |xid, v| Txn {
1801 xid,
1802 end_lsn: 10,
1803 changes: vec![ins(full("Q1", v))],
1804 };
1805 let batch = [t(3, "1"), t(5, "4"), t(4, "2"), t(6, "8")];
1806 let edits = mp.map(&mut m, &rels(b'f', &[]), &batch, 99).unwrap();
1807 assert_eq!(edits.len(), 1);
1808 assert_eq!(edits[0].value, Some(CellValue::num(15.0)));
1809 let truncate = Txn {
1810 xid: 3,
1811 end_lsn: 10,
1812 changes: vec![Message::Truncate {
1813 options: 0,
1814 rels: vec![F],
1815 }],
1816 };
1817 assert!(mp
1818 .map(&mut m, &rels(b'f', &[]), &[truncate], 0)
1819 .unwrap()
1820 .is_empty());
1821 }
1822
1823 struct Rng(u64);
1825
1826 impl Rng {
1827 fn below(&mut self, n: usize) -> usize {
1828 self.0 ^= self.0 << 13;
1829 self.0 ^= self.0 >> 7;
1830 self.0 ^= self.0 << 17;
1831 (self.0 % n as u64) as usize
1832 }
1833 }
1834
1835 #[test]
1839 fn folds_equal_a_fresh_aggregate_after_random_batches() {
1840 const VALUES: [Option<&str>; 9] = [
1841 Some("Infinity"),
1842 Some("NaN"),
1843 Some("1"),
1844 Some("2"),
1845 Some("2.5"),
1846 Some("-3"),
1847 Some("0.25"),
1848 Some("10"),
1849 None,
1850 ];
1851 let tuple = |(q, v): &(String, Option<String>)| row(Some(q), Some("P1"), v.as_deref());
1852 let r = rels(b'f', &[]);
1853 for (i, agg) in [
1854 Agg::Sum,
1855 Agg::Avg,
1856 Agg::Min,
1857 Agg::Max,
1858 Agg::Count,
1859 Agg::CountDistinct,
1860 ]
1861 .into_iter()
1862 .enumerate()
1863 {
1864 let mut rng = Rng(0x2545_f491_4f6c_dd1d + i as u64);
1865 let pick = |rng: &mut Rng| {
1866 (
1867 ["Q1", "Q2"][rng.below(2)].to_string(),
1868 VALUES[rng.below(VALUES.len())].map(str::to_string),
1869 )
1870 };
1871 let mut rows: Rows = (0..6).map(|_| pick(&mut rng)).collect();
1872 let mut m = model();
1873 let mut mp = mapper(&m, Some(agg));
1874 mp.seed(
1875 &mut m,
1876 MeasureId(5),
1877 25,
1878 &seed_rows(agg, &rows),
1879 Snap::default(),
1880 )
1881 .unwrap();
1882 assert_eq!(
1883 format!("{:?}", cells(&m)),
1884 format!("{:?}", expected(agg, &rows)),
1885 "{agg:?} seed"
1886 );
1887 let shown = |c: &BTreeMap<String, Value>| format!("{c:?}");
1889 let num = |v: &CellValue| Value::Number(v.as_num().unwrap());
1890 let mut engine = cells(&m);
1891 for batch in 0..300u32 {
1892 let mut changes = Vec::new();
1893 for _ in 0..1 + rng.below(4) {
1894 let new = pick(&mut rng);
1895 match rng.below(3) {
1896 0 => {
1897 changes.push(ins(tuple(&new)));
1898 rows.push(new);
1899 }
1900 _ if rows.is_empty() => {}
1901 1 => {
1902 let old = rows.swap_remove(rng.below(rows.len()));
1903 changes.push(del(tuple(&old)));
1904 }
1905 _ => {
1906 let at = rng.below(rows.len());
1907 changes.push(upd(Some(tuple(&rows[at])), tuple(&new)));
1908 rows[at] = new;
1909 }
1910 }
1911 }
1912 let t = Txn {
1913 xid: batch,
1914 end_lsn: 100 + u64::from(batch),
1915 changes,
1916 };
1917 for e in mp.map(&mut m, &r, &[t], 0).unwrap() {
1918 let q = if e.coord == key(11, 21) { "Q1" } else { "Q2" };
1919 match e.value {
1920 Some(v) => engine.insert(q.into(), num(&v)),
1921 None => engine.remove(q),
1922 };
1923 }
1924 let want = expected(agg, &rows);
1925 assert_eq!(shown(&cells(&m)), shown(&want), "{agg:?}, batch {batch}");
1926 assert_eq!(shown(&engine), shown(&want), "{agg:?} edits, batch {batch}");
1927 }
1928 }
1929 }
1930
1931 #[test]
1932 fn totals_are_exact_for_decimals_and_follow_float8_otherwise() {
1933 assert_eq!(nanos("-12.5"), Some(-12_500_000_000));
1934 assert_eq!(nanos("+0.000000001"), Some(1));
1935 assert_eq!(nanos(".5"), Some(500_000_000));
1936 assert_eq!(nanos("7"), Some(7 * NANOS));
1937 assert_eq!(nanos("123456789012345678901"), None);
1939 assert_eq!(
1940 Num::parse("123456789012345678901"),
1941 Ok(Num::Float(1.2345678901234568e20))
1942 );
1943 for s in ["1e5", "1.0000000001", "", "-", ".", "NaN", "1.2.3", "1-2"] {
1944 assert_eq!(nanos(s), None, "{s}");
1945 }
1946 let num = |s| Num::parse(s).unwrap();
1947 let mut t = Total::default();
1948 t.add(num("0.1"), 1);
1949 t.add(num("0.2"), 1);
1950 assert_eq!(t.value(), 0.3);
1952 t.add(num("1e300"), 1);
1953 assert_eq!(t.value(), 1e300);
1954 t.add(num("1e300"), -1);
1955 assert_eq!(t.value(), 0.3);
1956 t.add(num("Infinity"), 1);
1957 assert_eq!(t.value(), f64::INFINITY);
1958 t.add(num("-Infinity"), 1);
1959 assert!(t.value().is_nan());
1960 t.add(num("Infinity"), -1);
1961 assert_eq!(t.value(), f64::NEG_INFINITY);
1962 t.add(num("-Infinity"), -1);
1963 t.add(num("NaN"), 1);
1964 assert!(t.value().is_nan());
1965 assert_eq!(
1966 Num::parse("abc"),
1967 Err(Rebuild("value the loader would not read"))
1968 );
1969 let mut t = Total::default();
1970 t.add(num("-0.25"), 2);
1971 assert_eq!(t.value(), -0.5);
1972 }
1973
1974 #[test]
1975 fn averages_divide_as_postgres_numeric_does() {
1976 let cases: [(&str, i32, i64, f64); 13] = [
1978 ("11", 0, 9, 1.222222222222222),
1979 ("10", 0, 3, 3.3333333333333335),
1980 ("4", 0, 3, 1.3333333333333333),
1981 ("1", 0, 7, 0.14285714285714285),
1982 ("12345", 0, 7, 1763.5714285714287),
1983 ("98765432", 0, 3, 32921810.666666668),
1984 ("-11", 0, 9, -1.222222222222222),
1985 ("1.50", 2, 7, 0.21428571428571427),
1986 ("0.0001", 4, 1_000_000, 1e-10),
1987 ("-0.000000001", 9, 3, -3.333333333333333e-10),
1988 ("17", 0, 6, 2.8333333333333335),
1989 ("0", 0, 5, 0.0),
1990 (
1992 "8999999999999999999.999999999",
1993 9,
1994 9_000_000_000_000_000_000,
1995 1.0,
1996 ),
1997 ];
1998 for (s, scale, n, want) in cases {
1999 let got = numeric_div(nanos(s).unwrap(), scale, n);
2000 assert_eq!(got.to_bits(), want.to_bits(), "{s} / {n}: {got} != {want}");
2001 }
2002 let num = |s| Num::parse(s).unwrap();
2003 let mut t = Total::default();
2004 for s in ["1.50", "2.25", "-0.10"] {
2005 t.add(num(s), 1);
2006 }
2007 assert_eq!(t.avg(3, false), 1.2166666666666666);
2008 t.add(num("2.25"), -1);
2009 t.add(num("1.50"), -1);
2010 assert_eq!(t.avg(1, false), -0.1);
2011 let mut t = Total::default();
2013 t.add(num("11"), 1);
2014 assert_eq!(t.avg(9, true), 11.0 / 9.0);
2015 let mut t = Total::default();
2017 t.add(num("1e2"), 1);
2018 t.add(num("2"), 1);
2019 assert_eq!(t.avg(2, false), 51.0);
2020 }
2021
2022 #[test]
2023 fn keys_order_and_compare_as_postgres_does() {
2024 let xs = [
2025 f64::NEG_INFINITY,
2026 -2.5,
2027 -0.0,
2028 0.0,
2029 1e-300,
2030 3.0,
2031 f64::INFINITY,
2032 ];
2033 let ks: Vec<i64> = xs.iter().map(|x| ord_bits(*x)).collect();
2034 assert!(ks.windows(2).all(|w| w[0] < w[1]), "{ks:?}");
2035 assert!(ord_bits(f64::NAN) > ord_bits(f64::INFINITY));
2036 for x in xs {
2037 assert_eq!(Key::Num(ord_bits(x)).value(), Some(Value::Number(x)));
2038 }
2039 let day = Key::order(Value::DateTime(
2040 chrono::DateTime::from_timestamp_millis(86_400_000).unwrap(),
2041 ));
2042 assert_eq!(day, Ok(Key::Date(86_400_000)));
2043 assert!(Key::Bool(false) < Key::Bool(true));
2044 assert_eq!(
2045 Key::order(Value::Text("a".into())),
2046 Err(Rebuild("min or max of text"))
2047 );
2048 assert_eq!(distinct_text(BPCHAR, "ab "), "ab");
2049 assert_eq!(distinct_text(NUMERIC, "1.50"), "1.5");
2050 assert_eq!(distinct_text(NUMERIC, "2.000"), "2");
2051 assert_eq!(distinct_text(NUMERIC, "100"), "100");
2052 assert_eq!(distinct_text(FLOAT8, "-0"), "0");
2053 assert_eq!(distinct_text(FLOAT4, "-0"), "0");
2054 assert_eq!(distinct_text(FLOAT8, "-1"), "-1");
2055 assert_eq!(distinct_text(FLOAT4, "1.5"), "1.5");
2056 assert_eq!(
2057 Key::order(Value::Number(-0.0)),
2058 Key::order(Value::Number(0.0))
2059 );
2060 assert_eq!(Key::Text("a".into()).value(), Some(Value::Text("a".into())));
2061 assert_eq!(distinct_text(25, "a "), "a ");
2062 assert_eq!(distinct_text(BOOL, "true"), distinct_text(BOOL, "t"));
2063 assert_eq!(
2064 parse(ValueType::Boolean, "false", false),
2065 Ok(Value::Boolean(false))
2066 );
2067 assert_eq!(Agg::parse("count_distinct"), Some(Agg::CountDistinct));
2068 for a in ["sum", "avg", "min", "max", "count"] {
2069 assert!(Agg::parse(a).is_some(), "{a}");
2070 }
2071 assert_eq!(Agg::parse("median"), None);
2072 }
2073
2074 #[test]
2075 fn snapshots_say_which_transactions_they_saw() {
2076 let s = Snap::parse("100:105:101,103").unwrap();
2077 let seen: Vec<u32> = (95..110).filter(|x| s.sees(*x)).collect();
2078 assert_eq!(seen, [95, 96, 97, 98, 99, 100, 102, 104]);
2079 let s = Snap::parse("4294967300:4294967302:").unwrap();
2081 assert!(s.sees(4_294_967_295) && s.sees(4) && s.sees(5) && !s.sees(6));
2082 for bad in ["", "1", "1:2", "1:2:x", "1:2:3:4"] {
2083 assert_eq!(Snap::parse(bad), None, "{bad}");
2084 }
2085 assert_eq!(
2086 seed_sql("s", &["a".into()], "v", Agg::Count),
2087 "(SELECT \"a\"::text, NULL::text, count(\"v\"), count(*), NULL::text FROM s \
2088 GROUP BY 1) UNION ALL SELECT NULL, NULL, 0, 0, pg_current_snapshot()::text"
2089 );
2090 assert_eq!(
2091 seed_sql("s", &["a".into()], "v", Agg::Sum),
2092 "(SELECT \"a\"::text, coalesce(improv_special, sum(\"v\")::text), count(\"v\"), \
2093 count(*), NULL::text FROM (SELECT *, CASE WHEN \"v\"::text IN ('Infinity', \
2094 '-Infinity', 'NaN') THEN \"v\"::text END AS improv_special FROM s) t \
2095 GROUP BY 1, improv_special) UNION ALL SELECT NULL, NULL, 0, 0, \
2096 pg_current_snapshot()::text"
2097 );
2098 for agg in [Agg::Min, Agg::Max, Agg::CountDistinct] {
2099 assert_eq!(
2100 seed_sql("s", &["a".into()], "v", agg),
2101 "(SELECT \"a\"::text, \"v\"::text, count(\"v\"), count(*), NULL::text FROM s \
2102 GROUP BY 1, 2) UNION ALL SELECT NULL, NULL, 0, 0, pg_current_snapshot()::text"
2103 );
2104 }
2105 }
2106
2107 #[test]
2108 fn a_mapper_lists_its_aggregated_bindings() {
2109 let plain = Binding {
2110 measure: MeasureId(5),
2111 value_col: "price".into(),
2112 dims: vec![],
2113 agg: None,
2114 };
2115 let summed = |id| Binding {
2116 measure: MeasureId(id),
2117 agg: Some(Agg::Sum),
2118 ..plain.clone()
2119 };
2120 let mp = Mapper::new(
2121 &model(),
2122 HashMap::from([
2123 (F, vec![plain.clone(), summed(9)]),
2124 (OTHER, vec![summed(6)]),
2125 ]),
2126 );
2127 assert_eq!(mp.aggregated(), [(OTHER, summed(6)), (F, summed(9))]);
2128 assert!(mp.binds(F) && mp.binds(OTHER) && !mp.binds(3));
2129 }
2130
2131 fn group(q: Option<&str>, v: Option<&str>, n: i64, all: i64) -> FoldRow {
2133 (
2134 vec![q.map(str::to_string), Some("P1".into())],
2135 v.map(str::to_string),
2136 n,
2137 all,
2138 )
2139 }
2140
2141 fn fold_as(
2144 agg: Agg,
2145 (vt, oid): (ValueType, u32),
2146 seed: &[FoldRow],
2147 changes: Vec<Message>,
2148 ) -> Result<BTreeMap<String, Value>, Rebuild> {
2149 let mut m = model();
2150 m.measures.get_mut(&MeasureId(5)).unwrap().value_type = vt;
2151 let mut r = rels(b'f', &[]);
2152 r.get_mut(&F).unwrap().columns[2].type_oid = oid;
2153 let mut mp = mapper(&m, Some(agg));
2154 mp.seed(&mut m, MeasureId(5), oid, seed, Snap::default())?;
2155 mp.map(&mut m, &r, &txn(changes), 0)?;
2156 Ok(cells(&m))
2157 }
2158
2159 fn values(pairs: &[(&str, Value)]) -> Result<BTreeMap<String, Value>, Rebuild> {
2160 Ok(pairs
2161 .iter()
2162 .map(|(q, v)| (q.to_string(), v.clone()))
2163 .collect())
2164 }
2165
2166 #[test]
2167 fn min_and_max_fall_back_when_their_extreme_goes() {
2168 let num = (ValueType::Number, FLOAT8);
2169 let q1 = |v| full("Q1", v);
2170 let nan = [
2172 group(Some("Q1"), Some("3"), 1, 1),
2173 group(Some("Q1"), Some("NaN"), 1, 1),
2174 ];
2175 let max = |changes| fold_as(Agg::Max, num, &nan, changes).unwrap();
2176 assert!(matches!(max(vec![])["Q1"], Value::Number(x) if x.is_nan()));
2177 assert_eq!(Ok(max(vec![del(q1("NaN"))])), nums(&[("Q1", 3.0)]));
2178 let min = fold_as(Agg::Min, num, &nan, vec![del(q1("3"))]).unwrap();
2179 assert!(matches!(min["Q1"], Value::Number(x) if x.is_nan()));
2180 let five = [
2182 group(Some("Q1"), Some("3"), 1, 1),
2183 group(Some("Q1"), Some("5"), 2, 2),
2184 ];
2185 let moved = upd(Some(q1("5")), full("Q2", "5"));
2186 assert_eq!(
2187 fold_as(Agg::Max, num, &five, vec![moved.clone()]),
2188 nums(&[("Q1", 5.0), ("Q2", 5.0)])
2189 );
2190 assert_eq!(
2191 fold_as(Agg::Max, num, &five, vec![moved.clone(), moved]),
2192 nums(&[("Q1", 3.0), ("Q2", 5.0)])
2193 );
2194 let bool = (ValueType::Boolean, BOOL);
2196 let tf = [
2197 group(Some("Q1"), Some("false"), 1, 1),
2198 group(Some("Q1"), Some("true"), 2, 2),
2199 ];
2200 let b = |x| Value::Boolean(x);
2201 assert_eq!(
2202 fold_as(Agg::Max, bool, &tf, vec![del(q1("t"))]),
2203 values(&[("Q1", b(true))])
2204 );
2205 assert_eq!(
2206 fold_as(Agg::Max, bool, &tf, vec![del(q1("t")), del(q1("t"))]),
2207 values(&[("Q1", b(false))])
2208 );
2209 assert_eq!(
2210 fold_as(Agg::Min, bool, &tf, vec![del(q1("f"))]),
2211 values(&[("Q1", b(true))])
2212 );
2213 let distinct = (ValueType::Number, BOOL);
2214 assert_eq!(
2215 fold_as(Agg::CountDistinct, distinct, &tf, vec![del(q1("t"))]),
2216 nums(&[("Q1", 2.0)])
2217 );
2218 assert_eq!(
2219 fold_as(
2220 Agg::CountDistinct,
2221 distinct,
2222 &tf,
2223 vec![del(q1("t")), del(q1("t"))]
2224 ),
2225 nums(&[("Q1", 1.0)])
2226 );
2227 let date = (ValueType::DateTime, 1082);
2229 let days = [
2230 group(Some("Q1"), Some("2026-01-01"), 1, 1),
2231 group(Some("Q1"), Some("2026-03-01"), 1, 1),
2232 ];
2233 let day = |s| Value::DateTime(datetime(s).unwrap());
2234 assert_eq!(
2235 fold_as(Agg::Max, date, &days, vec![del(q1("2026-03-01"))]),
2236 values(&[("Q1", day("2026-01-01"))])
2237 );
2238 assert_eq!(
2239 fold_as(Agg::Min, date, &days, vec![del(q1("2026-01-01"))]),
2240 values(&[("Q1", day("2026-03-01"))])
2241 );
2242 let zero = [group(Some("Q1"), Some("0"), 1, 1)];
2244 assert_eq!(
2245 fold_as(Agg::CountDistinct, num, &zero, vec![ins(q1("-0"))]),
2246 nums(&[("Q1", 1.0)])
2247 );
2248 }
2249
2250 #[test]
2251 fn sums_and_averages_seed_from_groups() {
2252 let numeric = (ValueType::Number, NUMERIC);
2254 let q1 = |v| full("Q1", v);
2255 let two = [group(Some("Q1"), Some("3.75"), 2, 2)];
2256 assert_eq!(
2257 fold_as(Agg::Avg, numeric, &two, vec![ins(q1("-0.10"))]),
2258 nums(&[("Q1", 1.2166666666666666)])
2259 );
2260 assert_eq!(
2261 fold_as(
2262 Agg::Avg,
2263 numeric,
2264 &two,
2265 vec![ins(q1("-0.10")), del(q1("1.50"))]
2266 ),
2267 nums(&[("Q1", 1.075)])
2268 );
2269 assert_eq!(
2271 fold_as(
2272 Agg::Avg,
2273 numeric,
2274 &[group(Some("Q1"), Some("0.3"), 2, 2)],
2275 vec![ins(q1("0.05"))]
2276 ),
2277 nums(&[("Q1", 0.11666666666666667)])
2278 );
2279 let num = (ValueType::Number, FLOAT8);
2281 let nan = [
2282 group(Some("Q1"), Some("3"), 1, 1),
2283 group(Some("Q1"), Some("NaN"), 1, 1),
2284 ];
2285 let sum = fold_as(Agg::Sum, num, &nan, vec![]).unwrap();
2286 assert!(matches!(sum["Q1"], Value::Number(x) if x.is_nan()));
2287 assert_eq!(
2288 fold_as(Agg::Sum, num, &nan, vec![del(q1("NaN"))]),
2289 nums(&[("Q1", 3.0)])
2290 );
2291 }
2292
2293 #[test]
2294 fn seeds_skip_null_dimensions_and_refuse_bad_counts() {
2295 let num = (ValueType::Number, FLOAT8);
2296 let seed = [group(None, Some("7"), 1, 1), group(Some("Q2"), None, 0, 2)];
2299 assert_eq!(fold_as(Agg::Sum, num, &seed, vec![]), nums(&[]));
2300 assert_eq!(
2301 fold_as(Agg::Count, num, &seed, vec![]),
2302 nums(&[("Q2", 0.0)])
2303 );
2304 let null = || del(row(Some("Q2"), Some("P1"), None));
2305 assert_eq!(
2306 fold_as(Agg::Count, num, &seed, vec![null()]),
2307 nums(&[("Q2", 0.0)])
2308 );
2309 assert_eq!(
2310 fold_as(Agg::Count, num, &seed, vec![null(), null()]),
2311 nums(&[])
2312 );
2313 let step = Err(Rebuild("a fold went out of step"));
2315 assert_eq!(
2316 fold_as(Agg::Count, num, &[group(Some("Q1"), None, 0, -1)], vec![]),
2317 step
2318 );
2319 assert_eq!(
2320 fold_as(Agg::Sum, num, &[group(Some("Q1"), Some("3"), 2, 0)], vec![]),
2321 step
2322 );
2323 }
2324
2325 #[test]
2326 fn rows_a_change_cannot_place_are_skipped_or_rebuild() {
2327 assert_eq!(run(vec![del(row(None, Some("P1"), Some("1")))]), Ok(vec![]));
2330 assert_eq!(
2331 fold(
2332 Agg::Sum,
2333 &[("Q1", Some("1"))],
2334 vec![ins(row(None, Some("P1"), Some("5")))]
2335 ),
2336 nums(&[("Q1", 1.0)])
2337 );
2338 assert_eq!(
2340 run(vec![upd(None, full("Q1", "2"))]),
2341 Err(Rebuild("old row not replicated"))
2342 );
2343 let mut t = full("Q2", "1");
2344 t[0] = Datum::Unchanged;
2345 assert_eq!(run(vec![ins(t)]), Err(Rebuild("unchanged TOAST value")));
2346 let origin = Message::Origin {
2348 lsn: 1,
2349 name: "o".into(),
2350 };
2351 assert_eq!(run(vec![origin.clone()]), Ok(vec![]));
2352 let unbound = Message::Insert {
2354 rel: OTHER,
2355 new: full("Q9", "1"),
2356 };
2357 let r = rels(b'f', &[]);
2358 assert!(mapper(&model(), None)
2359 .unknown(&r, &txn(vec![origin, unbound]))
2360 .is_empty());
2361 let mut m = model();
2363 let truncate = txn(vec![Message::Truncate {
2364 options: 0,
2365 rels: vec![F],
2366 }]);
2367 assert_eq!(
2368 mapper(&m, None).map(&mut m, &r, &truncate, 10).err(),
2369 Some(Rebuild("truncate"))
2370 );
2371 }
2372
2373 #[test]
2380 fn new_items_feed_an_engine_that_serves_the_grown_model() {
2381 use improv_engine::session::Engine;
2382 let derived = |id, name: &str, text: &str, categories| MeasureRow {
2383 id,
2384 name: name.into(),
2385 value_type: "number".into(),
2386 kind: "derived".into(),
2387 formula: Some(text.into()),
2388 description: None,
2389 categories,
2390 };
2391 let mut cat = catalog();
2392 cat.measures
2393 .push(derived(6, "Total", "Total = SUM(Price OVER Time)", vec![2]));
2394 cat.measures
2395 .push(derived(7, "Pos", "Pos = POSITION(Time)", vec![1]));
2396 let mut m = build_model(&cat).unwrap();
2397 m.set_input(MeasureId(5), coordinate(11, 21), Value::Number(1.0));
2398 let (mut total, _) = Engine::new(&m, &[MeasureId(6)]).unwrap();
2399 let (pos, _) = Engine::new(&m, &[MeasureId(7)]).unwrap();
2400 let mut mp = mapper(&m, None);
2401 let r = rels(b'f', &[]);
2402 let batch = txn(vec![ins(full("Q3", "4")), ins(full("Q2", "2"))]);
2403 assert_eq!(mp.unknown(&r, &batch), [(CategoryId(1), "Q3".to_string())]);
2404 mp.add_item(CategoryId(1), "Q3", ItemId(13));
2405 m.add_item(ItemId(13), CategoryId(1), "Q3");
2406 assert!(total.serves(&m).unwrap());
2407 assert!(!pos.serves(&m).unwrap());
2408 let snap = total.apply(mp.map(&mut m, &r, &batch, 0).unwrap()).unwrap();
2409 let fresh = improv_engine::dataflow::evaluate(&m, &[MeasureId(6)]).unwrap();
2410 let total = &fresh[&MeasureId(6)];
2411 assert_eq!(total[&vec![(2, 21)]], CellValue::num(7.0));
2412 assert_eq!(
2413 snap.get(&MeasureId(6)).map(|v| (**v).clone()),
2414 Some(total.clone())
2415 );
2416 }
2417}