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") };
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"]));
}