latkit 0.2.0

Columnar data drawn as vertices, edges, lines, and areas on the GPU.
Documentation
//! Patches applied to a dataset agree with a plain model of rows, whatever the sequence.

use proptest::prelude::*;

use crate::{Data, Error, Id, Ids, Patch, Row, Table, Value};

/// One row of the model: id, number field `x`, and reference `r` to another row's id.
type ModelRow = (u64, Option<f64>, Option<u64>);

#[derive(Clone, Debug)]
enum Change {
    Set(Vec<(u64, Option<f64>, Option<u64>)>, bool, bool),
    Remove(Vec<u64>),
    Append(Vec<Option<f64>>),
}

fn change() -> impl Strategy<Value = Change> {
    let id = 0..12_u64;
    let row = (id.clone(), proptest::option::of(-10.0..10.0_f64), proptest::option::of(0..14_u64));
    prop_oneof![
        3 => (proptest::collection::vec(row, 0..8), any::<bool>(), any::<bool>()).prop_map(|(rows, x, r)| {
            let mut seen = std::collections::HashSet::new();
            Change::Set(rows.into_iter().filter(|(id, ..)| seen.insert(*id)).collect(), x, r)
        }),
        1 => proptest::collection::vec(id, 0..5).prop_map(Change::Remove),
        1 => proptest::collection::vec(proptest::option::of(-1.0..1.0_f64), 0..1).prop_map(Change::Append),
    ]
}

struct Model {
    rows: Vec<ModelRow>,
    /// Frames of field `s`: per frame, the value of each row present then.
    frames: Vec<Vec<(u64, Option<f64>)>>,
}

impl Model {
    fn apply(&mut self, change: &Change) -> Patch {
        match change {
            Change::Set(rows, write_x, write_r) => {
                let mut table = Table::new("T", rows.iter().map(|r| r.0).collect::<Vec<_>>());
                if *write_x {
                    table = table.field("x", rows.iter().map(|r| r.1).collect::<Vec<_>>());
                }
                if *write_r {
                    table = table.reference("r", "T", rows.iter().map(|r| r.2).collect::<Vec<_>>());
                }
                for &(id, x, r) in rows {
                    let at = self.rows.iter().position(|m| m.0 == id).unwrap_or_else(|| {
                        self.rows.push((id, None, None));
                        self.rows.len() - 1
                    });
                    if *write_x {
                        self.rows[at].1 = x;
                    }
                    if *write_r {
                        self.rows[at].2 = r;
                    }
                }
                Patch::new().set(table)
            }
            Change::Remove(ids) => {
                self.rows.retain(|m| !ids.contains(&m.0));
                Patch::new().remove("T", ids.clone())
            }
            Change::Append(values) => {
                let values: Vec<Option<f64>> =
                    (0..self.rows.len()).map(|i| values.first().copied().flatten().map(|v| v + i as f64)).collect();
                let at = self.frames.len() as f64;
                self.frames.push(self.rows.iter().zip(&values).map(|(m, v)| (m.0, *v)).collect());
                Patch::new().append([at], "T", [("s", values)])
            }
        }
    }

    fn check(&self, data: &Data) {
        let table = data.table("T").expect("the table exists");
        let ids: Vec<Id> = table.ids().collect();
        assert_eq!(ids, self.rows.iter().map(|m| Id::Number(m.0)).collect::<Vec<_>>());
        for &(id, x, r) in &self.rows {
            let row = Row::new("grid", "T", id);
            let has_x = table.kind("x").is_some();
            assert_eq!(data.get(&row, "x"), if has_x { x.map(Value::Number) } else { None }, "x of {id}");
            let wired = r.filter(|r| self.rows.iter().any(|m| m.0 == *r));
            assert_eq!(data.get(&row, "r"), wired.map(|r| Value::Row(Row::new("grid", "T", r))), "r of {id}");
        }
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(256))]

    #[test]
    fn patches_agree_with_the_model(changes in proptest::collection::vec(change(), 1..12)) {
        let mut data = Data::new("grid");
        data.apply(Patch::new().set(Table::new("T", Vec::<u64>::new()))).expect("an empty table");
        let mut model = Model { rows: Vec::new(), frames: Vec::new() };
        for change in &changes {
            let before = data.clone();
            let before_bytes = before.to_bytes();
            let patch = model.apply(change);
            data.apply(patch).expect("valid patches apply");
            model.check(&data);
            prop_assert_eq!(before.to_bytes(), before_bytes, "an earlier snapshot never changes");
            let back = Data::from_bytes(&data.to_bytes()).expect("round trip");
            model.check(&back);
        }
        for (frame, rows) in model.frames.iter().enumerate() {
            for &(id, value) in rows {
                if let Some(at) = model.rows.iter().position(|m| m.0 == id) {
                    let read = data.get_at(&Row::new("grid", "T", id), "s", frame as f64);
                    let want = value.map(|v| Value::Number(f64::from(v as f32)));
                    prop_assert_eq!(read, want, "frame {} of {} (row {})", frame, id, at);
                }
            }
        }
    }
}

