kevy-index 7.0.0

Declarative secondary indexes over prefix domains: range/unique kinds, derived-by-construction, cursor pagination.
Documentation
//! Describe replies and the declarations they carry.

use super::*;
use crate::table::TableIndex;
use crate::table_wire::parse_table_declare;
use crate::value::IndexValue;
use crate::view::{Leaf, Tree, ViewMode, ViewSpec};
use crate::{AnnSpec, FieldSpec, ValueSpec};

fn words(ws: &[&str]) -> Vec<Vec<u8>> {
    ws.iter().map(|w| w.as_bytes().to_vec()).collect()
}

fn declare(ws: &[&str]) -> TableSpec {
    let argv = words(ws);
    let refs: Vec<&[u8]> = argv.iter().map(Vec::as_slice).collect();
    parse_table_declare(&refs).expect("the test declarations are valid")
}

fn field<'a>(d: &'a Described, label: &str) -> &'a Described {
    let Described::Array(items) = d else { panic!("a describe reply is an array") };
    // Labels sit at even positions; a value can spell a label ("kind" ann).
    let at = items.iter().step_by(2).position(|i| *i == Described::Bulk(label.as_bytes().to_vec()));
    &items[at.expect("the label is present") * 2 + 1]
}

const FULL: &[&str] = &[
    "TABLE.DECLARE",
    "order",
    "PREFIX",
    "order:",
    "PK",
    "id",
    "COLUMN",
    "id",
    "str",
    "COLUMN",
    "total",
    "f64",
    "COLUMN",
    "at",
    "i64",
    "COLUMN",
    "who",
    "str",
    "INDEX",
    "at",
    "range",
    "VALUES",
    "total",
    "who",
    "INDEX",
    "id",
    "unique",
    "ORDERPATH",
    "recent",
    "ON",
    "who",
    "THEN",
    "at",
    "DESC",
    "WINDOW",
    "at",
    "SPAN",
    "86400",
    "BUCKET",
    "3600",
    "AUTODECLARE",
    "2",
];

#[test]
fn a_table_declaration_parses_back_to_the_same_spec() {
    let spec = declare(FULL);
    let again = table_declaration(&spec);
    assert_eq!(again, words(FULL));
    let refs: Vec<&[u8]> = again.iter().map(Vec::as_slice).collect();
    assert_eq!(parse_table_declare(&refs).expect("it parses"), spec);
}

#[test]
fn a_table_declaration_leaves_out_what_the_auto_loop_added() {
    let mut spec = declare(FULL);
    spec.indexes.push(TableIndex {
        column: b"who".to_vec(),
        kind: IndexKind::Range,
        values: vec![],
    });
    spec.auto_added.push(b"order.who".to_vec());
    let d = describe_table(&spec);
    assert_eq!(field(&d, "declaration"), &argv(words(FULL)));
    let Described::Array(indexes) = field(&d, "indexes") else { panic!("indexes is an array") };
    assert_eq!(indexes.len(), 3);
    assert_eq!(field(&indexes[2], "auto"), &b("1"));
    assert_eq!(field(&indexes[0], "auto"), &b("0"));
}

#[test]
fn a_compiled_index_names_its_table_and_has_no_declaration() {
    let spec = declare(FULL);
    let tables = [spec.clone()];
    let compiled = spec.compile().expect("compiles");
    for c in &compiled {
        let d = describe_index(c, &tables);
        assert_eq!(field(&d, "table"), &b("order"));
        assert_eq!(field(&d, "declaration"), &b("-"));
    }
    let recent = describe_index(&compiled[2], []);
    let Described::Array(cols) = field(&recent, "composite") else {
        panic!("an orderpath compiles to a composite")
    };
    assert_eq!(cols[1], Described::Array(vec![b("at"), b("i64"), b("desc")]));
}

