use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use modelvault_core::index::{decode_index_payload, encode_index_payload, IndexEntry, IndexState};
use modelvault_core::query::{Predicate, Query};
use modelvault_core::record::RowValue;
use modelvault_core::schema::{Constraint, FieldDef, FieldPath, IndexDef, IndexKind, Type};
use modelvault_core::validation::{
ensure_pk_type_primitive, validate_top_level_row, validate_value,
};
use modelvault_core::{Database, ScalarValue};
fn field(name: &str, ty: Type, constraints: Vec<Constraint>) -> FieldDef {
FieldDef {
path: FieldPath(vec![Cow::Owned(name.to_string())]),
ty,
constraints,
}
}
#[test]
fn index_payload_decode_covers_error_branches() {
let mut bad_ver = encode_index_payload(&[]);
bad_ver[0..2].copy_from_slice(&999u16.to_le_bytes());
assert!(decode_index_payload(&bad_ver).is_err());
let e = IndexEntry {
collection_id: 1,
index_name: "i".to_string(),
kind: IndexKind::Unique,
op: modelvault_core::index::IndexOp::Insert,
index_key: vec![1],
pk_key: vec![2],
};
let mut bytes = encode_index_payload(&[e]);
bytes[10] = 9;
assert!(decode_index_payload(&bytes).is_err());
let mut ok = encode_index_payload(&[]);
ok.extend_from_slice(&[1, 2, 3]);
assert!(decode_index_payload(&ok).is_err());
let trunc = encode_index_payload(&[])[..3].to_vec();
assert!(decode_index_payload(&trunc).is_err());
}
#[test]
fn index_state_non_unique_and_unique_branches() {
let mut st = IndexState::default();
st.apply(IndexEntry {
collection_id: 1,
index_name: "u".to_string(),
kind: IndexKind::Unique,
op: modelvault_core::index::IndexOp::Insert,
index_key: b"k".to_vec(),
pk_key: b"p".to_vec(),
})
.unwrap();
st.apply(IndexEntry {
collection_id: 1,
index_name: "u".to_string(),
kind: IndexKind::Unique,
op: modelvault_core::index::IndexOp::Insert,
index_key: b"k".to_vec(),
pk_key: b"p".to_vec(),
})
.unwrap();
assert_eq!(st.unique_lookup(1, "u", b"k"), Some(&b"p"[..]));
st.apply(IndexEntry {
collection_id: 1,
index_name: "n".to_string(),
kind: IndexKind::NonUnique,
op: modelvault_core::index::IndexOp::Insert,
index_key: b"k".to_vec(),
pk_key: b"p1".to_vec(),
})
.unwrap();
st.apply(IndexEntry {
collection_id: 1,
index_name: "n".to_string(),
kind: IndexKind::NonUnique,
op: modelvault_core::index::IndexOp::Insert,
index_key: b"k".to_vec(),
pk_key: b"p2".to_vec(),
})
.unwrap();
let got = st.non_unique_lookup(1, "n", b"k").unwrap();
let set: BTreeSet<Vec<u8>> = got.into_iter().collect();
assert_eq!(set, BTreeSet::from([b"p1".to_vec(), b"p2".to_vec()]));
}
#[test]
fn validation_covers_more_constraints_and_wrong_type_messages() {
assert!(ensure_pk_type_primitive(&Type::Optional(Box::new(Type::Int64))).is_err());
let mut path = vec!["x".to_string()];
assert!(validate_value(&mut path, &Type::Bool, &[], &RowValue::Int64(1)).is_err());
let mut path = vec!["n".to_string()];
assert!(validate_value(
&mut path,
&Type::Int64,
&[Constraint::MinI64(5)],
&RowValue::Int64(4)
)
.is_err());
assert!(validate_value(
&mut path,
&Type::Uint64,
&[Constraint::MaxU64(2)],
&RowValue::Uint64(3)
)
.is_err());
assert!(validate_value(
&mut path,
&Type::Float64,
&[Constraint::MinF64(1.5)],
&RowValue::Float64(1.0)
)
.is_err());
let mut path = vec!["s".to_string()];
assert!(validate_value(
&mut path,
&Type::String,
&[Constraint::MinLength(3)],
&RowValue::String("hi".into())
)
.is_err());
assert!(validate_value(
&mut path,
&Type::String,
&[Constraint::Regex("^a+$".into())],
&RowValue::String("bbb".into())
)
.is_err());
let mut path = vec!["w".to_string()];
assert!(validate_value(
&mut path,
&Type::String,
&[Constraint::MinI64(1)],
&RowValue::String("x".into())
)
.is_err());
}
#[test]
fn query_iter_covers_scan_and_index_paths_with_limits_and_residual() {
let mut db = Database::open_in_memory().unwrap();
let fields = vec![
field("id", Type::Int64, vec![]),
field("status", Type::String, vec![]),
];
let indexes = vec![IndexDef {
name: "status_idx".to_string(),
path: FieldPath(vec![Cow::Borrowed("status")]),
kind: IndexKind::NonUnique,
}];
let (cid, _) = db
.register_collection_with_indexes("t", fields, indexes, "id")
.unwrap();
for (id, st) in [(1, "open"), (2, "open"), (3, "closed")] {
let mut row = BTreeMap::new();
row.insert("id".into(), RowValue::Int64(id));
row.insert("status".into(), RowValue::String(st.into()));
db.insert(cid, row).unwrap();
}
let q = Query {
collection: cid,
predicate: Some(Predicate::And(vec![
Predicate::Eq {
path: FieldPath(vec![Cow::Borrowed("status")]),
value: ScalarValue::String("open".into()),
},
Predicate::Eq {
path: FieldPath(vec![Cow::Borrowed("status")]),
value: ScalarValue::String("nope".into()),
},
])),
limit: Some(1),
order_by: None,
};
let rows: Vec<_> = db
.query_iter(&q)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(rows.is_empty());
let q2 = Query {
collection: cid,
predicate: Some(Predicate::Eq {
path: FieldPath(vec![Cow::Borrowed("id")]),
value: ScalarValue::Int64(3),
}),
limit: Some(10),
order_by: None,
};
let rows2 = db
.query_iter(&q2)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(rows2.len(), 1);
assert_eq!(
rows2[0].get("status"),
Some(&RowValue::String("closed".into()))
);
}
#[test]
fn validate_top_level_row_covers_unknown_field_and_absent_optional() {
let fields = vec![
field("id", Type::Int64, vec![]),
field("note", Type::Optional(Box::new(Type::String)), vec![]),
];
let mut row = BTreeMap::new();
row.insert("id".into(), RowValue::Int64(1));
validate_top_level_row(&fields, "id", &row).unwrap();
row.insert("extra".into(), RowValue::Bool(true));
assert!(validate_top_level_row(&fields, "id", &row).is_err());
}