#[test]
fn a_failed_patch_changes_nothing() {
    let mut data = Data::new("grid");
    data.apply(Table::new("Bus", vec![1_u64, 2]).field("kv", vec![230.0, 115.0])).expect("valid");
    let before = data.to_bytes();
    let error = data
        .apply(
            Patch::new()
                .set(Table::new("Bus", vec![3_u64]).field("kv", vec![69.0]))
                .set(Table::new("Bus", vec![4_u64]).field("kv", vec!["text"])),
        )
        .expect_err("text into a number field");
    assert!(matches!(&error, Error::Invalid { .. }), "{error}");
    assert_eq!(error.to_string(), "patch[1].Bus.kv: Text values for a Number field");
    assert_eq!(data.to_bytes(), before);
}

#[test]
fn appends_one_after_another_record_as_one_and_name_their_own_errors() {
    let table = || Table::new("Bus", vec![1_u64, 2]).field("kv", vec![230.0, 115.0]);
    let mut one = Data::new("grid");
    one.apply(Patch::new().replace(table()).append(
        [0.0, 1.0, 2.0],
        "Bus",
        [("vm", vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0])],
    ))
    .expect("valid");
    let mut each = Data::new("grid");
    let mut patch = Patch::new().replace(table());
    for f in 0..3 {
        let v = 2.0 * f as f32;
        patch = patch.append([f64::from(f)], "Bus", [("vm", vec![v + 1.0, v + 2.0])]);
    }
    each.apply(patch).expect("valid");
    assert_eq!(each.to_bytes(), one.to_bytes(), "a frame a change records as the frames in one");
    let error = each
        .apply(
            Patch::new()
                .append([3.0], "Bus", [("vm", vec![1.0_f32, 1.0])])
                .append([4.0], "Bus", [("vm", vec![1.0_f32])])
                .append([5.0], "Bus", [("vm", vec![1.0_f32, 1.0])]),
        )
        .expect_err("a frame short of a row");
    assert!(error.to_string().starts_with("patch[1].Bus.vm"), "{error}");
}

#[test]
fn field_lengths_must_match_rows() {
    let mut data = Data::new("grid");
    let error = data.apply(Table::new("Bus", Ids::count(3)).field("kv", vec![1.0, 2.0])).expect_err("short field");
    assert_eq!(error.to_string(), "patch[0].Bus.kv: 2 values for 3 rows");
}

#[test]
fn repeated_ids_are_refused() {
    let mut data = Data::new("grid");
    let error = data.apply(Table::new("Bus", vec!["a", "a"])).expect_err("repeated id");
    assert!(error.to_string().starts_with("patch[0].Bus.ids[1]"), "{error}");
}

#[test]
fn references_to_unknown_tables_are_refused() {
    let mut data = Data::new("grid");
    let error =
        data.apply(Table::new("Branch", Ids::count(1)).reference("from", "Bus", vec![1_u64])).expect_err("no Bus");
    assert_eq!(error.to_string(), "Branch.from: no table Bus");
}