#[test]
fn a_weighted_text_index_is_spelled_with_fields_and_weights() {
    let mut s =
        IndexSpec::builder(b"doc.body".to_vec(), b"doc:".to_vec(), IndexKind::Text, ValType::Str)
            .with_field(b"title".to_vec())
            .build()
            .unwrap();
    s.fields = vec![FieldSpec { name: b"title".to_vec(), weight: 2.5 }, FieldSpec::new("body")];
    s.with_positions = true;
    s.values = vec![ValueSpec { name: b"year".to_vec(), ty: ValType::I64 }, ValueSpec::new("tag")];
    s.max_bytes = 1024;
    assert_eq!(
        index_declaration(&s),
        words(&[
            "IDX.CREATE",
            "doc.body",
            "ON",
            "PREFIX",
            "doc:",
            "FIELDS",
            "title",
            "body",
            "WEIGHTS",
            "2.5",
            "1",
            "TYPE",
            "str",
            "KIND",
            "text",
            "WITH",
            "POSITIONS",
            "VALUES",
            "year",
            "tag",
            "TYPES",
            "i64",
            "str",
            "MAXMEM",
            "1024",
        ])
    );
}

#[test]
fn a_single_field_index_is_spelled_with_field() {
    let s = IndexSpec::builder("emb", "v:", IndexKind::Ann, ValType::Vector)
        .with_field("vec")
        .with_ann(AnnSpec::new(4).with_distance(1))
        .build()
        .unwrap();
    assert_eq!(
        index_declaration(&s),
        words(&[
            "IDX.CREATE",
            "emb",
            "ON",
            "PREFIX",
            "v:",
            "FIELD",
            "vec",
            "TYPE",
            "vector",
            "KIND",
            "ann",
            "DIM",
            "4",
            "DISTANCE",
            "l2",
            "M",
            "16",
            "EF",
            "200",
        ])
    );
}

#[test]
fn a_view_declaration_writes_the_tree_as_create_reads_it() {
    let leaf = |index: &str, min: IndexValue, max: IndexValue| {
        Box::new(Tree::Leaf(Leaf { index: index.as_bytes().to_vec(), min, max }))
    };
    let v = ViewSpec {
        name: b"hot".to_vec(),
        tree: Tree::Diff(
            leaf("age", IndexValue::I64(18), IndexValue::I64(65)),
            leaf("score", IndexValue::F64(0.5), IndexValue::F64(0.5)),
        ),
        order_by: b"age".to_vec(),
        order: kevy_text::SortOrder::Desc,
        mode: ViewMode::Materialized { top_k: 10 },
        via: Some(b"user:{key}".to_vec()),
    };
    let expected = words(&[
        "VIEW.CREATE",
        "hot",
        "QUERY",
        "(",
        "DIFF",
        "age",
        "RANGE",
        "18",
        "65",
        "score",
        "EQ",
        "0.5",
        ")",
        "ORDER",
        "BY",
        "age",
        "DESC",
        "MODE",
        "materialized",
        "TOPK",
        "10",
        "VIA",
        "user:{key}",
    ]);
    assert_eq!(view_declaration(&v), expected);
    let d = describe_view(&v);
    assert_eq!(field(&d, "query"), &argv(expected[3..13].to_vec()));
    assert_eq!(field(&d, "topk"), &b("10"));
}

#[test]
fn every_index_option_is_described_and_declared() {
    let s = IndexSpec::builder("spend", "o:", IndexKind::Agg, ValType::F64)
        .with_field("total")
        .with_group_by("who")
        .build()
        .unwrap();
    assert_eq!(field(&describe_index(&s, []), "groupby"), &b("who"));
    assert_eq!(
        index_declaration(&s),
        words(&[
            "IDX.CREATE",
            "spend",
            "ON",
            "PREFIX",
            "o:",
            "FIELD",
            "total",
            "TYPE",
            "f64",
            "KIND",
            "agg",
            "GROUPBY",
            "who"
        ])
    );
    let mut text = IndexSpec::builder(b"t".to_vec(), b"d:".to_vec(), IndexKind::Text, ValType::Str)
        .with_field(b"body".to_vec())
        .build()
        .unwrap();
    text.fields = vec![FieldSpec { name: b"body".to_vec(), weight: 3.0 }];
    let declared = index_declaration(&text);
    assert_eq!(
        declared[5..9],
        words(&["FIELDS", "body", "WEIGHTS", "3"]),
        "one weighted field still needs FIELDS"
    );
    for (code, tag) in [(0, "cosine"), (1, "l2"), (2, "ip"), (9, "unknown")] {
        let ann = IndexSpec::builder("e", "v:", IndexKind::Ann, ValType::Vector)
            .with_field("vec")
            .with_ann(AnnSpec::new(2).with_distance(code).with_m(4).with_ef(16))
            .build()
            .unwrap();
        let d = describe_index(&ann, []);
        let Described::Array(params) = field(&d, "ann") else { panic!("ann is a group") };
        assert_eq!(params[3], b(tag), "distance code {code}");
    }
}

