hypersteeldb 0.5.5

A database that compiles questions instead of guessing answers: typed vocabulary discovered from your documents, queries type-checked before they run, roaring-bitmap set algebra over reified hyperedges, and Dempster-Shafer evidence with an explicit conflict guard.
Documentation
//! End-to-end projector → index → IKL query, on synthetic files and (if present) the real corpus.

use std::io::Write;
use steeldb::Corpus;

fn tmp(name: &str, body: &str) -> std::path::PathBuf {
    let p = std::env::temp_dir().join(format!("steeldb_test_{name}"));
    let mut f = std::fs::File::create(&p).unwrap();
    f.write_all(body.as_bytes()).unwrap();
    p
}

#[test]
fn incremental_corpus_grows() {
    // realtime/hot-model ingest core: append situations one at a time; queries + facet discovery see
    // each addition immediately.
    let mut c = Corpus::new_incremental("live", vec!["row".into()], steeldb::CorpusKind::Csv);
    assert_eq!(c.stats().situations, 0);
    c.add_situation(vec!["country/japan".into(), "make/toyota".into()], vec!["r1".into()]);
    c.add_situation(vec!["country/japan".into(), "make/honda".into()], vec!["r2".into()]);
    c.add_situation(vec!["country/korea".into(), "make/kia".into()], vec!["r3".into()]);
    let s = c.stats();
    assert_eq!(s.situations, 3);
    assert_eq!(c.query("country/japan", 10).count, 2);
    assert_eq!(c.query("(and country/japan make/toyota)", 10).count, 1);
    assert_eq!(c.query("(or make/toyota make/kia)", 10).count, 2);
    assert!(c.facet_tokens("make", 10).iter().any(|(t, _)| t == "make/toyota"));
}

#[test]
fn numeric_range_query() {
    // the `(num field op value)` numeric layer over CSV numeric columns
    let p = tmp("veh_num.csv", "make,year,range\nalpha,2020,500\nbeta,2022,800\ngamma,2019,400\n");
    let c = Corpus::from_csv(&p).unwrap();
    assert_eq!(c.query("(num range ge 500)", 10).count, 2); // 500, 800
    assert_eq!(c.query("(num year lt 2021)", 10).count, 2); // 2020, 2019
    assert_eq!(c.query("(num range gt 900)", 10).count, 0);
    assert_eq!(c.query("(and make/beta (num range ge 400))", 10).count, 1);
    assert_eq!(c.query("(num range eq 800)", 10).count, 1);
}

#[test]
fn csv_projector_query() {
    let p = tmp(
        "cars.csv",
        "make,country,status\nToyota,Japan,active\nHyundai,Korea,active\nToyota,Japan,closed\n",
    );
    let c = Corpus::from_csv(&p).unwrap();
    let s = c.stats();
    assert_eq!(s.situations, 3);

    // country/japan ∩ not status/closed  → only the first row
    let out = c.query("(and country/japan (not status/closed))", 10);
    assert_eq!(out.count, 1);
    assert_eq!(out.hits[0].cells[0], "Toyota");

    // or of two makes
    assert_eq!(c.query("(or make/toyota make/hyundai)", 10).count, 3);
}

#[test]
fn json_projector_flattens_nested() {
    let p = tmp(
        "recs.ndjson",
        r#"{"user":{"country":"thailand"},"tags":["a","b"]}
{"user":{"country":"korea"},"tags":["b"]}
"#,
    );
    let c = Corpus::from_json(&p).unwrap();
    assert_eq!(c.stats().situations, 2);
    assert_eq!(c.query("user/country/thailand", 10).count, 1);
    assert_eq!(c.query("tags/b", 10).count, 2); // array → one token per element
}

#[test]
fn real_situations_if_present() {
    // Set STEELDB_TEST_JSONL to a situations .jsonl to exercise the real-corpus path; else skip.
    let path = match std::env::var("STEELDB_TEST_JSONL") {
        Ok(p) => std::path::PathBuf::from(p),
        Err(_) => return,
    };
    if !path.exists() {
        return;
    }
    let c = Corpus::from_jsonl(&path, Some(50_000)).unwrap();
    assert_eq!(c.stats().situations, 50_000);
    let out = c.query("class/vegetation", 5);
    assert!(out.count > 0, "expected vegetation hits");
    assert!(out.micros < 50_000.0, "query should be sub-50ms, was {} µs", out.micros);
}