#[test]
fn dangling_references_wire_up_when_the_row_arrives() {
    let mut data = Data::new("grid");
    data.apply(Patch::new().set(Table::new("Bus", vec![1_u64])).set(Table::new("Branch", Ids::count(1)).reference(
        "to",
        "Bus",
        vec![2_u64],
    )))
    .expect("valid");
    let branch = Row::new("grid", "Branch", 0_u64);
    assert_eq!(data.get(&branch, "to"), None);
    data.apply(Table::new("Bus", vec![2_u64])).expect("valid");
    assert_eq!(data.get(&branch, "to"), Some(Value::Row(Row::new("grid", "Bus", 2_u64))));
    data.apply(Patch::new().remove("Bus", vec![1_u64])).expect("valid");
    assert_eq!(data.get(&branch, "to"), Some(Value::Row(Row::new("grid", "Bus", 2_u64))));
}

#[test]
fn inverses_follow_every_change_and_snapshots_keep_theirs() {
    let inverse = |data: &Data, target: usize| {
        let Some(super::table::Field::Reference(r)) = data.table("Branch").and_then(|t| t.field_named("from")) else {
            panic!("a reference");
        };
        r.inverse().of(target).to_vec()
    };
    let mut data = Data::new("grid");
    data.apply(
        Patch::new().set(Table::new("Bus", vec![1_u64, 2, 3])).set(Table::new("Branch", Ids::count(4)).reference(
            "from",
            "Bus",
            vec![1_u64, 2, 1, 3],
        )),
    )
    .expect("valid");
    assert_eq!(inverse(&data, 0), vec![0, 2]);
    assert_eq!(inverse(&data, 2), vec![3]);
    let before = data.clone();
    data.apply(Table::new("Branch", vec![1_u64]).reference("from", "Bus", vec![3_u64])).expect("valid");
    assert_eq!((inverse(&data, 0), inverse(&data, 2)), (vec![0, 2], vec![1, 3]));
    data.apply(Patch::new().remove("Bus", vec![1_u64])).expect("valid");
    // Bus 1 left: what named it names nothing, and the rest moved up a row.
    assert_eq!((inverse(&data, 0), inverse(&data, 1)), (vec![], vec![1, 3]));
    assert_eq!(inverse(&before, 0), vec![0, 2]);
}

/// A dataset of `rows` rows of `Line` with a `flow` sampled over `frames` frames, one a second.
fn sampled(rows: usize, frames: usize) -> Data {
    let mut data = Data::new("grid");
    let at: Vec<f64> = (0..frames).map(|f| f as f64).collect();
    let flow: Vec<f64> = (0..frames * rows).map(|i| ((i * 7919) % 1000) as f64 - 500.0).collect();
    data.apply(
        Patch::new()
            .axis("time", "s")
            .replace(Table::new("Line", Ids::count(rows as u32)).reference("from", "Bus", vec![0_u32; rows]))
            .replace(Table::new("Bus", Ids::count(1)).field("name", vec!["a"]))
            .append(at, "Line", [("flow", flow)]),
    )
    .expect("valid");
    data
}

#[test]
fn describe_names_tables_fields_and_spans() {
    let description = sampled(3, 10).describe();
    assert_eq!(description.axis, Some(["time".into(), "s".into()]));
    assert_eq!(description.span, Some([0.0, 9.0]));
    let line = description.tables.iter().find(|t| t.name.as_str() == "Line").expect("Line");
    assert_eq!(line.rows, 3);
    let json = serde_json::to_value(line).expect("serializes");
    assert_eq!(
        json["fields"],
        serde_json::json!([
            { "name": "flow", "kind": "sampled", "span": [0.0, 9.0] },
            { "name": "from", "kind": "reference", "to": "Bus" },
        ])
    );
}

