Skip to main content

improv_pg_model/
live.rs

1//! G1: map decoded `pgoutput` row changes onto live-engine edits.
2//!
3//! The logical-decoding worker keeps a live [`improv_engine::session::Engine`]
4//! per model. [`Mapper::map`] turns one batch of committed row changes into
5//! the input-cell [`Edit`]s it makes (one per touched cell, its final value),
6//! or says the batch cannot be applied cell by cell ([`Rebuild`]) and the
7//! caller must reload the model: two rows on one coordinate of a binding
8//! without `agg`, a value the loader would not read, or an old row whose
9//! values were not replicated. A TRUNCATE removes every cell its
10//! relation's bindings feed. Dimension values with no item yet are listed
11//! by [`Mapper::unknown`]: the caller mints them (`improv.mint_items`) and
12//! adds them ([`Mapper::add_item`]) before mapping.
13//!
14//! Aggregated bindings (`improv.binding.agg`) keep per-coordinate fold state
15//! (`Fold`): the row counts, an exact total for `sum` and `avg`, and for
16//! `min`, `max` and `count_distinct` each distinct value's row count, so
17//! deleting one of two rows that share a value keeps it. [`Mapper::seed`]
18//! loads that state at a rebuild; each change then removes its old row from
19//! its coordinate's fold and adds its new row to its (maybe other)
20//! coordinate's, and each touched cell takes its fold's value.
21//!
22//! Values are the `pgoutput` text of the column, read by the measure's type
23//! as the loader's `::float8`/`::bool`/`::text`/`timestamptz` casts read the
24//! row. A date or timestamp without a zone is taken as UTC: the sync worker
25//! runs with `TimeZone = UTC` and `DateStyle = ISO`.
26//!
27//! Replay: a rebuild reads the bound tables as they are *now*, which may
28//! already include transactions the slot has not yet delivered. Those are
29//! re-applied afterwards. Each change to a binding without `agg` is a
30//! per-cell "remove" or "set", so re-applying converges (the last change to
31//! a cell wins); the one check that is not idempotent, an insert onto an
32//! occupied coordinate, overwrites instead of rebuilding for transactions
33//! that ended by `replay_until`. A fold is not idempotent (a re-applied
34//! insert counts twice), so each aggregated binding's seed query also
35//! returns the snapshot it read with ([`Snap`], `pg_current_snapshot()` in
36//! the same statement), and [`Mapper::map`] skips exactly the transactions
37//! that snapshot saw.
38
39use 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/// How the rows on one coordinate fold into its cell (`improv.binding.agg`).
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Agg {
50    /// `sum(v)`.
51    Sum,
52    /// `avg(v)`.
53    Avg,
54    /// `min(v)`.
55    Min,
56    /// `max(v)`.
57    Max,
58    /// `count(v)`.
59    Count,
60    /// `count(DISTINCT v)`.
61    CountDistinct,
62}
63
64impl Agg {
65    /// The aggregate a `binding.agg` names; `None` for anything else.
66    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/// How one input measure is bound to the columns of one relation.
80#[derive(Debug, Clone, PartialEq)]
81pub struct Binding {
82    /// The bound input measure.
83    pub measure: MeasureId,
84    /// The value column.
85    pub value_col: String,
86    /// One `(category, column)` per dimension, in the measure's category order.
87    pub dims: Vec<(CategoryId, String)>,
88    /// Several rows may share a coordinate, folded by this aggregate
89    /// (`improv.binding.agg`).
90    pub agg: Option<Agg>,
91}
92
93/// Each cell a batch touched, with its value before the batch.
94type Touched = BTreeMap<(MeasureId, Coordinate), Option<Value>>;
95
96/// Item ids per category, by name.
97type Items = HashMap<CategoryId, HashMap<String, ItemId>>;
98
99/// One row of [`seed_sql`]: dimension names, a value's text, how many rows
100/// with a value it stands for, and how many rows in all.
101pub type FoldRow = (Vec<Option<String>>, Option<String>, i64, i64);
102
103/// A Postgres snapshot (`pg_current_snapshot()`): which transactions a
104/// query saw.
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct Snap {
107    xmin: u64,
108    xmax: u64,
109    xip: BTreeSet<u64>,
110}
111
112impl Snap {
113    /// From `pg_snapshot` text, `xmin:xmax:xip,…`.
114    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    /// Whether committed transaction `xid` (the 32 bits `pgoutput` sends,
129    /// within 2³¹ of the snapshot) was visible to the snapshot.
130    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
137/// The `SELECT` that loads an `agg` binding's fold state for
138/// [`Mapper::seed`]: per coordinate (dimension columns as text, `rel`
139/// already quoted), the value text the fold keeps, `count(value_col)` and
140/// `count(*)` ([`FoldRow`]), then a NULL. For `count` no value; for `sum` and
141/// `avg` the finite values' sum and, in groups of their own, the infinities
142/// and NaNs; for `min`, `max` and `count_distinct` one group per value text.
143/// One more row has NULL dimensions and, last, the statement's snapshot.
144pub 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/// Why a batch cannot be applied incrementally; the caller rebuilds.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct Rebuild(pub &'static str);
185
186/// Maps row changes of bound relations to input-cell edits.
187#[derive(Debug, Clone, Default)]
188pub struct Mapper {
189    /// Bindings per relation oid.
190    bindings: HashMap<u32, Vec<Binding>>,
191    /// Item ids per category, by name.
192    items: Items,
193    /// Aggregated bindings' fold state, per measure and coordinate.
194    folds: HashMap<MeasureId, HashMap<Coordinate, Fold>>,
195    /// Per aggregated measure, the snapshot its fold was seeded with.
196    seeded: HashMap<MeasureId, Snap>,
197}
198
199impl Mapper {
200    /// A mapper for `model` (its items name the dimension values) with
201    /// `bindings` per relation oid. Aggregated bindings start with no rows:
202    /// [`Mapper::seed`] them.
203    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    /// The aggregated bindings, with their relation oids: each needs a
220    /// [`Mapper::seed`].
221    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    /// Whether relation `oid` feeds the model.
233    pub fn binds(&self, oid: u32) -> bool {
234        self.bindings.contains_key(&oid)
235    }
236
237    /// The dimension values in `txns`' rows of bound relations (old rows
238    /// and new, as the loader's `::text` reads them) that have no item yet,
239    /// sorted. As the loader does, a row's non-NULL values count even when
240    /// another of its dimensions is NULL.
241    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    /// Add item `id`, named `name`, of category `cat` (minted by the caller).
275    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    /// Load aggregated measure `measure`'s fold state from the rows of its
283    /// [`seed_sql`] (`snap` the snapshot they were read with), its value
284    /// column being of Postgres type `oid`, and set `model`'s cells for it
285    /// to the folds'. A text `min` or `max` keeps no state (its changes
286    /// rebuild) and leaves the model's cells as they are.
287    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                    // A group's finite sum stands for its `n` rows at once.
319                    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    /// Apply the changes of `txns` (tuples of the relations in `rels`) to
339    /// `model`'s input cells, as the loader would see the rows, and return
340    /// one edit per cell whose value the batch changed, with its final value
341    /// (a cell set back to its old value makes none). Changes to relations
342    /// without a binding are ignored. Transactions that ended at or before
343    /// `replay_until` may already be in `model` (see the module docs). On
344    /// `Err` the model's inputs and this mapper's folds are half-updated:
345    /// discard both.
346    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        // Each touched cell, with its value before the batch.
354        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        // As engine values: numbers compare by bits, like the engine's.
362        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        // `old`: None = no old row (insert); Some(None) = an update that
385        // did not send its old row (the replica identity is unchanged).
386        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    /// Whether aggregated binding `b`'s seed already saw transaction `xid`
414    /// (see the module docs).
415    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    /// TRUNCATE of relation `rel`: every cell its bindings feed goes. A
426    /// replayed one (see the module docs) of a binding without `agg` rebuilds.
427    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                // Already in the load, and rows inserted after it may be
442                // in a later batch: reload rather than empty the cells.
443                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
475/// One row change through a binding without `agg`: retract the old row's
476/// cell, then assert the new row's.
477fn 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    // REPLICA IDENTITY FULL sends the whole old row. A key identity sends
488    // only the key (and nothing when it is unchanged); usable only when
489    // the key is exactly the dimension columns, so the old dimension
490    // values are known and the coordinate names one row.
491    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            // A NULL-valued row was no cell (and may share its coordinate
511            // with one that is).
512            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
532/// One row change through an aggregated binding: take the old row out of
533/// its coordinate's fold, put the new row into its (maybe other)
534/// coordinate's, and set each touched cell to its fold's value.
535fn 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    // Only a FULL old row carries the old value (the worker gives the
547    // relations of aggregated bindings REPLICA IDENTITY FULL).
548    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
577/// Postgres orders text by its collation, which a byte order would not
578/// match, so a text `min` or `max` keeps no fold and rebuilds.
579// shortcut: text min/max rebuilds on every change; fold it under COLLATE "C"
580// (or ask Postgres for the order) if it ever matters.
581fn unfoldable(agg: Agg, vt: ValueType) -> bool {
582    matches!(agg, Agg::Min | Agg::Max) && vt == ValueType::Text
583}
584
585/// The coordinate of tuple `t`; `None` when a dimension is NULL (the
586/// loader skips such rows).
587fn 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
601/// Dimension column `col` of tuple `t` as the loader's `col::text` reads
602/// it: a `bool` as `true`/`false` (`pgoutput` sends `t`/`f`), `char(n)`
603/// without its padding, anything else as `pgoutput` sends it. `None` for
604/// NULL.
605fn 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
625/// The coordinate of dimension values `names` (in `b.dims` order); `None`
626/// when one is NULL.
627fn 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
644/// `bpchar`'s oid: its text keeps the padding `::text` strips.
645const BPCHAR: u32 = 1042;
646const BOOL: u32 = 16;
647const FLOAT4: u32 = 700;
648const FLOAT8: u32 = 701;
649const NUMERIC: u32 = 1700;
650
651/// The text of tuple `t`'s value column (`None` = SQL NULL).
652fn 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
661/// The Postgres type of `b`'s value column in `r`.
662fn 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
669/// The value of tuple `t` as a `vt` (`None` = SQL NULL).
670fn 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
677/// Value text `s` as a `vt` (`bpchar`: padded `char(n)` text).
678fn 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        // pgoutput sends t/f; a fold seed reads bool::text, true/false.
682        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
696/// ISO `timestamptz`, `timestamp` or `date` text (the last two in UTC) at
697/// the millisecond, rounded half away from zero as the loader's
698/// `(extract(epoch from v) * 1000)::int8` rounds. `None` for anything else
699/// (`infinity`, BC dates), which the loader cannot read either.
700fn 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/// One coordinate's rows, as its aggregate needs them.
716#[derive(Debug, Clone, Default, PartialEq)]
717struct Fold {
718    /// Rows, NULL values included: a `count` over rows that are all NULL
719    /// is 0, a cell.
720    rows: i64,
721    /// Rows with a value.
722    n: i64,
723    /// `sum`, `avg`: the values' total.
724    total: Total,
725    /// `min`, `max`: each value's row count, in value order;
726    /// `count_distinct`: each distinct value's.
727    values: BTreeMap<Key, i64>,
728}
729
730/// What one row adds to a fold.
731#[derive(Debug, Clone, PartialEq)]
732enum Part {
733    /// A NULL value.
734    Null,
735    /// A value only its presence matters for (`count`).
736    Count,
737    /// A value to total (`sum`, `avg`).
738    Total(Num),
739    /// A value to keep (`min`, `max`, `count_distinct`).
740    Key(Key),
741}
742
743impl Fold {
744    /// Add (`k > 0`) or remove (`k < 0`) `k` rows of `p`. `false` when that
745    /// removes rows the fold does not have.
746    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    /// The cell the rows make, as Postgres's aggregate computes it over a
768    /// value column of type `oid`.
769    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            // Over NULLs only, the other aggregates are NULL: no cell.
778            _ 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
787/// What a row whose value has text `s` (of Postgres type `oid`) adds to an
788/// `agg` fold of a `vt` measure.
789fn 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
798/// [`part_of`] tuple `t`'s value.
799fn 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
806/// Value text `s` of type `oid`, normalized so that two values Postgres
807/// finds equal have the same text: `char(n)` without its padding, a bool as
808/// `t` or `f` (pgoutput's; `bool::text` is `true`), `numeric` without
809/// trailing fractional zeros (`1.50` = `1.5`), and float `-0` as `0`.
810// shortcut: other types compare as their text, exact for the default
811// (deterministic) collations; a nondeterministic collation would need
812// Postgres to compare.
813fn 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/// A value as `min`, `max` and `count_distinct` order and compare it.
824#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
825enum Key {
826    /// A number by its bits made totally ordered ([`ord_bits`]).
827    Num(i64),
828    Bool(bool),
829    /// Milliseconds since the epoch.
830    Date(i64),
831    /// `count_distinct`: the normalized text ([`distinct_text`]).
832    Text(String),
833}
834
835impl Key {
836    /// `v` in `min`/`max` order.
837    fn order(v: Value) -> Result<Key, Rebuild> {
838        Ok(match v {
839            // + 0.0: -0 and 0 are one key, as float8 finds them equal.
840            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            // Text min/max keeps no fold ([`unfoldable`]).
844            _ => 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
858/// `x`'s bits as an `i64` whose order is `x`'s (`f64::total_cmp`).
859fn ord_bits(x: f64) -> i64 {
860    let b = x.to_bits() as i64;
861    b ^ ((((b >> 63) as u64) >> 1) as i64)
862}
863
864/// The inverse of [`ord_bits`] (it is its own inverse).
865fn ord_bits_inverse(k: i64) -> u64 {
866    (k ^ ((((k >> 63) as u64) >> 1) as i64)) as u64
867}
868
869/// A summed number: exact nanos when its text is a plain decimal with at
870/// most 9 decimals (every integer, most `numeric`s), else a float.
871#[derive(Debug, Clone, Copy, PartialEq)]
872enum Num {
873    /// Nanos, and the text's decimal places.
874    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
899/// Plain decimal text (`-12.5`, at most 20 integer digits and 9 decimals)
900/// as integer nanos.
901fn 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
921/// Postgres's numeric `x / n` for `x` = `nanos` / 10⁹ of display scale
922/// `scale` and a count `n` > 0, cast to float8: the quotient rounded (half
923/// away from zero) to `select_div_scale`'s result scale, at least 16
924/// significant digits and at least `scale` decimals, then read as float8
925/// text is.
926fn numeric_div(nanos: i128, scale: i32, n: i64) -> f64 {
927    // The weight (base-10000 digit position) and first base-10000 digit of
928    // a numeric, as numeric.c normalizes it.
929    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    // The quotient exceeds 10^(4·qweight) and rscale keeps 16 digits below
972    // that: a nonzero sum never rounds to zero, so its sign stands.
973    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/// A running total that removing a value undoes exactly: decimal values in
979/// integer nanos (so `sum(int)` and `sum(numeric)` come out as Postgres's
980/// exact sum rounded once to `float8`), other finite values as a float sum,
981/// and the infinities and NaNs counted.
982// shortcut: values with more than 9 decimals or an exponent sum as floats in
983// change order, and a float8 column's decimals sum exactly then round once
984// where Postgres adds float8s in scan order: either can differ from a fresh
985// sum in the last bits (as Postgres's own float8 sum does across scan
986// orders). Integer and numeric(p, s <= 9) sums are exact.
987#[derive(Debug, Clone, Default, PartialEq)]
988struct Total {
989    nanos: i128,
990    /// Rows summed in nanos, per decimal places (a numeric sum's scale is
991    /// its values' largest).
992    // shortcut: a seed group counts once at its sum's scale, so after
993    // deletes the scale can read low; it only matters for an avg of 10^8 or
994    // more with 9 decimals (the result scale is then the column's).
995    scales: [i64; 10],
996    float: f64,
997    /// Rows summed as floats.
998    floats: i64,
999    /// Rows of +∞, -∞ and NaN.
1000    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    /// The mean of `n` rows: in float8 for a `float` column, else as
1021    /// Postgres's numeric `sum / count` ([`numeric_div`]) cast to float8.
1022    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                // Decimal text to float8, as Postgres's numeric_float8 does.
1041                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    /// Time {Q1=11, Q2=12} × Product {P1=21}; input Price (5).
1068    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    /// [`catalog`]'s model with Price[Q1,P1] = 1.
1087    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    /// Relation `f (quarter, product, price, note)` with `keys` as its key.
1102    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    /// Map `changes` over a fresh model with a FULL-identity `f`.
1149    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        // Net no-ops make no edit.
1219        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        // A NULL dimension is no row; a NULL value is no cell.
1235        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        // Deleting a NULL-valued row on Price[Q1,P1] keeps the cell.
1255        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            // Only `note` changed: the cell keeps its value, no edit.
1285            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    /// Map one insert of `price` text onto Price[Q2,P1], with Price of type
1329    /// `vt` in a column of type `type_oid`.
1330    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        // `char(n)` pads; `::text` strips the padding.
1349        assert_eq!(
1350            typed(Text, BPCHAR, "ab  "),
1351            ok(CellValue::Text("ab".into()))
1352        );
1353        let day = 19_723 * 86_400_000; // 2024-01-01
1354        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        // The old row is key-only: its value is unknown, the coordinate unique.
1388        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        // A key that is not the dimensions cannot name the old cell.
1402        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        // Price[Q1,P1] = 1 is already loaded; the slot re-delivers its insert
1420        // and a later update, then a fresh insert onto the same cell.
1421        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        // Rows inserted after it in the batch stay.
1522        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        // An aggregated binding's folds start over.
1527        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    /// Rows `(quarter, price)` of product P1.
1535    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    /// `rows` as [`seed_sql`]'s query returns them for `agg`.
1544    fn seed_rows(agg: Agg, rows: &Rows) -> Vec<FoldRow> {
1545        // (quarter, group value) -> (rows with a value, all rows, their sum)
1546        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    /// What Postgres's `agg … GROUP BY` makes of `rows`, per quarter.
1574    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                    // Postgres orders NaN above every number.
1593                    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    /// The model's Price cells by quarter.
1609    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    /// Fold `changes` into Price bound through `agg`, its model and folds
1624    /// loaded from `seed`; the resulting cells by quarter.
1625    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        // An insert, a delete, and an update that moves a row to Q2.
1648        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        // count counts values; a coordinate of NULLs only counts 0.
1661        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        // sum over NULLs only is NULL: no cell.
1671        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        // min and max keep each value's row count.
1681        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        // Deleting one of two rows that share a key keeps the distinct count.
1699        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        // The coordinate's last row takes its cell with it.
1713        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        // Only a FULL old row carries the old value.
1741        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        // Text min/max is not folded (collation order): it always rebuilds.
1765        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        // The seed saw transactions 3 and 5, not 4 (running) or 6 and later.
1798        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    /// xorshift64: deterministic test randomness.
1824    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    /// Random INSERT, UPDATE (moving rows between coordinates) and DELETE
1836    /// batches: after each, the model's cells and the edits' running result
1837    /// equal what the aggregate gives over the rows now in the table.
1838    #[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            // As text: NaN equals NaN (whatever its bits).
1888            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        // Past 20 integer digits a value sums as a float.
1938        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        // Postgres sums an int or numeric column exactly, then casts.
1951        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        // (sum, its scale, count) -> Postgres's (sum::numeric / count)::float8
1977        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            // Rounding carries through every digit into the integer part.
1991            (
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        // A float8 column averages in float8.
2012        let mut t = Total::default();
2013        t.add(num("11"), 1);
2014        assert_eq!(t.avg(9, true), 11.0 / 9.0);
2015        // So does a numeric column once a value was summed as a float.
2016        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        // pgoutput's 32 bits, across an epoch boundary.
2080        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    /// One [`seed_sql`] row of Price over (`q`, P1).
2132    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    /// Price as a `vt` in a value column of type `oid`, bound through
2142    /// `agg` and seeded with `seed`, after `changes`: its cells by quarter.
2143    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        // NaN orders above every number: it is the max until it goes.
2171        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        // An UPDATE moves the max to Q2; Q1 falls back to its next value.
2181        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        // A bool's seed text is true/false and pgoutput's t/f: one value.
2195        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        // Dates.
2228        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        // count_distinct finds float -0 and 0 equal, as Postgres does.
2243        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        // Postgres: avg(1.50, 2.25, -0.10) and avg(2.25, -0.10).
2253        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        // Postgres: avg(0.1, 0.2, 0.05) = 0.11666666666666666667.
2270        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        // A NaN group of its own makes the sum NaN until its row goes.
2280        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        // The loader skips a NULL dimension; a sum over NULLs only is no cell,
2297        // a count over them is 0.
2298        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        // count(*) below count(value) cannot be: the seed is out of step.
2314        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        // A NULL dimension is no coordinate: not a plain binding's old row,
2328        // nor an aggregated binding's row.
2329        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        // A FULL identity sends every old row; an update without one rebuilds.
2339        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        // Messages other than row changes change nothing.
2347        let origin = Message::Origin {
2348            lsn: 1,
2349            name: "o".into(),
2350        };
2351        assert_eq!(run(vec![origin.clone()]), Ok(vec![]));
2352        // Nor do they, or unbound relations, have unknown dimension values.
2353        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        // A replayed TRUNCATE of a plain binding's relation rebuilds.
2362        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    /// New dimension values without a rebuild: minted into the mapper and
2374    /// the model, the batch's edits bring an engine to a fresh evaluation
2375    /// when its plans serve the grown model ([`Engine::serves`]); a plan
2376    /// that enumerates the grown category does not.
2377    ///
2378    /// [`Engine::serves`]: improv_engine::session::Engine::serves
2379    #[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}