1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
21#![cfg_attr(coverage_nightly, allow(unused_features))]
22
23pub mod live;
24pub mod pgoutput;
25
26use std::collections::btree_map::Entry;
27use std::collections::{BTreeMap, BTreeSet, HashMap};
28
29use improv_core_model::parser::{self, Definition};
30use improv_core_model::{
31 CategoryId, Coordinate, Expr, Formula, ItemId, Measure, MeasureId, MeasureKind, Model, Name,
32 Value, ValueType,
33};
34use improv_data_source::ItemInterner;
35use improv_engine::{CellValue, CoordKey};
36
37mod delta;
38pub use delta::{
39 apply_edits, cell_changes, coord_json, dependents, measure_ids, plan, quote_literal, scopable,
40 scope_filter, scoped_rel_sql, slot_name, sync_trigger_name, touched_sql, value_json, Plan,
41 Scope,
42};
43
44pub const MAX_NAME_LEN: usize = 56;
47
48#[derive(Debug, Clone, PartialEq, thiserror::Error)]
50pub enum Error {
51 #[error("invalid {what} name {name:?}: must match ^[A-Za-z][A-Za-z0-9_]*$ (at most 56 chars, not \"value\")")]
53 InvalidName {
54 what: &'static str,
56 name: String,
58 },
59 #[error("definition: {0}")]
61 Definition(String),
62 #[error("duplicate coordinate for measure {measure}: {coord:?}")]
64 DuplicateCoordinate {
65 measure: String,
67 coord: Vec<String>,
69 },
70 #[error("{cells} bound input cells exceed improv.max_cells = {max}")]
72 TooManyCells {
73 cells: usize,
75 max: usize,
77 },
78 #[error("catalog: {0}")]
80 Catalog(String),
81}
82
83pub type Result<T> = std::result::Result<T, Error>;
85
86pub fn validate_name(what: &'static str, name: &str) -> Result<()> {
89 let mut chars = name.chars();
90 let ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic())
91 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
92 && name.len() <= MAX_NAME_LEN
93 && !name.eq_ignore_ascii_case("value");
94 if ok {
95 Ok(())
96 } else {
97 Err(Error::InvalidName {
98 what,
99 name: name.to_string(),
100 })
101 }
102}
103
104pub fn quote_ident(s: &str) -> String {
107 format!("\"{}\"", s.replace('"', "\"\""))
108}
109
110pub fn result_schema(model: &str) -> String {
112 format!("improv_{}", model.to_ascii_lowercase())
113}
114
115pub fn result_table(model: &str, measure: &str) -> String {
117 format!(
118 "{}.{}",
119 quote_ident(&result_schema(model)),
120 quote_ident(&measure.to_ascii_lowercase())
121 )
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub struct CategoryRow {
127 pub id: u32,
129 pub name: String,
131}
132
133#[derive(Debug, Clone, PartialEq)]
135pub struct ItemRow {
136 pub id: u32,
138 pub category_id: u32,
140 pub name: String,
142}
143
144#[derive(Debug, Clone, PartialEq)]
146pub struct MeasureRow {
147 pub id: u32,
149 pub name: String,
151 pub value_type: String,
153 pub kind: String,
155 pub formula: Option<String>,
157 pub description: Option<String>,
159 pub categories: Vec<u32>,
161}
162
163impl MeasureRow {
164 pub fn input(id: u32, name: &str, categories: Vec<u32>) -> Self {
166 MeasureRow {
167 id,
168 name: name.to_string(),
169 value_type: "number".into(),
170 kind: "input".into(),
171 formula: None,
172 description: None,
173 categories,
174 }
175 }
176
177 pub fn is_derived(&self) -> bool {
179 self.kind == "derived"
180 }
181}
182
183pub fn value_type_name(vt: ValueType) -> &'static str {
185 match vt {
186 ValueType::Number => "number",
187 ValueType::Boolean => "boolean",
188 ValueType::Text => "text",
189 ValueType::DateTime => "datetime",
190 ValueType::Enum => "enum",
191 }
192}
193
194fn value_type_parse(s: &str) -> Result<ValueType> {
195 Ok(match s {
196 "number" => ValueType::Number,
197 "boolean" => ValueType::Boolean,
198 "text" => ValueType::Text,
199 "datetime" => ValueType::DateTime,
200 "enum" => ValueType::Enum,
201 other => return Err(Error::Catalog(format!("unknown value type {other:?}"))),
202 })
203}
204
205#[derive(Debug, Clone, Default, PartialEq)]
207pub struct Catalog {
208 pub categories: Vec<CategoryRow>,
210 pub items: Vec<ItemRow>,
212 pub measures: Vec<MeasureRow>,
214 pub lambdas: Vec<LambdaRow>,
216}
217
218#[derive(Debug, Clone, PartialEq)]
220pub struct LambdaRow {
221 pub id: u32,
223 pub name: String,
225 pub definition: String,
228}
229
230fn parse_formula(model: &Model, text: &str) -> Result<(String, Formula)> {
233 match parser::parse_definition(model, text).map_err(|e| Error::Definition(e.to_string()))? {
234 Definition::Formula(ft) => Ok((ft.target.0, ft.formula)),
235 Definition::Lambda(lt) => Err(Error::Definition(format!(
236 "{} = LAMBDA(…) defines a named LAMBDA, not a measure",
237 lt.name.0
238 ))),
239 _ => Err(Error::Definition(
240 "SQL(...) and CALL(...) definitions are not supported inside Postgres".into(),
241 )),
242 }
243}
244
245pub fn build_model(catalog: &Catalog) -> Result<Model> {
250 let mut model = Model::new();
251 for c in &catalog.categories {
252 model.add_category(CategoryId(c.id), c.name.clone());
253 }
254 for i in &catalog.items {
255 if !model.categories.contains_key(&CategoryId(i.category_id)) {
256 return Err(Error::Catalog(format!(
257 "item {} references unknown category {}",
258 i.id, i.category_id
259 )));
260 }
261 model.add_item(ItemId(i.id), CategoryId(i.category_id), i.name.clone());
262 }
263 let mut rows: Vec<&MeasureRow> = catalog.measures.iter().collect();
264 rows.sort_by_key(|r| r.id);
265 for r in &rows {
266 if !matches!(
267 (r.kind.as_str(), &r.formula),
268 ("input", _) | ("derived", Some(_))
269 ) {
270 return Err(Error::Catalog(format!(
271 "measure {} has kind {:?} and formula {:?}",
272 r.name, r.kind, r.formula
273 )));
274 }
275 model.add_measure(Measure {
277 id: MeasureId(r.id),
278 name: Name(r.name.clone()),
279 value_type: value_type_parse(&r.value_type)?,
280 categories: r.categories.iter().map(|&c| CategoryId(c)).collect(),
281 kind: MeasureKind::Input,
282 description: r.description.clone(),
283 });
284 }
285 let lambdas: Vec<(u32, String)> = catalog
286 .lambdas
287 .iter()
288 .map(|l| (l.id, l.definition.clone()))
289 .collect();
290 model.load_lambdas(&lambdas).map_err(Error::Catalog)?;
291 for l in &catalog.lambdas {
292 if !model.lambdas[&l.id].name.0.eq_ignore_ascii_case(&l.name) {
293 return Err(Error::Catalog(format!(
294 "lambda {} is named {} but defines {}",
295 l.id, l.name, model.lambdas[&l.id].name.0
296 )));
297 }
298 }
299 for r in rows {
300 let Some(text) = r.formula.as_deref().filter(|_| r.is_derived()) else {
301 continue;
302 };
303 let formula = match serde_json::from_str::<Formula>(text) {
304 Ok(f) => f,
305 Err(_) => {
306 let (target, formula) = parse_formula(&model, text)?;
307 if target != r.name {
308 return Err(Error::Catalog(format!(
309 "measure {} has a formula for {target}",
310 r.name
311 )));
312 }
313 formula
314 }
315 };
316 model
318 .measures
319 .entry(MeasureId(r.id))
320 .and_modify(|m| m.kind = MeasureKind::Derived(formula));
321 }
322 Ok(model)
323}
324
325#[derive(Debug, Clone, PartialEq)]
327pub enum Defined {
328 New(MeasureRow),
330 Unchanged(u32),
332 Replaced {
335 row: MeasureRow,
337 reshaped: bool,
339 },
340 Lambda {
343 row: LambdaRow,
345 callers: Vec<MeasureId>,
348 },
349}
350
351fn is_lambda(text: &str) -> bool {
353 text.split_once('=').is_some_and(|(_, rhs)| {
354 let r = rhs.trim_start();
355 r.get(..6).is_some_and(|h| h.eq_ignore_ascii_case("lambda"))
356 && r[6..].trim_start().starts_with('(')
357 })
358}
359
360pub fn define_lambda(model: &Model, text: &str, replace: bool) -> Result<Option<Defined>> {
370 if !is_lambda(text) {
371 return Ok(None);
372 }
373 let lt = match parser::parse_definition(model, text)
374 .map_err(|e| Error::Definition(e.to_string()))?
375 {
376 Definition::Lambda(lt) => lt,
377 _ => {
378 return Err(Error::Definition(format!(
379 "{text:?} is not Name = LAMBDA(…)"
380 )))
381 }
382 };
383 let name = lt.name.0.as_str();
384 if let Some(m) = model
385 .measures
386 .values()
387 .find(|m| m.name.0.eq_ignore_ascii_case(name))
388 {
389 return Err(Error::Definition(format!(
390 "{} is a measure; name the LAMBDA differently",
391 m.name.0
392 )));
393 }
394 let row = |id| LambdaRow {
395 id,
396 name: name.to_string(),
397 definition: text.trim().to_string(),
398 };
399 match model.lambda_by_name(name) {
400 None => Ok(Some(Defined::Lambda {
401 row: row(model.lambdas.keys().max().map_or(1, |m| m + 1)),
402 callers: Vec::new(),
403 })),
404 Some((_, old)) if !replace => Err(Error::Definition(format!(
405 "LAMBDA {} already exists (improv.define(model, definition, replace => true) \
406 redefines it)",
407 old.name.0
408 ))),
409 Some((id, old)) if old.params == lt.lambda.params && old.body == lt.lambda.body => {
410 Ok(Some(Defined::Unchanged(id)))
411 }
412 Some((id, old)) => {
413 let callers = replace_lambda(model, id, &old.name.0, lt.lambda)?;
414 Ok(Some(Defined::Lambda {
415 row: row(id),
416 callers,
417 }))
418 }
419 }
420}
421
422fn replace_lambda(
425 model: &Model,
426 id: u32,
427 name: &str,
428 new: improv_core_model::NamedLambda,
429) -> Result<Vec<MeasureId>> {
430 let (lambdas, callers) = model.lambda_users(id);
431 let lambda_name = |i: &u32| model.lambdas[i].name.0.clone();
432 for l in &lambdas {
434 if calls_named(&new.body, *l) {
435 return Err(Error::Definition(format!(
436 "{name} cannot call {}: {} calls {name}",
437 lambda_name(l),
438 lambda_name(l)
439 )));
440 }
441 }
442 let mut next = model.clone();
443 next.lambdas.insert(id, new);
444 for l in &lambdas {
445 next.expand_lambdas(&next.lambdas[l].body).map_err(|e| {
446 Error::Definition(format!(
447 "redefining {name} breaks LAMBDA {}, which calls it: {e}",
448 lambda_name(l)
449 ))
450 })?;
451 }
452 keep_shapes(&next, name, &dependents(&next, &callers))?;
453 Ok(callers)
454}
455
456fn keep_shapes(next: &Model, name: &str, ids: &[MeasureId]) -> Result<()> {
460 for d in ids {
461 let m = &next.measures[d];
462 let MeasureKind::Derived(f) = &m.kind else {
463 continue;
464 };
465 let shape =
466 improv_engine::compiler::derived_shape(next, *d, &m.name.0, f).map_err(|e| {
467 Error::Definition(format!(
468 "redefining {name} breaks {}, which depends on it: {e}",
469 m.name.0
470 ))
471 })?;
472 if shape != (m.value_type, m.categories.clone()) {
473 return Err(Error::Definition(format!(
474 "redefining {name} would change the type or dimensions of {}, which depends \
475 on it: redefine or drop {} first",
476 m.name.0, m.name.0
477 )));
478 }
479 }
480 Ok(())
481}
482
483fn calls_named(e: &Expr, id: u32) -> bool {
485 match e {
486 Expr::Call(f, a) => {
487 improv_core_model::functions::named_lambda_of(*f) == Some(id)
488 || a.iter().any(|x| calls_named(x, id))
489 }
490 Expr::UnaryOp(_, a) => calls_named(a, id),
491 Expr::BinaryOp(_, a, b) => calls_named(a, id) || calls_named(b, id),
492 Expr::Literal(_) | Expr::Ref(..) | Expr::Category(_) => false,
493 }
494}
495
496pub fn drop_lambda(model: &Model, name: &str) -> Result<Option<u32>> {
500 let Some((id, l)) = model.lambda_by_name(name) else {
501 return Ok(None);
502 };
503 let (lambdas, measures) = model.lambda_users(id);
504 let mut users: Vec<&str> = measures
505 .iter()
506 .filter_map(|m| model.measures.get(m).map(|m| m.name.0.as_str()))
507 .chain(lambdas.iter().map(|i| model.lambdas[i].name.0.as_str()))
508 .collect();
509 users.sort_unstable();
510 if users.is_empty() {
511 return Ok(Some(id));
512 }
513 Err(Error::Definition(format!(
514 "cannot drop {}: {} still {} it (drop or redefine {} first)",
515 l.name.0,
516 users.join(", "),
517 if users.len() == 1 { "calls" } else { "call" },
518 if users.len() == 1 { "it" } else { "them" }
519 )))
520}
521
522pub fn define(model: &Model, text: &str, id: u32) -> Result<MeasureRow> {
528 match target(model, text) {
529 (name, None) if model.lambda_by_name(name).is_some() => Err(Error::Definition(format!(
530 "{name} is a named LAMBDA; name the measure differently"
531 ))),
532 (_, None) => new_measure(model, text, id),
533 (_, Some(m)) => Err(Error::Definition(format!(
534 "measure {} already exists (improv.define(model, definition, replace => true) \
535 redefines it)",
536 m.name.0
537 ))),
538 }
539}
540
541fn target<'a>(model: &'a Model, text: &'a str) -> (&'a str, Option<&'a Measure>) {
544 let name = text.split('=').next().unwrap_or("").trim();
545 let m = model
546 .measures
547 .values()
548 .find(|m| m.name.0.eq_ignore_ascii_case(name));
549 (name, m)
550}
551
552pub fn define_measure(model: &Model, text: &str, id: u32, replace: bool) -> Result<Defined> {
559 match target(model, text) {
560 (_, Some(m)) if replace => replace_measure(model, text, m),
561 _ => define(model, text, id).map(Defined::New),
562 }
563}
564
565fn new_measure(model: &Model, text: &str, id: u32) -> Result<MeasureRow> {
566 let name = text.split('=').next().unwrap_or("").trim();
569 let (target, formula) = parse_formula(&model.with_provisional(MeasureId(id), name), text)?;
570 validate_name("measure", &target)?;
571 if model
572 .measures
573 .values()
574 .any(|m| m.name.0.eq_ignore_ascii_case(&target))
575 {
576 return Err(Error::Definition(format!(
577 "measure {target} already exists"
578 )));
579 }
580 let (value_type, categories) =
581 improv_engine::compiler::derived_shape(model, MeasureId(id), &target, &formula)
582 .map_err(|e| Error::Definition(e.to_string()))?;
583 if value_type == ValueType::Enum {
584 return Err(no_enum_result(&target));
585 }
586 Ok(MeasureRow {
587 id,
588 name: target,
589 value_type: value_type_name(value_type).into(),
590 kind: "derived".into(),
591 formula: Some(text.trim().to_string()),
592 description: None,
593 categories: categories.iter().map(|c| c.0).collect(),
594 })
595}
596
597fn no_enum_result(name: &str) -> Error {
598 Error::Definition(format!(
599 "{name} is an enum; enum results are not supported yet"
600 ))
601}
602
603fn replace_measure(model: &Model, text: &str, m: &Measure) -> Result<Defined> {
604 let MeasureKind::Derived(old) = &m.kind else {
605 return Err(Error::Definition(format!(
606 "{} is an input measure; only a derived measure can be redefined",
607 m.name.0
608 )));
609 };
610 let (_, formula) = parse_formula(model, text)?;
611 if formula == *old {
612 return Ok(Defined::Unchanged(m.id.0));
613 }
614 let name_of = |id: &MeasureId| model.measures.get(id).map_or("?", |m| m.name.0.as_str());
615 let downstream: Vec<MeasureId> = dependents(model, &[m.id])
616 .into_iter()
617 .filter(|d| *d != m.id)
618 .collect();
619 if let Some(d) = formula
620 .referenced_measures()
621 .iter()
622 .find(|r| downstream.contains(r))
623 {
624 return Err(Error::Definition(format!(
625 "{} cannot read {}: {} depends on {}",
626 m.name.0,
627 name_of(d),
628 name_of(d),
629 m.name.0
630 )));
631 }
632 let (value_type, categories) =
633 improv_engine::compiler::derived_shape(model, m.id, &m.name.0, &formula)
634 .map_err(|e| Error::Definition(e.to_string()))?;
635 if value_type == ValueType::Enum {
636 return Err(no_enum_result(&m.name.0));
637 }
638 let readers = referencing(model, m.id);
639 if value_type != m.value_type && !readers.is_empty() {
640 return Err(Error::Definition(format!(
641 "redefining {} would change its type from {} to {}, but {} {} it: \
642 redefine or drop {} first",
643 m.name.0,
644 value_type_name(m.value_type),
645 value_type_name(value_type),
646 readers.join(", "),
647 if readers.len() == 1 { "reads" } else { "read" },
648 readers.join(", ")
649 )));
650 }
651 let mut next = model.clone();
653 next.add_measure(Measure {
654 kind: MeasureKind::Derived(formula),
655 value_type,
656 categories: categories.clone(),
657 ..m.clone()
658 });
659 keep_shapes(&next, &m.name.0, &downstream)?;
660 Ok(Defined::Replaced {
661 reshaped: value_type != m.value_type || categories != m.categories,
662 row: MeasureRow {
663 id: m.id.0,
664 name: m.name.0.clone(),
665 value_type: value_type_name(value_type).into(),
666 kind: "derived".into(),
667 formula: Some(text.trim().to_string()),
668 description: m.description.clone(),
669 categories: categories.iter().map(|c| c.0).collect(),
670 },
671 })
672}
673
674pub fn referencing(model: &Model, id: MeasureId) -> Vec<String> {
677 let mut names: Vec<String> = model
678 .measures
679 .values()
680 .filter(|m| m.id != id)
681 .filter(
682 |m| matches!(&m.kind, MeasureKind::Derived(f) if f.referenced_measures().contains(&id)),
683 )
684 .map(|m| m.name.0.clone())
685 .collect();
686 names.sort();
687 names
688}
689
690pub fn drop_target(model: &Model, name: &str) -> Result<MeasureId> {
693 let m = model
694 .measures
695 .values()
696 .find(|m| m.name.0.eq_ignore_ascii_case(name))
697 .ok_or_else(|| Error::Definition(format!("measure {name} does not exist")))?;
698 let readers = referencing(model, m.id);
699 if readers.is_empty() {
700 return Ok(m.id);
701 }
702 Err(Error::Definition(format!(
703 "cannot drop {}: {} still {} it (drop or redefine {} first)",
704 m.name.0,
705 readers.join(", "),
706 if readers.len() == 1 {
707 "references"
708 } else {
709 "reference"
710 },
711 if readers.len() == 1 { "it" } else { "them" }
712 )))
713}
714
715#[derive(Debug, Clone, Default, PartialEq)]
719pub struct BoundRows {
720 pub measure_id: u32,
722 pub rows: Vec<(Vec<Option<String>>, Option<CellValue>)>,
724}
725
726#[derive(Debug, Clone, Default, PartialEq)]
728pub struct Loaded {
729 pub new_items: Vec<ItemRow>,
733 pub cells: usize,
735 pub null_dim_rows: usize,
737}
738
739type Shaped = (MeasureId, String, Vec<CategoryId>);
741
742type Distinct<'a> = BTreeMap<CategoryId, BTreeSet<&'a str>>;
744
745fn shape<'a>(model: &Model, bound: &'a [BoundRows]) -> Result<(Vec<Shaped>, Distinct<'a>)> {
748 let mut shaped = Vec::with_capacity(bound.len());
749 let mut distinct = Distinct::new();
750 for b in bound {
751 let m = model
752 .measures
753 .get(&MeasureId(b.measure_id))
754 .ok_or_else(|| {
755 Error::Catalog(format!("binding for unknown measure {}", b.measure_id))
756 })?;
757 for (dims, _) in &b.rows {
758 if dims.len() != m.categories.len() {
759 return Err(Error::Catalog(format!(
760 "{} has {} dimensions but a bound row has {}",
761 m.name.0,
762 m.categories.len(),
763 dims.len()
764 )));
765 }
766 for (c, v) in m.categories.iter().zip(dims) {
767 if let Some(v) = v {
768 distinct.entry(*c).or_default().insert(v);
769 }
770 }
771 }
772 shaped.push((
773 MeasureId(b.measure_id),
774 m.name.0.clone(),
775 m.categories.clone(),
776 ));
777 }
778 Ok((shaped, distinct))
779}
780
781pub fn unknown_items(model: &Model, bound: &[BoundRows]) -> Result<Vec<(CategoryId, String)>> {
785 let known: BTreeSet<(CategoryId, &str)> = model
786 .items
787 .values()
788 .map(|i| (i.category, i.name.0.as_str()))
789 .collect();
790 let (_, distinct) = shape(model, bound)?;
791 Ok(distinct
792 .into_iter()
793 .flat_map(|(c, vs)| vs.into_iter().map(move |v| (c, v)))
794 .filter(|k| !known.contains(k))
795 .map(|(c, v)| (c, v.to_string()))
796 .collect())
797}
798
799pub fn load_inputs(model: &mut Model, bound: &[BoundRows]) -> Result<Loaded> {
805 let known: BTreeSet<ItemId> = model.items.keys().copied().collect();
806 let (shaped, distinct) = shape(model, bound)?;
807
808 let mut interner = ItemInterner::seeded_from(model, 1);
809 for (c, values) in &distinct {
810 for v in values {
811 interner.intern(*c, v);
812 }
813 }
814
815 let mut cells: BTreeMap<(MeasureId, Coordinate), Value> = BTreeMap::new();
816 let mut null_dim_rows = 0;
817 for (b, (mid, name, cats)) in bound.iter().zip(&shaped) {
818 for (dims, value) in &b.rows {
819 let Some(names) = dims
820 .iter()
821 .map(Option::as_deref)
822 .collect::<Option<Vec<_>>>()
823 else {
824 null_dim_rows += 1;
825 continue;
826 };
827 let Some(v) = value else { continue };
828 let v = to_value(v)
829 .ok_or_else(|| Error::Catalog(format!("{name}: {v} is out of range")))?;
830 let coord = Coordinate::from_pairs(
831 cats.iter()
832 .zip(&names)
833 .map(|(c, n)| (*c, interner.intern(*c, n))),
834 );
835 match cells.entry((*mid, coord)) {
836 Entry::Occupied(_) => {
837 return Err(Error::DuplicateCoordinate {
838 measure: name.clone(),
839 coord: names.iter().map(|n| n.to_string()).collect(),
840 })
841 }
842 Entry::Vacant(e) => {
843 e.insert(v);
844 }
845 }
846 }
847 }
848
849 interner.register(model);
850 let mut new_items: Vec<ItemRow> = model
851 .items
852 .values()
853 .filter(|i| !known.contains(&i.id))
854 .map(|i| ItemRow {
855 id: i.id.0,
856 category_id: i.category.0,
857 name: i.name.0.clone(),
858 })
859 .collect();
860 new_items.sort_by_key(|i| i.id);
861 let n = cells.len();
862 for ((mid, coord), v) in cells {
863 model.set_input(mid, coord, v);
864 }
865 Ok(Loaded {
866 new_items,
867 cells: n,
868 null_dim_rows,
869 })
870}
871
872pub fn load_cells(model: &mut Model, cells: &[(u32, String, String)]) -> Result<usize> {
875 use serde_json::Value as J;
876 for (mid, coord, value) in cells {
877 let bad = |what: &str| Error::Catalog(format!("improv.cell {mid} {coord}: {what}"));
878 let vt = model
879 .measures
880 .get(&MeasureId(*mid))
881 .ok_or_else(|| bad("unknown measure"))?
882 .value_type;
883 let dims: BTreeMap<CategoryId, ItemId> =
884 serde_json::from_str(coord).map_err(|e| bad(&e.to_string()))?;
885 let json: J = serde_json::from_str(value).map_err(|e| bad(&e.to_string()))?;
886 let v = json_value(vt, &json)
887 .ok_or_else(|| bad(&format!("{json} is not a {} value", value_type_name(vt))))?;
888 model.set_input(MeasureId(*mid), Coordinate { dims }, v);
889 }
890 Ok(cells.len())
891}
892
893pub(crate) fn json_value(vt: ValueType, json: &serde_json::Value) -> Option<Value> {
896 use serde_json::Value as J;
897 match (vt, json) {
898 (ValueType::Number, J::Number(n)) => n.as_f64().map(Value::Number),
899 (ValueType::Enum, J::Number(n)) => n
900 .as_u64()
901 .and_then(|n| u32::try_from(n).ok())
902 .map(Value::Enum),
903 (ValueType::Boolean, J::Bool(b)) => Some(Value::Boolean(*b)),
904 (ValueType::Text, J::String(s)) => Some(Value::Text(s.clone())),
905 (ValueType::DateTime, J::String(s)) => chrono::DateTime::parse_from_rfc3339(s)
906 .ok()
907 .map(|d| Value::DateTime(d.with_timezone(&chrono::Utc))),
908 _ => None,
909 }
910}
911
912pub fn check_cell_budget(cells: usize, max: usize) -> Result<()> {
914 if cells > max {
915 Err(Error::TooManyCells { cells, max })
916 } else {
917 Ok(())
918 }
919}
920
921pub type ResultRows = BTreeMap<Vec<String>, CellValue>;
923
924fn to_value(v: &CellValue) -> Option<Value> {
927 Some(match v {
928 CellValue::Num(b) => Value::Number(f64::from_bits(*b)),
929 CellValue::Bool(b) => Value::Boolean(*b),
930 CellValue::Text(t) => Value::Text(t.clone()),
931 CellValue::Date(ms) => Value::DateTime(chrono::DateTime::from_timestamp_millis(*ms)?),
932 CellValue::Err(_) => return None,
933 })
934}
935
936fn is_a(vt: ValueType, v: &CellValue) -> bool {
938 matches!(
939 (vt, v),
940 (ValueType::Number, CellValue::Num(_))
941 | (ValueType::Boolean, CellValue::Bool(_))
942 | (ValueType::Text, CellValue::Text(_))
943 | (ValueType::DateTime, CellValue::Date(_))
944 )
945}
946
947fn no_enum() -> Error {
948 Error::Catalog("enum measures have no Postgres column type yet".into())
949}
950
951pub fn bound_value_type(typname: &str, shown: &str, agg: Option<&str>) -> Result<ValueType> {
955 let vt = match typname {
956 "float4" | "float8" | "numeric" | "int2" | "int4" | "int8" => ValueType::Number,
957 "bool" => ValueType::Boolean,
958 "text" | "varchar" | "bpchar" => ValueType::Text,
959 "date" | "timestamp" | "timestamptz" => ValueType::DateTime,
960 _ => {
961 return Err(Error::Catalog(format!(
962 "cannot bind a measure to a column of type {shown}"
963 )))
964 }
965 };
966 match agg.map(agg_sql).transpose()? {
967 Some("count" | "count_distinct") => Ok(ValueType::Number),
968 Some(a @ ("sum" | "avg")) if vt != ValueType::Number => Err(Error::Catalog(format!(
969 "agg {a} needs a number column, not {shown}"
970 ))),
971 _ => Ok(vt),
972 }
973}
974
975pub fn sql_type(vt: ValueType) -> Result<&'static str> {
977 Ok(match vt {
978 ValueType::Number => "double precision",
979 ValueType::Boolean => "boolean",
980 ValueType::Text => "text",
981 ValueType::DateTime => "timestamp with time zone",
982 ValueType::Enum => return Err(no_enum()),
983 })
984}
985
986fn read_expr(vt: ValueType, e: &str) -> Result<String> {
989 Ok(match vt {
990 ValueType::Number => format!("{e}::float8"),
991 ValueType::Boolean => format!("{e}::bool"),
992 ValueType::Text => format!("{e}::text"),
993 ValueType::DateTime => format!("(extract(epoch from {e}::timestamptz) * 1000)::int8"),
994 ValueType::Enum => return Err(no_enum()),
995 })
996}
997
998pub fn check_value_column(table: &str, vt: ValueType, found: &str) -> Result<()> {
1001 if found == sql_type(vt)? {
1002 return Ok(());
1003 }
1004 Err(Error::Catalog(format!(
1005 "{table}.value is {found} but the measure is {}: redefine the measure \
1006 (improv.define(model, definition, replace => true)) to recreate its table",
1007 value_type_name(vt)
1008 )))
1009}
1010
1011pub fn result_rows(
1014 model: &Model,
1015 measure: MeasureId,
1016 cells: &HashMap<CoordKey, CellValue>,
1017) -> Result<ResultRows> {
1018 let m = model
1019 .measures
1020 .get(&measure)
1021 .ok_or_else(|| Error::Catalog(format!("unknown measure {}", measure.0)))?;
1022 let mut out = ResultRows::new();
1023 for (key, val) in cells {
1024 if !is_a(m.value_type, val) {
1025 continue;
1026 }
1027 let names = m
1028 .categories
1029 .iter()
1030 .map(|c| {
1031 key.iter()
1032 .find(|(kc, _)| *kc == c.0)
1033 .and_then(|(_, i)| model.items.get(&ItemId(*i)))
1034 .map(|it| it.name.0.clone())
1035 })
1036 .collect::<Option<Vec<_>>>()
1037 .ok_or_else(|| {
1038 Error::Catalog(format!(
1039 "engine cell {key:?} does not match {}'s dimensions",
1040 m.name.0
1041 ))
1042 })?;
1043 out.insert(names, val.clone());
1044 }
1045 Ok(out)
1046}
1047
1048pub fn dim_columns(model: &Model, measure: MeasureId) -> Result<Vec<String>> {
1050 let m = model
1051 .measures
1052 .get(&measure)
1053 .ok_or_else(|| Error::Catalog(format!("unknown measure {}", measure.0)))?;
1054 m.categories
1055 .iter()
1056 .map(|c| {
1057 model
1058 .categories
1059 .get(c)
1060 .map(|cat| cat.name.0.to_ascii_lowercase())
1061 .ok_or_else(|| Error::Catalog(format!("unknown category {}", c.0)))
1062 })
1063 .collect()
1064}
1065
1066pub fn create_table_sql(table: &str, dim_cols: &[String], vt: ValueType) -> Result<String> {
1070 let vt = sql_type(vt)?;
1071 if dim_cols.is_empty() {
1072 return Ok(format!("CREATE TABLE IF NOT EXISTS {table} (value {vt})"));
1073 }
1074 let cols: Vec<String> = dim_cols
1075 .iter()
1076 .map(|c| format!("{} text NOT NULL", quote_ident(c)))
1077 .collect();
1078 Ok(format!(
1079 "CREATE TABLE IF NOT EXISTS {table} ({}, value {vt}, PRIMARY KEY ({}))",
1080 cols.join(", "),
1081 quoted_list(dim_cols)
1082 ))
1083}
1084
1085pub fn select_result_sql(table: &str, dim_cols: &[String], vt: ValueType) -> Result<String> {
1088 let mut cols: Vec<String> = dim_cols.iter().map(|c| quote_ident(c)).collect();
1089 cols.push(read_expr(vt, "value")?);
1090 Ok(format!("SELECT {} FROM {table}", cols.join(", ")))
1091}
1092
1093pub fn agg_sql(agg: &str) -> Result<&'static str> {
1096 Ok(match agg {
1097 "sum" => "sum",
1098 "avg" => "avg",
1099 "min" => "min",
1100 "max" => "max",
1101 "count" => "count",
1102 "count_distinct" => "count_distinct",
1103 other => {
1104 return Err(Error::Catalog(format!(
1105 "agg must be sum, avg, min, max, count or count_distinct, not {other:?}"
1106 )))
1107 }
1108 })
1109}
1110
1111pub fn agg_call(agg: &str, v: &str) -> Result<String> {
1114 Ok(match agg_sql(agg)? {
1115 "count_distinct" => format!("count(DISTINCT {v})"),
1116 a => format!("{a}({v})"),
1117 })
1118}
1119
1120pub fn select_bound_sql(
1127 rel: &str,
1128 dim_cols: &[String],
1129 value_col: &str,
1130 agg: Option<&str>,
1131 vt: ValueType,
1132) -> Result<String> {
1133 let dims: Vec<String> = dim_cols
1134 .iter()
1135 .map(|c| format!("{}::text", quote_ident(c)))
1136 .collect();
1137 let v = quote_ident(value_col);
1138 Ok(match agg {
1139 None => format!(
1140 "SELECT {}, {} FROM {rel}",
1141 dims.join(", "),
1142 read_expr(vt, &v)?
1143 ),
1144 Some(a) => {
1145 let by: Vec<String> = (1..=dims.len()).map(|i| i.to_string()).collect();
1146 let call = match (agg_sql(a)?, vt) {
1147 ("min", ValueType::Boolean) => format!("bool_and({v})"),
1148 ("max", ValueType::Boolean) => format!("bool_or({v})"),
1149 _ => agg_call(a, &v)?,
1150 };
1151 format!(
1152 "SELECT {}, {} FROM {rel} GROUP BY {}",
1153 dims.join(", "),
1154 read_expr(vt, &call)?,
1155 by.join(", ")
1156 )
1157 }
1158 })
1159}
1160
1161fn quoted_list(cols: &[String]) -> String {
1162 cols.iter()
1163 .map(|c| quote_ident(c))
1164 .collect::<Vec<_>>()
1165 .join(", ")
1166}
1167
1168#[derive(Debug, Clone, PartialEq)]
1172pub struct Statement {
1173 pub sql: String,
1175 pub dims: Vec<Vec<String>>,
1177 pub values: Option<Values>,
1179}
1180
1181#[derive(Debug, Clone, PartialEq)]
1184pub enum Values {
1185 Number(Vec<f64>),
1187 Text(Vec<String>),
1189}
1190
1191fn sql_text(vt: ValueType, v: &CellValue) -> Option<String> {
1193 match (vt, v) {
1194 (ValueType::Boolean, CellValue::Bool(b)) => Some(b.to_string()),
1195 (ValueType::Text, CellValue::Text(t)) => Some(t.clone()),
1196 (ValueType::DateTime, CellValue::Date(ms)) => {
1197 chrono::DateTime::from_timestamp_millis(*ms).map(|d| d.to_rfc3339())
1198 }
1199 _ => None,
1200 }
1201}
1202
1203impl Statement {
1204 pub fn rows(&self) -> usize {
1206 match (self.dims.first(), &self.values) {
1207 (Some(d), _) => d.len(),
1208 (None, Some(Values::Number(v))) => v.len(),
1209 (None, Some(Values::Text(v))) => v.len(),
1210 (None, None) => 1,
1212 }
1213 }
1214}
1215
1216pub fn diff_statements(
1223 table: &str,
1224 dim_cols: &[String],
1225 vt: ValueType,
1226 old: &ResultRows,
1227 new: &ResultRows,
1228) -> Result<Vec<Statement>> {
1229 let n = dim_cols.len();
1230 let columns = |keys: &mut dyn Iterator<Item = &Vec<String>>| {
1231 let mut cols = vec![Vec::new(); n];
1232 for k in keys {
1233 for (col, v) in cols.iter_mut().zip(k) {
1234 col.push(v.clone());
1235 }
1236 }
1237 cols
1238 };
1239 let unnest = |extra: &str| {
1240 let params: Vec<String> = (1..=n).map(|i| format!("${i}::text[]")).collect();
1241 format!("unnest({}{extra})", params.join(", "))
1242 };
1243 let cols = quoted_list(dim_cols);
1244 let mut out = Vec::new();
1245
1246 let changed: Vec<(&Vec<String>, &CellValue)> = new
1247 .iter()
1248 .filter(|(k, v)| old.get(*k) != Some(*v))
1249 .collect();
1250 if !changed.is_empty() {
1251 let bad = |v: &CellValue| {
1252 Error::Catalog(format!("{table}: {v} is not a {}", value_type_name(vt)))
1253 };
1254 let (values, param) = match vt {
1255 ValueType::Number => (
1256 Values::Number(
1257 changed
1258 .iter()
1259 .map(|(_, v)| v.as_num().ok_or_else(|| bad(v)))
1260 .collect::<Result<_>>()?,
1261 ),
1262 "float8[]".to_string(),
1263 ),
1264 _ => (
1265 Values::Text(
1266 changed
1267 .iter()
1268 .map(|(_, v)| sql_text(vt, v).ok_or_else(|| bad(v)))
1269 .collect::<Result<_>>()?,
1270 ),
1271 format!("text[]::{}[]", sql_type(vt)?),
1272 ),
1273 };
1274 let sql = if n == 0 {
1275 format!(
1276 "WITH gone AS (DELETE FROM {table}) \
1277 INSERT INTO {table} (value) SELECT * FROM unnest($1::{param})"
1278 )
1279 } else {
1280 format!(
1281 "INSERT INTO {table} ({cols}, value) SELECT * FROM {} \
1282 ON CONFLICT ({cols}) DO UPDATE SET value = EXCLUDED.value",
1283 unnest(&format!(", ${}::{param}", n + 1))
1284 )
1285 };
1286 out.push(Statement {
1287 sql,
1288 dims: columns(&mut changed.iter().map(|(k, _)| *k)),
1289 values: Some(values),
1290 });
1291 }
1292
1293 let gone: Vec<&Vec<String>> = old.keys().filter(|k| !new.contains_key(*k)).collect();
1294 if !gone.is_empty() {
1295 let sql = if n == 0 {
1296 format!("DELETE FROM {table}")
1297 } else {
1298 format!(
1299 "DELETE FROM {table} WHERE ({cols}) IN (SELECT * FROM {})",
1300 unnest("")
1301 )
1302 };
1303 out.push(Statement {
1304 sql,
1305 dims: columns(&mut gone.into_iter()),
1306 values: None,
1307 });
1308 }
1309 Ok(out)
1310}
1311
1312#[cfg(test)]
1313#[cfg_attr(coverage_nightly, coverage(off))]
1314mod tests {
1315 use super::*;
1316
1317 fn s(v: &str) -> String {
1318 v.to_string()
1319 }
1320
1321 fn catalog() -> Catalog {
1323 Catalog {
1324 categories: vec![
1325 CategoryRow {
1326 id: 1,
1327 name: s("Time"),
1328 },
1329 CategoryRow {
1330 id: 2,
1331 name: s("Product"),
1332 },
1333 ],
1334 items: vec![],
1335 measures: vec![
1336 MeasureRow::input(1, "Price", vec![1, 2]),
1337 MeasureRow::input(2, "Quantity", vec![1, 2]),
1338 ],
1339 lambdas: vec![],
1340 }
1341 }
1342
1343 #[test]
1346 fn named_lambdas_in_the_catalog() {
1347 let mut cat = catalog();
1348 let model = build_model(&cat).unwrap();
1349 let text = "LOGNORM = LAMBDA(x, c, LN1P(x) / LN1P(MAX(x OVER c)))";
1350 let Ok(Some(Defined::Lambda { row, callers })) = define_lambda(&model, text, false) else {
1351 panic!()
1352 };
1353 assert_eq!((row.id, row.name.as_str(), callers), (1, "LOGNORM", vec![]));
1354 cat.lambdas.push(row);
1355 let model = build_model(&cat).unwrap();
1356 cat.measures
1357 .push(define(&model, "PriceN = LOGNORM(Price, Time)", 3).unwrap());
1358 cat.measures
1359 .push(define(&model, "Qn = LOGNORM(Quantity, Product) * 2", 4).unwrap());
1360 let mut model = build_model(&cat).unwrap();
1361 assert_eq!(
1362 model.measures[&MeasureId(3)].categories,
1363 vec![CategoryId(1), CategoryId(2)]
1364 );
1365 load_inputs(&mut model, &sales()).unwrap();
1366 let out = improv_engine::dataflow::evaluate(&model, &[MeasureId(3), MeasureId(4)]).unwrap();
1367 let rows = result_rows(&model, MeasureId(4), &out[&MeasureId(4)]).unwrap();
1368 let want = 2.0 * 100f64.ln_1p() / 200f64.ln_1p();
1370 assert!(
1371 matches!(rows[&vec![s("Q2"), s("Gadget")]], CellValue::Num(v) if (f64::from_bits(v) - want).abs() < 1e-12),
1372 "{rows:?}"
1373 );
1374
1375 let def = |t: &str, replace| define_lambda(&model, t, replace);
1376 assert_eq!(def("Price = Quantity * 2", true), Ok(None));
1377 assert_eq!(def(text, true), Ok(Some(Defined::Unchanged(1))));
1378 assert_eq!(
1379 def("lognorm = LAMBDA( x,c, LN1P(x)/LN1P(MAX(x OVER c)) )", true),
1380 Ok(Some(Defined::Unchanged(1)))
1381 );
1382 let e = |r: Result<Option<Defined>>| r.unwrap_err().to_string();
1383 assert!(e(def(text, false)).contains("already exists"));
1384 let msg = e(def("quantity = LAMBDA(x, x)", false));
1385 assert!(msg.contains("Quantity is a measure"), "{msg}");
1386 assert!(e(def("X = LAMBDA(x, x + Price)", false)).contains("only its parameters"));
1387 assert!(matches!(
1388 def("Half = LAMBDA(x, x / 2)", false),
1389 Ok(Some(Defined::Lambda { row, .. })) if row.id == 2
1390 ));
1391 let err = define_measure(&model, "lognorm = Price * 2", 9, false).unwrap_err();
1392 assert!(err.to_string().contains("is a named LAMBDA"), "{err}");
1393
1394 let twice = "LOGNORM = LAMBDA(x, c, 2 * LN1P(x) / LN1P(MAX(x OVER c)))";
1396 assert!(matches!(
1397 def(twice, true),
1398 Ok(Some(Defined::Lambda { row, callers }))
1399 if row.id == 1 && row.definition == twice
1400 && callers == vec![MeasureId(3), MeasureId(4)]
1401 ));
1402 let msg = e(def("LOGNORM = LAMBDA(x, c, x > MAX(x OVER c))", true));
1403 assert!(
1404 msg.contains("change the type or dimensions of PriceN"),
1405 "{msg}"
1406 );
1407 let msg = e(def("LOGNORM = LAMBDA(x, LN1P(x))", true));
1408 assert!(
1409 msg.contains("breaks PriceN") && msg.contains("takes 1"),
1410 "{msg}"
1411 );
1412 let msg = e(def("LOGNORM = LAMBDA(x, c, MAX(x OVER c))", true));
1413 assert!(
1414 msg.contains("change the type or dimensions of PriceN"),
1415 "{msg}"
1416 );
1417 let mut chain = cat.clone();
1418 chain.lambdas.push(LambdaRow {
1419 id: 2,
1420 name: s("Outer"),
1421 definition: s("Outer = LAMBDA(v, LOGNORM(v, Time) + 1)"),
1422 });
1423 chain.lambdas.push(LambdaRow {
1424 id: 3,
1425 name: s("Again"),
1426 definition: s("Again = LAMBDA(v, Outer(v))"),
1427 });
1428 let chained = build_model(&chain).unwrap();
1429 let msg = define_lambda(&chained, "LOGNORM = LAMBDA(x, c, Outer(x))", true)
1430 .unwrap_err()
1431 .to_string();
1432 assert!(
1433 msg.contains("LOGNORM cannot call Outer: Outer calls LOGNORM"),
1434 "{msg}"
1435 );
1436 for body in ["-Again(x)", "Again(x) + 1", "1 + Again(x)", "ABS(Again(x))"] {
1438 let t = format!("LOGNORM = LAMBDA(x, c, {body})");
1439 let msg = define_lambda(&chained, &t, true).unwrap_err().to_string();
1440 assert!(msg.contains("cannot call Again"), "{body}: {msg}");
1441 }
1442 let e = drop_lambda(&chained, "Outer").unwrap_err().to_string();
1443 assert_eq!(
1444 e,
1445 "definition: cannot drop Outer: Again still calls it (drop or redefine it first)"
1446 );
1447 let msg = define_lambda(&chained, "LOGNORM = LAMBDA(x, x)", true)
1448 .unwrap_err()
1449 .to_string();
1450 assert!(msg.contains("breaks LAMBDA Outer, which calls it"), "{msg}");
1451
1452 let e = drop_lambda(&model, "LogNorm").unwrap_err().to_string();
1453 assert!(
1454 e.contains("cannot drop LOGNORM: PriceN, Qn still call it"),
1455 "{e}"
1456 );
1457 assert_eq!(drop_lambda(&model, "PriceN"), Ok(None));
1458 cat.measures.truncate(2);
1459 assert_eq!(
1460 drop_lambda(&build_model(&cat).unwrap(), "lognorm"),
1461 Ok(Some(1))
1462 );
1463
1464 cat.lambdas[0].name = s("Other");
1466 let e = build_model(&cat).unwrap_err().to_string();
1467 assert!(
1468 e.contains("lambda 1 is named Other but defines LOGNORM"),
1469 "{e}"
1470 );
1471 cat.lambdas[0].definition = s("LOGNORM = LAMBDA(x, Nope(x))");
1472 assert!(build_model(&cat)
1473 .unwrap_err()
1474 .to_string()
1475 .contains("lambda 1:"));
1476 }
1477
1478 fn row(t: &str, p: &str, v: f64) -> (Vec<Option<String>>, Option<CellValue>) {
1479 (vec![Some(s(t)), Some(s(p))], Some(CellValue::num(v)))
1480 }
1481
1482 fn n(v: f64) -> CellValue {
1483 CellValue::num(v)
1484 }
1485
1486 fn sales() -> Vec<BoundRows> {
1488 let mut price = BoundRows {
1489 measure_id: 1,
1490 rows: vec![],
1491 };
1492 let mut qty = BoundRows {
1493 measure_id: 2,
1494 rows: vec![],
1495 };
1496 for (q, t) in ["Q1", "Q2", "Q3", "Q4"].iter().enumerate() {
1497 let q = q as f64 + 1.0;
1498 price.rows.push(row(t, "Widget", 10.0));
1499 price.rows.push(row(t, "Gadget", 20.0));
1500 qty.rows.push(row(t, "Widget", 100.0 * q));
1501 qty.rows.push(row(t, "Gadget", 50.0 * q));
1502 }
1503 vec![price, qty]
1504 }
1505
1506 #[test]
1507 fn name_validation() {
1508 for ok in ["plan", "Revenue", "a1_b", "X"] {
1509 validate_name("model", ok).unwrap();
1510 }
1511 let long = "a".repeat(MAX_NAME_LEN + 1);
1512 for bad in [
1513 "",
1514 "1abc",
1515 "_a",
1516 "a-b",
1517 "a b",
1518 "a\"b",
1519 "Value",
1520 "réal",
1521 long.as_str(),
1522 ] {
1523 assert!(
1524 matches!(validate_name("model", bad), Err(Error::InvalidName { .. })),
1525 "{bad:?} accepted"
1526 );
1527 }
1528 validate_name("model", &"a".repeat(MAX_NAME_LEN)).unwrap();
1529 }
1530
1531 #[test]
1532 fn identifiers_are_quoted() {
1533 assert_eq!(quote_ident("a\"b"), "\"a\"\"b\"");
1534 assert_eq!(result_schema("Plan"), "improv_plan");
1535 assert_eq!(
1536 result_table("Plan", "Revenue"),
1537 "\"improv_plan\".\"revenue\""
1538 );
1539 let cols = vec![s("time"), s("product")];
1540 assert_eq!(
1541 create_table_sql("t", &cols, ValueType::Number).unwrap(),
1542 "CREATE TABLE IF NOT EXISTS t (\"time\" text NOT NULL, \"product\" text NOT NULL, \
1543 value double precision, PRIMARY KEY (\"time\", \"product\"))"
1544 );
1545 assert!(create_table_sql("t", &cols, ValueType::Boolean)
1546 .unwrap()
1547 .contains("value boolean,"));
1548 assert!(create_table_sql("t", &cols, ValueType::Enum).is_err());
1549 assert_eq!(
1550 select_result_sql("t", &cols, ValueType::Number).unwrap(),
1551 "SELECT \"time\", \"product\", value::float8 FROM t"
1552 );
1553 assert_eq!(
1554 select_result_sql("t", &cols, ValueType::DateTime).unwrap(),
1555 "SELECT \"time\", \"product\", \
1556 (extract(epoch from value::timestamptz) * 1000)::int8 FROM t"
1557 );
1558 let num = ValueType::Number;
1559 assert_eq!(
1560 select_bound_sql("public.sales", &cols, "price", None, num).unwrap(),
1561 "SELECT \"time\"::text, \"product\"::text, \"price\"::float8 FROM public.sales"
1562 );
1563 assert_eq!(
1564 select_bound_sql("s", &cols, "price", Some("avg"), num).unwrap(),
1565 "SELECT \"time\"::text, \"product\"::text, avg(\"price\")::float8 FROM s GROUP BY 1, 2"
1566 );
1567 let bool = ValueType::Boolean;
1569 assert_eq!(
1570 select_bound_sql("s", &cols, "ok", Some("max"), bool).unwrap(),
1571 "SELECT \"time\"::text, \"product\"::text, bool_or(\"ok\")::bool FROM s GROUP BY 1, 2"
1572 );
1573 assert_eq!(
1574 select_bound_sql("s", &cols, "ok", Some("min"), bool).unwrap(),
1575 "SELECT \"time\"::text, \"product\"::text, bool_and(\"ok\")::bool FROM s GROUP BY 1, 2"
1576 );
1577 assert_eq!(
1578 select_bound_sql("s", &cols, "ok", Some("count"), num).unwrap(),
1579 "SELECT \"time\"::text, \"product\"::text, count(\"ok\")::float8 FROM s GROUP BY 1, 2"
1580 );
1581 assert_eq!(
1582 select_bound_sql("s", &cols, "price", Some("min"), num).unwrap(),
1583 "SELECT \"time\"::text, \"product\"::text, min(\"price\")::float8 FROM s GROUP BY 1, 2"
1584 );
1585 assert!(select_bound_sql("s", &cols, "price", Some("median; --"), num).is_err());
1586 assert_eq!(agg_sql("count").unwrap(), "count");
1587 assert_eq!(
1588 select_bound_sql("s", &cols, "sha", Some("count_distinct"), num).unwrap(),
1589 "SELECT \"time\"::text, \"product\"::text, count(DISTINCT \"sha\")::float8 \
1590 FROM s GROUP BY 1, 2"
1591 );
1592 }
1593
1594 #[test]
1595 fn catalog_to_model_and_define() {
1596 let mut cat = catalog();
1597 let model = build_model(&cat).unwrap();
1598 assert_eq!(model.categories.len(), 2);
1599 assert!(matches!(
1600 model.measure_by_name("Price").unwrap().kind,
1601 MeasureKind::Input
1602 ));
1603
1604 let rev = define(&model, " Revenue = Price * Quantity ", 3).unwrap();
1605 assert_eq!(rev.formula.as_deref(), Some("Revenue = Price * Quantity"));
1606 assert_eq!(rev.categories, vec![1, 2]);
1607 cat.measures.push(rev);
1608 let model = build_model(&cat).unwrap();
1609 let total = define(&model, "Total = SUM(Revenue OVER Time)", 4).unwrap();
1610 assert_eq!(total.categories, vec![2], "OVER Time drops Time");
1611 cat.measures.push(total);
1612
1613 cat.measures.reverse();
1615 let model = build_model(&cat).unwrap();
1616 assert!(matches!(
1617 model.measure_by_name("Total").unwrap().kind,
1618 MeasureKind::Derived(_)
1619 ));
1620 assert_eq!(
1621 dim_columns(&model, MeasureId(3)).unwrap(),
1622 vec![s("time"), s("product")]
1623 );
1624 }
1625
1626 #[test]
1627 fn define_rejections() {
1628 let model = build_model(&catalog()).unwrap();
1629 let err = |t: &str| define(&model, t, 9).unwrap_err();
1630 assert!(
1631 matches!(err("Price = Quantity * 2"), Error::Definition(m) if m.contains("exists"))
1632 );
1633 assert!(matches!(err("Bad = Nope * 2"), Error::Definition(_)));
1634 assert!(define(&model, "Two = 2", 9).unwrap().categories.is_empty());
1636 assert!(
1637 matches!(err("X = SQL(\"select 1\")"), Error::Definition(m) if m.contains("not supported"))
1638 );
1639 assert!(
1640 matches!(err("X = CALL(f, Price)"), Error::Definition(m) if m.contains("not supported"))
1641 );
1642 assert!(matches!(
1643 err("value = Price * 2"),
1644 Error::InvalidName { .. }
1645 ));
1646 assert!(define(&model, "Big = Price > 2", 9).unwrap().value_type == "boolean");
1647 assert!(matches!(err("garbage"), Error::Definition(_)));
1648 }
1649
1650 #[test]
1652 fn redefine_and_drop() {
1653 let mut cat = catalog();
1654 let model = build_model(&cat).unwrap();
1655 cat.measures
1656 .push(define(&model, "Revenue = Price * Quantity", 3).unwrap());
1657 let model = build_model(&cat).unwrap();
1658 cat.measures
1659 .push(define(&model, "Total = SUM(Revenue OVER Time)", 4).unwrap());
1660 let model = build_model(&cat).unwrap();
1661 let def = |t: &str| define_measure(&model, t, 9, true);
1662 let err = |t: &str| match def(t) {
1663 Err(Error::Definition(m)) => m,
1664 other => panic!("{t}: {other:?}"),
1665 };
1666 assert_eq!(
1667 def("Total = SUM(Revenue OVER Time)"),
1668 Ok(Defined::Unchanged(4))
1669 );
1670 assert_eq!(
1671 def("total=SUM( Revenue OVER Time )"),
1672 Ok(Defined::Unchanged(4))
1673 );
1674 let Ok(Defined::Replaced { row, reshaped }) = def("Revenue = Price * Quantity * 2") else {
1675 panic!()
1676 };
1677 assert_eq!((row.id, row.name.as_str(), reshaped), (3, "Revenue", false));
1678 assert_eq!(
1679 row.formula.as_deref(),
1680 Some("Revenue = Price * Quantity * 2")
1681 );
1682 let Ok(Defined::Replaced { reshaped, row }) = def("Total = SUM(Revenue OVER Product)")
1683 else {
1684 panic!()
1685 };
1686 assert!(reshaped, "Total moves from Product to Time");
1687 assert_eq!(row.categories, vec![1]);
1688 assert!(err("Revenue = Price > 2").contains("from number to boolean, but Total reads it"));
1689 let Ok(Defined::Replaced { row, reshaped }) = def("Total = SUM(Revenue OVER Time) > 1")
1691 else {
1692 panic!()
1693 };
1694 assert_eq!((row.value_type.as_str(), reshaped), ("boolean", true));
1695 assert!(err("Revenue = Total * 2").contains("Total depends on Revenue"));
1696 assert!(matches!(
1698 def("Revenue = SUM(Price OVER Time)"),
1699 Ok(Defined::Replaced { reshaped: true, .. })
1700 ));
1701 let mut wider = cat.clone();
1702 wider
1703 .measures
1704 .push(define(&model, "Double = Revenue * 2", 5).unwrap());
1705 let wide = build_model(&wider).unwrap();
1706 let e = define_measure(&wide, "Revenue = SUM(Price OVER Time)", 9, true).unwrap_err();
1707 assert!(e.to_string().contains("dimensions of Double"), "{e}");
1708 assert!(err("Price = Quantity").contains("input measure"));
1709 assert!(matches!(def("New = Price"), Ok(Defined::New(r)) if r.id == 9));
1710 assert!(define_measure(&model, "Total = Revenue", 9, false)
1711 .is_err_and(|e| e.to_string().contains("replace => true")));
1712
1713 assert_eq!(referencing(&model, MeasureId(3)), vec![s("Total")]);
1714 assert_eq!(drop_target(&model, "total"), Ok(MeasureId(4)));
1715 let e = drop_target(&model, "Price").unwrap_err().to_string();
1716 assert!(
1717 e.contains("cannot drop Price: Revenue still references it"),
1718 "{e}"
1719 );
1720 assert!(drop_target(&model, "Nope").is_err());
1721
1722 cat.measures[2].formula = Some(s("Revenue = Total * 1"));
1724 cat.measures[3].formula = Some(s("Total = SUM(Price OVER Time)"));
1725 let model = build_model(&cat).unwrap();
1726 assert_eq!(
1727 model.measures[&MeasureId(3)].kind,
1728 MeasureKind::Derived(
1729 parser::parse_formula(&model, "Revenue = Total * 1")
1730 .unwrap()
1731 .formula
1732 )
1733 );
1734 }
1735
1736 #[test]
1737 fn bad_catalogs() {
1738 let mut cat = catalog();
1739 cat.items.push(ItemRow {
1740 id: 1,
1741 category_id: 7,
1742 name: s("x"),
1743 });
1744 assert!(matches!(build_model(&cat), Err(Error::Catalog(_))));
1745
1746 let mut cat = catalog();
1747 let mut bad = MeasureRow::input(3, "Revenue", vec![1, 2]);
1748 bad.kind = s("derived");
1749 bad.formula = Some(s("Other = Price * 2"));
1750 cat.measures.push(bad.clone());
1751 assert!(matches!(build_model(&cat), Err(Error::Catalog(_))));
1752 bad.formula = None;
1753 cat.measures[2] = bad.clone();
1754 assert!(matches!(build_model(&cat), Err(Error::Catalog(m)) if m.contains("kind")));
1755 bad.kind = s("input");
1756 bad.value_type = s("money");
1757 cat.measures[2] = bad;
1758 assert!(matches!(build_model(&cat), Err(Error::Catalog(m)) if m.contains("money")));
1759
1760 let mut cat = catalog();
1761 cat.measures[0].value_type = s("text");
1762 let mut model = build_model(&cat).unwrap();
1763 let errs = BoundRows {
1764 measure_id: 1,
1765 rows: vec![(vec![Some(s("Q1")), Some(s("W"))], Some(CellValue::Err(0)))],
1766 };
1767 assert!(matches!(
1768 load_inputs(&mut model, &[errs]),
1769 Err(Error::Catalog(m)) if m.contains("out of range")
1770 ));
1771
1772 let mut model = build_model(&catalog()).unwrap();
1773 let unknown = BoundRows {
1774 measure_id: 99,
1775 rows: vec![],
1776 };
1777 assert!(matches!(
1778 load_inputs(&mut model, &[unknown]),
1779 Err(Error::Catalog(_))
1780 ));
1781 let short = BoundRows {
1782 measure_id: 1,
1783 rows: vec![(vec![Some(s("Q1"))], Some(n(1.0)))],
1784 };
1785 assert!(matches!(
1786 load_inputs(&mut model, &[short]),
1787 Err(Error::Catalog(_))
1788 ));
1789 assert!(dim_columns(&model, MeasureId(99)).is_err());
1790 assert!(result_rows(&model, MeasureId(99), &HashMap::new()).is_err());
1791 }
1792
1793 #[test]
1794 fn catalog_contract_with_storage_pg() {
1795 let mut cat = catalog();
1799 cat.measures[0].categories = vec![2, 1];
1800 cat.measures[0].description = Some(s("list price"));
1801 let model = build_model(&cat).unwrap();
1802 let price = model.measure_by_name("Price").unwrap();
1803 assert_eq!(price.categories, vec![CategoryId(2), CategoryId(1)]);
1804 assert_eq!(price.description.as_deref(), Some("list price"));
1805 assert_eq!(
1806 dim_columns(&model, MeasureId(1)).unwrap(),
1807 vec![s("product"), s("time")]
1808 );
1809
1810 let rev = define(&model, "Revenue = Price * Quantity", 3).unwrap();
1811 let mut with_text = cat.clone();
1812 with_text.measures.push(rev.clone());
1813 let parsed = build_model(&with_text).unwrap().measures[&MeasureId(3)]
1814 .kind
1815 .clone();
1816 let MeasureKind::Derived(f) = parsed else {
1817 panic!("derived")
1818 };
1819 let mut json = rev;
1820 json.formula = Some(serde_json::to_string(&f).unwrap());
1821 cat.measures.push(json);
1822 let model = build_model(&cat).unwrap();
1823 assert_eq!(model.measures[&MeasureId(3)].kind, MeasureKind::Derived(f));
1824 assert_eq!(value_type_name(ValueType::Enum), "enum");
1825 assert_eq!(value_type_name(ValueType::Text), "text");
1826 }
1827
1828 #[test]
1830 fn definition_refusals_name_their_cause() {
1831 let mut cat = catalog();
1832 let model = build_model(&cat).unwrap();
1833 let err = |r: Result<MeasureRow>| r.unwrap_err().to_string();
1834 let msg = err(define(&model, "Half = LAMBDA(x, x / 2)", 9));
1835 assert!(
1836 msg.contains("defines a named LAMBDA, not a measure"),
1837 "{msg}"
1838 );
1839 let msg = err(define(&model, "'Price' = 1", 9));
1841 assert_eq!(msg, "definition: measure Price already exists");
1842 let msg = err(define(&model, "X = Price + \"a\"", 9));
1843 assert!(msg.contains("type mismatch"), "{msg}");
1844 let msg = define_lambda(&model, "'x = LAMBDA(' = 1", false)
1846 .unwrap_err()
1847 .to_string();
1848 assert!(msg.contains("is not Name = LAMBDA(…)"), "{msg}");
1849 cat.measures[0].value_type = s("enum");
1851 cat.measures
1852 .push(define(&model, "D = Quantity * 2", 3).unwrap());
1853 let model = build_model(&cat).unwrap();
1854 let msg = err(define(&model, "E = Price", 9));
1855 assert!(msg.contains("E is an enum; enum results"), "{msg}");
1856 let redefine = |t: &str| define_measure(&model, t, 9, true).unwrap_err().to_string();
1857 assert!(redefine("D = Price").contains("D is an enum"));
1858 assert!(redefine("D = Quantity + \"a\"").contains("type mismatch"));
1859 }
1860
1861 #[test]
1862 fn inputs_keep_their_shape() {
1863 let model = build_model(&catalog()).unwrap();
1864 assert_eq!(
1865 keep_shapes(&model, "X", &[MeasureId(1), MeasureId(2)]),
1866 Ok(())
1867 );
1868 }
1869
1870 #[test]
1873 fn redefinitions_keep_every_dependent_valid() {
1874 let mut cat = catalog();
1875 cat.measures.push(MeasureRow::input(5, "Cost", vec![1, 2]));
1876 let model = build_model(&cat).unwrap();
1877 cat.measures
1878 .push(define(&model, "D = Quantity * 2", 3).unwrap());
1879 let model = build_model(&cat).unwrap();
1880 cat.measures
1881 .push(define(&model, "Prev = PREV(D, Time)", 4).unwrap());
1882 let model = build_model(&cat).unwrap();
1883 let redefine = |t: &str| define_measure(&model, t, 9, true);
1884 let msg = redefine("D = SUM(Quantity OVER Time)")
1885 .unwrap_err()
1886 .to_string();
1887 assert!(
1888 msg.contains("redefining D breaks Prev, which depends on it")
1889 && msg.contains("does not range over Time"),
1890 "{msg}"
1891 );
1892 assert!(matches!(
1893 redefine("D = Cost * 2"),
1894 Ok(Defined::Replaced {
1895 reshaped: false,
1896 ..
1897 })
1898 ));
1899 cat.measures.push(define(&model, "Neg = -D", 6).unwrap());
1901 let model = build_model(&cat).unwrap();
1902 let msg = define_measure(&model, "D = Quantity > 1", 9, true)
1903 .unwrap_err()
1904 .to_string();
1905 assert!(msg.contains("but Neg, Prev read it"), "{msg}");
1906 let msg = drop_target(&model, "d").unwrap_err().to_string();
1907 assert_eq!(
1908 msg,
1909 "definition: cannot drop D: Neg, Prev still reference it (drop or redefine them first)"
1910 );
1911 }
1912
1913 #[test]
1914 fn unbound_cells_from_improv_cell() {
1915 let mut cat = catalog();
1916 cat.items = vec![
1917 ItemRow {
1918 id: 1,
1919 category_id: 1,
1920 name: s("Q1"),
1921 },
1922 ItemRow {
1923 id: 2,
1924 category_id: 2,
1925 name: s("Widget"),
1926 },
1927 ];
1928 for (id, vt) in [(5, "boolean"), (6, "text"), (7, "enum"), (8, "datetime")] {
1929 let mut m = MeasureRow::input(id, &format!("M{id}"), vec![1, 2]);
1930 m.value_type = s(vt);
1931 cat.measures.push(m);
1932 }
1933 let mut model = build_model(&cat).unwrap();
1934 let c = s(r#"{"1": 1, "2": 2}"#);
1935 let cell = |m: u32, v: &str| (m, c.clone(), s(v));
1936 let n = load_cells(
1937 &mut model,
1938 &[
1939 cell(1, "4.5"),
1940 cell(5, "true"),
1941 cell(6, "\"x\""),
1942 cell(7, "3"),
1943 ],
1944 )
1945 .unwrap();
1946 assert_eq!(n, 4);
1947 let coord =
1948 Coordinate::from_pairs([(CategoryId(1), ItemId(1)), (CategoryId(2), ItemId(2))]);
1949 assert_eq!(
1950 model.inputs[&(MeasureId(1), coord.clone())],
1951 Value::Number(4.5)
1952 );
1953 assert_eq!(model.inputs[&(MeasureId(7), coord.clone())], Value::Enum(3));
1954 load_cells(&mut model, &[cell(8, "\"2026-01-01T00:00:00Z\"")]).unwrap();
1955 assert!(matches!(
1956 model.inputs[&(MeasureId(8), coord)],
1957 Value::DateTime(_)
1958 ));
1959
1960 for bad in [
1961 cell(9, "1"),
1962 cell(1, "\"x\""),
1963 cell(8, "\"yesterday\""),
1964 (1, s("[1]"), s("1")),
1965 (1, c.clone(), s("nope")),
1966 ] {
1967 assert!(matches!(
1968 load_cells(&mut model, &[bad]),
1969 Err(Error::Catalog(_))
1970 ));
1971 }
1972 }
1973
1974 #[test]
1975 fn dim_columns_unknown_category() {
1976 let mut cat = catalog();
1977 cat.measures[0].categories = vec![1, 5];
1978 let model = build_model(&cat).unwrap();
1979 assert!(matches!(
1980 dim_columns(&model, MeasureId(1)),
1981 Err(Error::Catalog(_))
1982 ));
1983 }
1984
1985 #[test]
1986 fn rows_to_cells_mint_stable_items() {
1987 let mut cat = catalog();
1988 let mut model = build_model(&cat).unwrap();
1989 let loaded = load_inputs(&mut model, &sales()).unwrap();
1990 assert_eq!(loaded.cells, 16);
1991 assert_eq!(loaded.null_dim_rows, 0);
1992 let names: Vec<(u32, &str)> = loaded
1994 .new_items
1995 .iter()
1996 .map(|i| (i.id, i.name.as_str()))
1997 .collect();
1998 assert_eq!(
1999 names,
2000 vec![
2001 (1, "Q1"),
2002 (2, "Q2"),
2003 (3, "Q3"),
2004 (4, "Q4"),
2005 (5, "Gadget"),
2006 (6, "Widget")
2007 ]
2008 );
2009
2010 cat.items = loaded.new_items.clone();
2013 let mut model = build_model(&cat).unwrap();
2014 let mut more = sales();
2015 more[0].rows.push(row("Q1", "Doohickey", 5.0));
2016 assert_eq!(
2017 unknown_items(&model, &more).unwrap(),
2018 vec![(CategoryId(2), s("Doohickey"))]
2019 );
2020 let again = load_inputs(&mut model, &more).unwrap();
2021 assert_eq!(again.cells, 17);
2022 assert_eq!(
2023 again.new_items,
2024 vec![ItemRow {
2025 id: 7,
2026 category_id: 2,
2027 name: s("Doohickey")
2028 }]
2029 );
2030 let widget = model.items.values().find(|i| i.name.0 == "Widget").unwrap();
2031 assert_eq!(widget.id, ItemId(6));
2032 }
2033
2034 #[test]
2035 fn nulls_and_duplicates() {
2036 let mut model = build_model(&catalog()).unwrap();
2037 let b = BoundRows {
2038 measure_id: 1,
2039 rows: vec![
2040 (vec![None, Some(s("Widget"))], Some(n(1.0))),
2041 (vec![Some(s("Q1")), Some(s("Widget"))], None),
2042 row("Q2", "Widget", 3.0),
2043 ],
2044 };
2045 let loaded = load_inputs(&mut model, &[b]).unwrap();
2046 assert_eq!((loaded.cells, loaded.null_dim_rows), (1, 1));
2047
2048 let mut model = build_model(&catalog()).unwrap();
2049 let dup = BoundRows {
2050 measure_id: 1,
2051 rows: vec![row("Q1", "Widget", 1.0), row("Q1", "Widget", 2.0)],
2052 };
2053 assert_eq!(
2054 load_inputs(&mut model, &[dup]).unwrap_err(),
2055 Error::DuplicateCoordinate {
2056 measure: s("Price"),
2057 coord: vec![s("Q1"), s("Widget")]
2058 }
2059 );
2060 }
2061
2062 #[test]
2063 fn cell_budget() {
2064 check_cell_budget(5, 5).unwrap();
2065 assert_eq!(
2066 check_cell_budget(6, 5).unwrap_err().to_string(),
2067 "6 bound input cells exceed improv.max_cells = 5"
2068 );
2069 }
2070
2071 #[test]
2074 fn engine_results_to_statements() {
2075 let mut cat = catalog();
2076 let model = build_model(&cat).unwrap();
2077 cat.measures
2078 .push(define(&model, "Revenue = Price * Quantity", 3).unwrap());
2079 let model = build_model(&cat).unwrap();
2080 cat.measures
2081 .push(define(&model, "Total = SUM(Revenue OVER Time)", 4).unwrap());
2082 let mut model = build_model(&cat).unwrap();
2083 load_inputs(&mut model, &sales()).unwrap();
2084
2085 let out = improv_engine::dataflow::evaluate(&model, &[MeasureId(3), MeasureId(4)]).unwrap();
2086 let rev = result_rows(&model, MeasureId(3), &out[&MeasureId(3)]).unwrap();
2087 assert_eq!(rev.len(), 8);
2088 assert_eq!(rev[&vec![s("Q3"), s("Widget")]], n(3000.0));
2089 assert_eq!(rev[&vec![s("Q4"), s("Gadget")]], n(4000.0));
2090 let total = result_rows(&model, MeasureId(4), &out[&MeasureId(4)]).unwrap();
2091 assert_eq!(
2093 total,
2094 BTreeMap::from([
2095 (vec![s("Gadget")], n(10000.0)),
2096 (vec![s("Widget")], n(10000.0))
2097 ])
2098 );
2099
2100 let cols = dim_columns(&model, MeasureId(3)).unwrap();
2101 let table = result_table("plan", "Revenue");
2102 let num = ValueType::Number;
2103 let fresh = diff_statements(&table, &cols, num, &ResultRows::new(), &rev).unwrap();
2104 assert_eq!(fresh.len(), 1);
2105 assert_eq!(fresh[0].rows(), 8);
2106 assert!(fresh[0].sql.starts_with(
2107 "INSERT INTO \"improv_plan\".\"revenue\" (\"time\", \"product\", value) \
2108 SELECT * FROM unnest($1::text[], $2::text[], $3::float8[]) ON CONFLICT"
2109 ));
2110 assert!(diff_statements(&table, &cols, num, &rev, &rev)
2111 .unwrap()
2112 .is_empty());
2113 }
2114
2115 #[test]
2116 fn diff_upserts_changes_and_deletes_vanished() {
2117 let cols = vec![s("product")];
2118 let rows = |v: &[(&str, f64)]| -> ResultRows {
2119 v.iter().map(|(k, x)| (vec![s(k)], n(*x))).collect()
2120 };
2121 let old = rows(&[("a", 1.0), ("b", 2.0), ("c", 3.0)]);
2122 let new = rows(&[("a", 1.0), ("b", 2.5), ("d", 4.0)]);
2123 let st = diff_statements("t", &cols, ValueType::Number, &old, &new).unwrap();
2124 assert_eq!(st.len(), 2);
2125 assert_eq!(st[0].dims, vec![vec![s("b"), s("d")]]);
2126 assert_eq!(st[0].values, Some(Values::Number(vec![2.5, 4.0])));
2127 assert_eq!(
2128 st[1].sql,
2129 "DELETE FROM t WHERE (\"product\") IN (SELECT * FROM unnest($1::text[]))"
2130 );
2131 assert_eq!(st[1].dims, vec![vec![s("c")]]);
2132 assert_eq!(st[1].values, None);
2133 assert_eq!(st.iter().map(Statement::rows).sum::<usize>(), 3);
2134 assert_eq!(
2136 Statement {
2137 sql: s(""),
2138 dims: vec![],
2139 values: None
2140 }
2141 .rows(),
2142 1
2143 );
2144 }
2145
2146 #[test]
2149 fn dimensionless_results() {
2150 let mut cat = catalog();
2151 let model = build_model(&cat).unwrap();
2152 let row = define(&model, "MaxQ = MAX(Quantity OVER Time, Product)", 3).unwrap();
2153 assert!(row.categories.is_empty());
2154 cat.measures.push(row);
2155 let mut model = build_model(&cat).unwrap();
2156 load_inputs(&mut model, &sales()).unwrap();
2157 let out = improv_engine::dataflow::evaluate(&model, &[MeasureId(3)]).unwrap();
2158 let rows = result_rows(&model, MeasureId(3), &out[&MeasureId(3)]).unwrap();
2159 assert_eq!(rows, BTreeMap::from([(vec![], n(400.0))]));
2160 let cols = dim_columns(&model, MeasureId(3)).unwrap();
2161 assert!(cols.is_empty());
2162 assert_eq!(
2163 create_table_sql("t", &cols, ValueType::Number).unwrap(),
2164 "CREATE TABLE IF NOT EXISTS t (value double precision)"
2165 );
2166 assert_eq!(
2167 select_result_sql("t", &cols, ValueType::Number).unwrap(),
2168 "SELECT value::float8 FROM t"
2169 );
2170 let num = ValueType::Number;
2171 let none = ResultRows::new();
2172 let st = diff_statements("t", &cols, num, &none, &rows).unwrap();
2173 assert_eq!(st.len(), 1);
2174 assert_eq!(
2175 st[0].sql,
2176 "WITH gone AS (DELETE FROM t) INSERT INTO t (value) SELECT * FROM unnest($1::float8[])"
2177 );
2178 assert_eq!((st[0].rows(), st[0].dims.len()), (1, 0));
2179 assert!(diff_statements("t", &cols, num, &rows, &rows)
2180 .unwrap()
2181 .is_empty());
2182 let st = diff_statements("t", &cols, num, &rows, &none).unwrap();
2183 assert_eq!((st[0].sql.as_str(), st[0].rows()), ("DELETE FROM t", 1));
2184 let text = ValueType::Text;
2185 let one = BTreeMap::from([(vec![], CellValue::Text(s("x")))]);
2186 let st = diff_statements("t", &cols, text, &none, &one).unwrap();
2187 assert!(
2188 st[0].sql.ends_with("unnest($1::text[]::text[])"),
2189 "{}",
2190 st[0].sql
2191 );
2192 assert_eq!(st[0].rows(), 1);
2193 }
2194
2195 #[test]
2196 fn result_rows_skips_non_numeric_and_rejects_foreign_coords() {
2197 let mut cat = catalog();
2198 cat.items = vec![
2199 ItemRow {
2200 id: 1,
2201 category_id: 1,
2202 name: s("Q1"),
2203 },
2204 ItemRow {
2205 id: 2,
2206 category_id: 2,
2207 name: s("Widget"),
2208 },
2209 ];
2210 let model = build_model(&cat).unwrap();
2211 let cells = HashMap::from([
2212 (vec![(1, 1), (2, 2)], CellValue::num(7.0)),
2213 (vec![(1, 1), (2, 2), (3, 3)], CellValue::Text(s("x"))),
2214 ]);
2215 let rows = result_rows(&model, MeasureId(1), &cells).unwrap();
2216 assert_eq!(
2217 rows,
2218 ResultRows::from([(vec![s("Q1"), s("Widget")], n(7.0))])
2219 );
2220 let foreign = HashMap::from([(vec![(1, 1)], CellValue::num(1.0))]);
2221 assert!(result_rows(&model, MeasureId(1), &foreign).is_err());
2222 }
2223
2224 #[test]
2227 fn typed_bindings_and_results() {
2228 let vt = |t: &str, agg| bound_value_type(t, t, agg);
2229 for (t, want) in [
2230 ("numeric", ValueType::Number),
2231 ("int8", ValueType::Number),
2232 ("bool", ValueType::Boolean),
2233 ("varchar", ValueType::Text),
2234 ("bpchar", ValueType::Text),
2235 ("date", ValueType::DateTime),
2236 ("timestamptz", ValueType::DateTime),
2237 ] {
2238 assert_eq!(vt(t, None).unwrap(), want, "{t}");
2239 }
2240 assert_eq!(vt("text", Some("count")).unwrap(), ValueType::Number);
2241 assert_eq!(
2242 vt("text", Some("count_distinct")).unwrap(),
2243 ValueType::Number
2244 );
2245 assert_eq!(vt("bool", Some("max")).unwrap(), ValueType::Boolean);
2246 assert!(matches!(vt("jsonb", None), Err(Error::Catalog(m)) if m.contains("type jsonb")));
2247 assert!(matches!(vt("text", Some("sum")), Err(Error::Catalog(m)) if m.contains("number")));
2248 assert!(matches!(vt("bool", Some("avg")), Err(Error::Catalog(m)) if m.contains("agg avg")));
2249 assert_eq!(vt("int4", Some("sum")).unwrap(), ValueType::Number);
2250 assert_eq!(vt("numeric", Some("avg")).unwrap(), ValueType::Number);
2251 assert!(vt("int4", Some("median")).is_err());
2252 assert_eq!(sql_type(ValueType::Text).unwrap(), "text");
2253 assert!(sql_type(ValueType::Enum).is_err());
2254 let cols = vec![s("p")];
2255 assert_eq!(
2256 select_bound_sql("s", &cols, "v", None, ValueType::Text).unwrap(),
2257 "SELECT \"p\"::text, \"v\"::text FROM s"
2258 );
2259 assert!(select_bound_sql("s", &cols, "v", None, ValueType::Enum).is_err());
2260 check_value_column("t", ValueType::Boolean, "boolean").unwrap();
2261 assert!(matches!(
2262 check_value_column("t", ValueType::Boolean, "double precision"),
2263 Err(Error::Catalog(m)) if m.contains("replace => true")
2264 ));
2265
2266 let mut cat = catalog();
2267 cat.measures[0].value_type = s("text");
2268 cat.measures[1].value_type = s("datetime");
2269 let mut model = build_model(&cat).unwrap();
2270 let at = |v: CellValue| vec![(vec![Some(s("Q1")), Some(s("Widget"))], Some(v))];
2271 let bound = [
2272 BoundRows {
2273 measure_id: 1,
2274 rows: at(CellValue::Text(s("tools"))),
2275 },
2276 BoundRows {
2277 measure_id: 2,
2278 rows: at(CellValue::Date(86_400_000)),
2279 },
2280 ];
2281 load_inputs(&mut model, &bound).unwrap();
2282 let got: BTreeSet<String> = model.inputs.values().map(|v| format!("{v:?}")).collect();
2283 assert!(got.contains("Text(\"tools\")"), "{got:?}");
2284 assert!(
2285 got.iter().any(|v| v.starts_with("DateTime(1970-01-02")),
2286 "{got:?}"
2287 );
2288 let huge = [BoundRows {
2289 measure_id: 2,
2290 rows: at(CellValue::Date(i64::MAX)),
2291 }];
2292 assert!(load_inputs(&mut model, &huge).is_err());
2293 cat.measures[1].value_type = s("boolean");
2294 let mut model = build_model(&cat).unwrap();
2295 let flag = [BoundRows {
2296 measure_id: 2,
2297 rows: at(CellValue::Bool(true)),
2298 }];
2299 load_inputs(&mut model, &flag).unwrap();
2300 assert_eq!(
2301 model.inputs.values().collect::<Vec<_>>(),
2302 [&Value::Boolean(true)]
2303 );
2304
2305 let cols = vec![s("product")];
2306 let one = |v: CellValue| ResultRows::from([(vec![s("a")], v)]);
2307 let st = diff_statements(
2308 "t",
2309 &cols,
2310 ValueType::Boolean,
2311 &ResultRows::new(),
2312 &one(CellValue::Bool(true)),
2313 )
2314 .unwrap();
2315 assert!(st[0].sql.contains("$2::text[]::boolean[]"), "{}", st[0].sql);
2316 assert_eq!(st[0].values, Some(Values::Text(vec![s("true")])));
2317 let dt = ValueType::DateTime;
2318 let st =
2319 diff_statements("t", &cols, dt, &ResultRows::new(), &one(CellValue::Date(0))).unwrap();
2320 assert_eq!(
2321 st[0].values,
2322 Some(Values::Text(vec![s("1970-01-01T00:00:00+00:00")]))
2323 );
2324 let text = one(CellValue::Text(s("x")));
2325 assert!(diff_statements("t", &cols, ValueType::Number, &ResultRows::new(), &text).is_err());
2326 assert!(diff_statements("t", &cols, dt, &ResultRows::new(), &text).is_err());
2327 }
2328}