#[test]
fn envelopes_read_levels_as_frames_would() {
    let (rows, frames) = (3, 700);
    let data = sampled(rows, frames);
    let line = data.table("Line").expect("Line");
    let value = |row: usize, frame: usize| (((frame * rows + row) * 7919) % 1000) as f32 - 500.0;
    for (range, spans) in [([0.0, 699.0], 1), ([0.0, 699.0], 7), ([13.5, 600.0], 5), ([650.0, 2000.0], 3)] {
        let envelope = line.envelope("flow", range, spans, None).expect("an envelope");
        assert_eq!(envelope.at.len(), spans + 1);
        for row in 0..rows {
            for s in 0..spans {
                let (lo, hi) = (envelope.at[s], envelope.at[s + 1]);
                let inside =
                    (0..frames).filter(|&f| f as f64 >= lo && (f as f64) < hi || s == spans - 1 && f as f64 == hi);
                let values: Vec<f32> = inside.map(|f| value(row, f)).collect();
                let least = values.iter().copied().reduce(f32::min).unwrap_or(f32::NAN);
                let greatest = values.iter().copied().reduce(f32::max).unwrap_or(f32::NAN);
                let i = row * spans + s;
                let same = |a: f32, b: f32| a == b || a.is_nan() && b.is_nan();
                assert!(
                    same(envelope.least[i], least) && same(envelope.greatest[i], greatest),
                    "{range:?}/{spans} row {row} span {s}"
                );
            }
        }
    }
    let picked = line.envelope("flow", [0.0, 699.0], 2, Some(&vec![2_u32, 0].into())).expect("an envelope");
    let all = line.envelope("flow", [0.0, 699.0], 2, None).expect("an envelope");
    assert_eq!(picked.least, [&all.least[4..6], &all.least[0..2]].concat());
    assert!(line.envelope("from", [0.0, 1.0], 1, None).is_err());
    assert!(line.envelope("flow", [0.0, 1.0], 1, Some(&vec![9_u32].into())).is_err());
}

#[cfg(feature = "gpu")]
#[test]
fn queries_filter_sort_page_and_select() {
    use crate::{Order, Query, field};
    let mut data = Data::new("grid");
    let loading = vec![40.0, 95.0, 120.0, 95.0, 10.0];
    data.apply(
        Table::new("Branch", Ids::count(5)).field("loading", loading).field("name", vec!["a", "b", "c", "d", "e"]),
    )
    .expect("valid");
    let names = |table: &Table| table.ids().map(|id| table.get(id, "name")).collect::<Vec<_>>();
    let text = |s: &str| Some(Value::Text(s.into()));
    let hot = Query::of("Branch").filter(field("loading").gt(90.0)).sort(field("loading"), Order::Descending);
    let (sorted, kept) = data.query(&hot).expect("a query");
    assert_eq!((names(&sorted), kept), (vec![text("c"), text("b"), text("d")], 3), "ties keep their order");
    let (page, kept) = data.query(&hot.clone().offset(1).limit(1)).expect("a query");
    assert_eq!((names(&page), kept), (vec![text("b")], 3), "the filter kept three, the page one");
    let (least, _) =
        data.query(&Query::of("Branch").sort(field("loading"), Order::Ascending).limit(2)).expect("a query");
    assert_eq!(names(&least), [text("e"), text("a")]);
    let (selected, _) =
        data.query(&Query::of("Branch").limit(1).select("double", field("loading") * 2.0)).expect("a query");
    assert_eq!(selected.fields().map(|(name, _)| name.as_str()).collect::<Vec<_>>(), ["double"]);
    assert_eq!(selected.get(0_u64, "double"), Some(Value::Number(80.0)));
    let json: Query = serde_json::from_value(serde_json::json!({
        "of": "Branch", "filter": { "op": "lt", "args": ["loading", 100] }, "sort": [["loading", "descending"]], "limit": 2,
    }))
    .expect("a query");
    assert_eq!(names(&data.query(&json).expect("a query").0), [text("b"), text("d")]);
    let (named, _) = data.query(&Query::of("Branch").ids([3_u64, 1, 9])).expect("by ids");
    assert_eq!(names(&named), [text("d"), text("b")], "in the order named, unknown ids passed over");
    let (lake, _) = data.query(&Query::of("Branch").filter(field("name").contains("C"))).expect("contains");
    assert_eq!(names(&lake), [text("c")], "ignoring case");
    assert!(data.query(&Query::of("Nope")).is_err());
}

