use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fs;
use modelvault_core::error::{DbError, SchemaError};
use modelvault_core::file_format::{decode_header, FILE_HEADER_SIZE};
use modelvault_core::record::{RowValue, ScalarValue};
use modelvault_core::schema::{FieldDef, FieldPath, Type};
use modelvault_core::CollectionId;
use modelvault_core::Database;
fn title() -> FieldDef {
FieldDef {
path: FieldPath(vec![Cow::Owned("title".to_string())]),
ty: Type::String,
constraints: vec![],
}
}
fn year() -> FieldDef {
FieldDef {
path: FieldPath(vec![Cow::Owned("year".to_string())]),
ty: Type::Int64,
constraints: vec![],
}
}
#[test]
fn collection_id_named_unknown_errors() {
let dir = tempfile::tempdir().unwrap();
let db = Database::open(dir.path().join("x.modelvault")).unwrap();
let e = db.collection_id_named("nope").unwrap_err();
assert!(matches!(
e,
DbError::Schema(SchemaError::UnknownCollectionName { name }) if name == "nope"
));
}
#[test]
fn insert_row_unknown_field_errors() {
let dir = tempfile::tempdir().unwrap();
let mut db = Database::open(dir.path().join("u.modelvault")).unwrap();
let (id, _) = db
.register_collection("b", vec![title(), year()], "title")
.unwrap();
let mut row = BTreeMap::new();
row.insert("title".into(), RowValue::String("t".into()));
row.insert("year".into(), RowValue::Int64(1));
row.insert("extra".into(), RowValue::Int64(0));
let e = db.insert(id, row).unwrap_err();
assert!(matches!(e, DbError::Validation(_)));
}
#[test]
fn insert_missing_non_pk_field_errors() {
let dir = tempfile::tempdir().unwrap();
let mut db = Database::open(dir.path().join("m.modelvault")).unwrap();
let (id, _) = db
.register_collection("b", vec![title(), year()], "title")
.unwrap();
let mut row = BTreeMap::new();
row.insert("title".into(), RowValue::String("t".into()));
let e = db.insert(id, row).unwrap_err();
assert!(matches!(e, DbError::Validation(_)));
}
#[test]
fn insert_pk_type_mismatch_errors() {
let dir = tempfile::tempdir().unwrap();
let mut db = Database::open(dir.path().join("p.modelvault")).unwrap();
let (id, _) = db.register_collection("b", vec![title()], "title").unwrap();
let mut row = BTreeMap::new();
row.insert("title".into(), RowValue::Int64(1));
let e = db.insert(id, row).unwrap_err();
assert!(matches!(e, DbError::Validation(_)));
}
#[test]
fn get_pk_type_mismatch_errors() {
let dir = tempfile::tempdir().unwrap();
let mut db = Database::open(dir.path().join("g.modelvault")).unwrap();
let (id, _) = db.register_collection("b", vec![title()], "title").unwrap();
let e = db.get(id, &ScalarValue::Int64(1)).unwrap_err();
assert!(matches!(
e,
DbError::Schema(SchemaError::PrimaryKeyTypeMismatch { .. })
));
}
#[test]
fn insert_nested_path_schema_requires_value_or_optional() {
let dir = tempfile::tempdir().unwrap();
let mut db = Database::open(dir.path().join("n.modelvault")).unwrap();
let nested = FieldDef {
path: FieldPath(vec![Cow::Owned("a".into()), Cow::Owned("b".into())]),
ty: Type::String,
constraints: vec![],
};
let (id, _) = db
.register_collection("x", vec![nested, title()], "title")
.unwrap();
let mut row = BTreeMap::new();
row.insert("title".into(), RowValue::String("t".into()));
let e = db.insert(id, row).unwrap_err();
assert!(matches!(
e,
DbError::Schema(SchemaError::RowMissingField { .. })
));
}
#[test]
fn lazy_header_v4_to_v5_on_first_record_write() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bump.modelvault");
let mut header = [0u8; FILE_HEADER_SIZE];
header[0..4].copy_from_slice(b"TDB0");
header[4..6].copy_from_slice(&0u16.to_le_bytes());
header[6..8].copy_from_slice(&2u16.to_le_bytes());
header[8..12].copy_from_slice(&(FILE_HEADER_SIZE as u32).to_le_bytes());
fs::write(&path, header).unwrap();
{
let mut db = Database::open(&path).unwrap();
db.register_collection("books", vec![title(), year()], "title")
.unwrap();
let bytes = fs::read(&path).unwrap();
let h = decode_header(&bytes[..FILE_HEADER_SIZE]).unwrap();
assert_eq!(
h.format_minor,
modelvault_core::file_format::FORMAT_MINOR_V6
);
let mut row = BTreeMap::new();
row.insert("title".into(), RowValue::String("Rust".into()));
row.insert("year".into(), RowValue::Int64(2024));
db.insert(CollectionId(1), row).unwrap();
}
let bytes = fs::read(&path).unwrap();
let h = decode_header(&bytes[..FILE_HEADER_SIZE]).unwrap();
assert_eq!(
h.format_minor,
modelvault_core::file_format::FORMAT_MINOR_V6
);
}
#[test]
fn new_database_starts_at_format_minor_6() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("new.modelvault");
let _db = Database::open(&path).unwrap();
let bytes = fs::read(&path).unwrap();
let h = decode_header(&bytes[..FILE_HEADER_SIZE]).unwrap();
assert_eq!(
h.format_minor,
modelvault_core::file_format::FORMAT_MINOR_V6
);
}