#[test]
fn a_composite_with_no_table_still_has_no_declaration() {
    let spec = declare(FULL);
    let recent = spec.compile().expect("compiles").remove(2);
    assert!(recent.composite.is_some());
    let d = describe_index(&recent, []);
    assert_eq!((field(&d, "table"), field(&d, "declaration")), (&b("-"), &b("-")));
}

#[test]
fn a_virtual_view_with_or_and_text_bounds_and_via() {
    let leaf = |index: &str, min: IndexValue, max: IndexValue| {
        Box::new(Tree::Leaf(Leaf { index: index.as_bytes().to_vec(), min, max }))
    };
    let v = ViewSpec {
        name: b"v".to_vec(),
        tree: Tree::Or(
            Box::new(Tree::And(
                leaf(
                    "city",
                    IndexValue::Str(b"kyoto".to_vec()),
                    IndexValue::Str(b"kyoto".to_vec()),
                ),
                leaf("age", IndexValue::I64(1), IndexValue::I64(2)),
            )),
            leaf("name", IndexValue::Str(b"a".to_vec()), IndexValue::Str(b"b".to_vec())),
        ),
        order_by: b"age".to_vec(),
        order: kevy_text::SortOrder::Asc,
        mode: ViewMode::Virtual,
        via: Some(b"u:{key}".to_vec()),
    };
    assert_eq!(
        view_declaration(&v),
        words(&[
            "VIEW.CREATE",
            "v",
            "QUERY",
            "(",
            "OR",
            "(",
            "AND",
            "city",
            "EQ",
            "kyoto",
            "age",
            "RANGE",
            "1",
            "2",
            ")",
            "name",
            "RANGE",
            "a",
            "b",
            ")",
            "ORDER",
            "BY",
            "age",
            "VIA",
            "u:{key}",
        ])
    );
    let d = describe_view(&v);
    assert_eq!(
        (field(&d, "mode"), field(&d, "topk"), field(&d, "via")),
        (&b("virtual"), &b("0"), &b("u:{key}"))
    );
    let bare = ViewSpec { via: None, mode: ViewMode::Materialized { top_k: 0 }, ..v };
    assert_eq!(field(&describe_view(&bare), "via"), &b("-"));
    assert!(
        !view_declaration(&bare).contains(&b"TOPK".to_vec()),
        "an unbounded view writes no TOPK"
    );
}

#[test]
fn a_table_without_window_describes_it_as_absent() {
    let spec = declare(&["TABLE.DECLARE", "t", "PREFIX", "t:", "PK", "id", "COLUMN", "id", "str"]);
    let d = describe_table(&spec);
    assert_eq!((field(&d, "window"), field(&d, "autodeclare")), (&b("-"), &b("0")));
}

#[test]
fn neutrally_weighted_text_fields_are_spelled_without_weights() {
    let s = IndexSpec::builder("doc", "doc:", IndexKind::Text, ValType::Str)
        .with_fields(vec![FieldSpec::new("title"), FieldSpec::new("body")])
        .build()
        .unwrap();
    let got = index_declaration(&s);
    assert_eq!(got[5..8], words(&["FIELDS", "title", "body"]));
    assert!(!got.contains(&b"WEIGHTS".to_vec()), "{got:?}");
}

#[test]
fn a_global_path_sampled_from_rows_writes_global_without_split_points() {
    let spec = declare(&[
        "TABLE.DECLARE",
        "u",
        "PREFIX",
        "u:",
        "PK",
        "id",
        "COLUMN",
        "id",
        "i64",
        "INDEX",
        "id",
        "range",
    ]);
    let mut cat = crate::Catalog::new();
    for s in spec.compile().unwrap() {
        cat.create_with(s, crate::Partitioning::Global { splits: Vec::new() }).unwrap();
    }
    let argv = crate::table_declaration_partitioned(&spec, &cat);
    assert_eq!(argv[argv.len() - 4..], words(&["INDEX", "id", "range", "GLOBAL"]));
}