#[cfg(feature = "gpu")]
#[test]
fn queries_read_sampled_fields_at_a_coordinate() {
    use crate::{Query, field};
    let data = sampled(2, 4);
    let (at_two, _) = data.query(&Query::of("Line").at(2.0).select("flow", field("flow"))).expect("a query");
    let flow = |row: usize, frame: usize| Value::Number(((((frame * 2 + row) * 7919) % 1000) as f64) - 500.0);
    assert_eq!(at_two.get(1_u64, "flow"), Some(flow(1, 2)));
    let (last, _) = data.query(&Query::of("Line")).expect("a query");
    assert_eq!(last.kind("flow"), Some(crate::Kind::Number));
    assert_eq!(last.get(0_u64, "flow"), Some(flow(0, 3)));
}

#[test]
fn ids_too_large_for_json_numbers_cross_as_text() {
    let big = (1_u64 << 53) + 1;
    let mut data = Data::new("grid");
    data.apply(Table::new("Bus", vec![big, 7]).field("kv", vec![1.0, 2.0])).expect("valid");
    assert_eq!(serde_json::to_string(&Id::Number(big)).expect("an id"), format!("\"{big}\""));
    assert_eq!(serde_json::to_string(&Id::Number(7)).expect("an id"), "7");
    let bus = data.table("Bus").expect("Bus");
    assert_eq!(bus.get(Id::from(big.to_string()), "kv"), Some(Value::Number(1.0)), "the text names the row");
    assert_eq!(bus.get(big, "kv"), Some(Value::Number(1.0)));
    assert_eq!(bus.values("kv").and_then(|v| v.numbers()), Some(vec![1.0, 2.0]));
    let counted = Table::new("Count", Ids::count(3)).field("x", vec![0.0, 1.0, 2.0]);
    data.apply(counted).expect("valid");
    assert_eq!(data.table("Count").expect("Count").get(Id::from("2"), "x"), Some(Value::Number(2.0)));
}

#[test]
fn schema_changes_only_with_fields() {
    let mut data = Data::new("grid");
    data.apply(Table::new("Bus", Ids::count(2)).field("kv", vec![1.0, 2.0])).expect("valid");
    let schema = data.schema();
    data.apply(Table::new("Bus", Ids::count(3)).field("kv", vec![1.0, 2.0, 3.0])).expect("valid");
    assert_eq!(data.schema(), schema);
    data.apply(Table::new("Bus", Ids::count(1)).field("name", vec!["a"])).expect("valid");
    assert_ne!(data.schema(), schema);
}

#[test]
fn rows_appended_by_id_leave_the_rest_holding() {
    let mut data = Data::new("grid");
    data.apply(Patch::new().set(Table::new("Bus", [10_u64, 20, 30])).append(
        [0.0],
        "Bus",
        [("vm", vec![1.0, 1.1, 1.2])],
    ))
    .expect("valid");
    data.apply(Patch::new().append_rows([1.0], "Bus", [30_u64], [("vm", vec![0.9])])).expect("by id");
    let vm = |id: u64| data.get_at(&Row::new("grid", "Bus", id), "vm", 1.0);
    assert_eq!(vm(10), Some(Value::Number(1.0)), "held");
    assert_eq!(vm(30), Some(Value::Number(f64::from(0.9_f32))), "recorded");
    let error = data.apply(Patch::new().append_rows([2.0], "Bus", [40_u64], [("vm", vec![1.0])]));
    assert!(error.is_err_and(|e| e.to_string().contains("no row")), "rows appended to are rows the table holds");
}

#[test]
fn a_patch_to_a_dataset_changes_it_alone() {
    let mut grid = Data::new("grid");
    grid.apply(Patch::to("grid").set(Table::new("Bus", Ids::count(1)))).expect("its own");
    let error = grid.apply(Patch::to("weather").set(Table::new("Cell", Ids::count(1))));
    assert!(error.is_err_and(|e| e.to_string().contains("weather")));
    let bytes = grid.to_bytes();
    assert_eq!(Patch::from_bytes(&bytes).expect("a patch").data().map(crate::Name::as_str), Some("grid"));
    assert!(Data::from_bytes(&Patch::new().to_bytes()).is_err(), "a dataset is a patch to one");
}