use modelvault_core::error::{DbError, FormatError};
use modelvault_core::record::{
decode_record_payload, encode_record_payload_v2, encode_row_value, encode_tagged_scalar,
RowValue, ScalarValue, OP_INSERT, RECORD_PAYLOAD_VERSION_V2,
};
use modelvault_core::schema::{Constraint, FieldDef, FieldPath, Type};
use modelvault_core::validation::validate_value;
use std::borrow::Cow;
use std::collections::BTreeMap;
fn seg(s: &str) -> FieldPath {
FieldPath::new([Cow::Owned(s.to_string())]).unwrap()
}
#[test]
fn validation_constraints_cover_all_variants() {
let mut p = vec!["x".to_string()];
assert!(matches!(
validate_value(
&mut p,
&Type::Int64,
&[Constraint::MaxI64(1)],
&RowValue::Int64(2)
),
Err(DbError::Validation(_))
));
validate_value(
&mut p,
&Type::Int64,
&[Constraint::MinI64(-2)],
&RowValue::Int64(-2),
)
.unwrap();
assert!(matches!(
validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MinU64(2)],
&RowValue::Uint64(1)
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MaxU64(2)],
&RowValue::Uint64(3)
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::Float64,
&[Constraint::MinF64(2.0)],
&RowValue::Float64(1.0)
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::Float64,
&[Constraint::MaxF64(2.0)],
&RowValue::Float64(3.0)
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::MinLength(3)],
&RowValue::String("hi".into())
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::Bytes,
&[Constraint::MaxLength(1)],
&RowValue::Bytes(vec![1, 2])
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::List(Box::new(Type::Int64)),
&[Constraint::MinLength(1)],
&RowValue::List(vec![])
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::Regex("(".into())],
&RowValue::String("x".into())
),
Err(DbError::Validation(v))
if v.message.contains("invalid regex")
|| v.message.contains("unbalanced parentheses")
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::Regex("^a+$".into())],
&RowValue::String("b".into())
),
Err(DbError::Validation(v)) if v.message.contains("does not match")
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::Email],
&RowValue::String("nope".into())
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::Url],
&RowValue::String("ftp://x".into())
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::String,
&[Constraint::NonEmpty],
&RowValue::String("".into())
),
Err(DbError::Validation(_))
));
assert!(matches!(
validate_value(
&mut p,
&Type::Int64,
&[Constraint::NonEmpty],
&RowValue::Int64(1)
),
Err(DbError::Validation(_))
));
validate_value(
&mut p,
&Type::String,
&[Constraint::Url],
&RowValue::String("https://example.com/x".into()),
)
.unwrap();
validate_value(
&mut p,
&Type::String,
&[Constraint::Email],
&RowValue::String("a@b.co".into()),
)
.unwrap();
assert!(matches!(
validate_value(
&mut p,
&Type::Bytes,
&[Constraint::NonEmpty],
&RowValue::Bytes(vec![])
),
Err(DbError::Validation(_))
));
validate_value(
&mut p,
&Type::Bytes,
&[Constraint::NonEmpty],
&RowValue::Bytes(vec![0]),
)
.unwrap();
assert!(matches!(
validate_value(
&mut p,
&Type::Bytes,
&[Constraint::MinLength(2)],
&RowValue::Bytes(vec![1])
),
Err(DbError::Validation(_))
));
validate_value(
&mut p,
&Type::Bytes,
&[Constraint::MaxLength(2)],
&RowValue::Bytes(vec![1, 2]),
)
.unwrap();
}
#[test]
fn validation_primitive_mismatch_uint_float_bytes_and_object_missing_required() {
let mut p = vec!["x".to_string()];
assert!(validate_value(&mut p, &Type::Uint64, &[], &RowValue::Int64(1)).is_err());
assert!(validate_value(&mut p, &Type::Float64, &[], &RowValue::Int64(1)).is_err());
assert!(validate_value(&mut p, &Type::Bytes, &[], &RowValue::String("x".into())).is_err());
let obj_fields = vec![FieldDef {
path: seg("req"),
ty: Type::String,
constraints: vec![],
}];
let row_missing = BTreeMap::new();
assert!(validate_value(
&mut p,
&Type::Object(obj_fields.clone()),
&[],
&RowValue::Object(row_missing),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Int64,
&[Constraint::MinI64(0)],
&RowValue::String("n".into()),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Int64,
&[Constraint::MaxI64(0)],
&RowValue::String("n".into()),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MinU64(0)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MaxU64(0)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Float64,
&[Constraint::MinF64(0.0)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Float64,
&[Constraint::MaxF64(0.0)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::String,
&[Constraint::MinLength(3)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Int64,
&[Constraint::MaxLength(3)],
&RowValue::Int64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::List(Box::new(Type::Int64)),
&[Constraint::MaxLength(1)],
&RowValue::List(vec![
RowValue::Int64(1),
RowValue::Int64(2),
RowValue::Int64(3),
]),
)
.is_err());
}
#[test]
fn validation_numeric_bound_violations_and_maxlength_wrong_type() {
let mut p = vec!["x".to_string()];
assert!(validate_value(
&mut p,
&Type::Int64,
&[Constraint::MaxI64(10)],
&RowValue::Int64(11),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MinU64(10)],
&RowValue::Uint64(1),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Uint64,
&[Constraint::MaxU64(1)],
&RowValue::Uint64(2),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Float64,
&[Constraint::MinF64(10.0)],
&RowValue::Float64(1.0),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Float64,
&[Constraint::MaxF64(1.0)],
&RowValue::Float64(2.0),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::String,
&[Constraint::MinLength(5)],
&RowValue::String("abcd".into()),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Bytes,
&[Constraint::MinLength(3)],
&RowValue::Bytes(vec![1, 2]),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::List(Box::new(Type::Int64)),
&[Constraint::MinLength(3)],
&RowValue::List(vec![RowValue::Int64(1)]),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::String,
&[Constraint::MaxLength(1)],
&RowValue::String("ab".into()),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::List(Box::new(Type::Int64)),
&[Constraint::MaxLength(1)],
&RowValue::List(vec![RowValue::Int64(1), RowValue::Int64(2)]),
)
.is_err());
assert!(validate_value(
&mut p,
&Type::Int64,
&[Constraint::MaxLength(3)],
&RowValue::Int64(1),
)
.is_err());
}
#[test]
fn record_payload_v2_optional_presence_tag_mismatch_errors() {
let fields = vec![
FieldDef {
path: seg("id"),
ty: Type::String,
constraints: vec![],
},
FieldDef {
path: seg("opt"),
ty: Type::Optional(Box::new(Type::String)),
constraints: vec![],
},
];
let pk_ty = &fields[0].ty;
let pk = ScalarValue::String("k".into());
let non_pk = vec![(fields[1].clone(), RowValue::None)];
let mut payload = encode_record_payload_v2(1, 1, &pk, pk_ty, &non_pk).unwrap();
let idx = payload
.iter()
.rposition(|b| *b == 0)
.expect("presence byte");
payload[idx] = 2;
let e = decode_record_payload(&payload, "id", pk_ty, &fields).unwrap_err();
assert!(matches!(
e,
DbError::Format(FormatError::RecordPayloadTypeMismatch)
));
}
#[test]
fn record_payload_v2_decode_errors_field_count_and_trailing_bytes() {
let fields = vec![
FieldDef {
path: seg("id"),
ty: Type::String,
constraints: vec![],
},
FieldDef {
path: seg("n"),
ty: Type::Int64,
constraints: vec![],
},
];
let pk_ty = &fields[0].ty;
let pk = ScalarValue::String("k".into());
let non_pk = vec![(
fields[1].clone(),
RowValue::Int64(1), )];
let payload = encode_record_payload_v2(1, 1, &pk, pk_ty, &non_pk).unwrap();
let mut with_trailing = payload.clone();
with_trailing.push(0);
let e = decode_record_payload(&with_trailing, "id", pk_ty, &fields).unwrap_err();
assert!(matches!(
e,
DbError::Format(FormatError::TrailingRecordPayload)
));
let mut bad_n = Vec::new();
bad_n.extend_from_slice(&RECORD_PAYLOAD_VERSION_V2.to_le_bytes());
bad_n.extend_from_slice(&1u32.to_le_bytes()); bad_n.extend_from_slice(&1u32.to_le_bytes()); bad_n.push(OP_INSERT);
encode_tagged_scalar(&mut bad_n, &pk, pk_ty).unwrap();
bad_n.extend_from_slice(&0u32.to_le_bytes()); encode_row_value(&mut bad_n, &RowValue::Int64(1), &Type::Int64).unwrap();
let e = decode_record_payload(&bad_n, "id", pk_ty, &fields).unwrap_err();
assert!(matches!(
e,
DbError::Format(FormatError::RecordPayloadTypeMismatch)
));
}