#![cfg(test)]
use std::collections::BTreeMap;
use revision::{DeserializeRevisioned, SerializeRevisioned, WalkRevisioned};
use surrealdb_strand::Strand;
use super::{
Extracted, PathSegment, SlotScanResult, TEST_DEPTH_LIMIT, descend_record_value_path,
descend_value_slice_path, extract_field_from_record_bytes, record_data_bytes,
rev2_record_data_bytes, scan_record_object_at_path_with_slots,
};
use crate::catalog::{Record, RecordType};
use crate::val::{Number, Object, Value};
fn p(segments: &[&str]) -> Vec<PathSegment> {
segments.iter().copied().map(PathSegment::from).collect()
}
fn wire_record_plain_object(obj: Object) -> Vec<u8> {
let rec = Record {
metadata: None,
data: Value::Object(obj),
};
let mut out = Vec::new();
rec.serialize_revisioned(&mut out).unwrap();
out
}
fn scan_slots_decoded(
record_bytes: &[u8],
path: &[&PathSegment],
needles: &[&[u8]],
) -> SlotScanResult<Vec<Value>> {
scan_record_object_at_path_with_slots(record_bytes, path, needles, TEST_DEPTH_LIMIT, |slots| {
slots
.iter()
.map(|s| match s {
None => Value::None,
Some(bytes) => {
let mut r: &[u8] = bytes;
<Value as DeserializeRevisioned>::deserialize_revisioned(&mut r)
.unwrap_or(Value::None)
}
})
.collect()
})
}
#[test]
fn extract_existing_top_level_field() {
let obj = Object::from(BTreeMap::from([
(Strand::from("a"), Value::Number(Number::Int(1))),
(Strand::from("b"), Value::Bool(true)),
]));
let rec = wire_record_plain_object(obj);
let result = extract_field_from_record_bytes(&rec, &p(&["b"]), TEST_DEPTH_LIMIT);
match result {
Extracted::Found(Value::Bool(true)) => {}
other => panic!("expected Found(Bool(true)), got {:?}", other),
}
}
#[test]
fn extract_nested_field_descends_correctly() {
let inner = Object::from(BTreeMap::from([(Strand::from("x"), Value::Number(Number::Int(42)))]));
let outer = Object::from(BTreeMap::from([(Strand::from("o"), Value::Object(inner))]));
let rec = wire_record_plain_object(outer);
let result = extract_field_from_record_bytes(&rec, &p(&["o", "x"]), TEST_DEPTH_LIMIT);
assert!(matches!(result, Extracted::Found(Value::Number(Number::Int(42)))));
}
#[test]
fn missing_key_yields_missing() {
let obj = Object::from(BTreeMap::from([(Strand::from("only"), Value::Number(Number::Int(1)))]));
let rec = wire_record_plain_object(obj);
let result = extract_field_from_record_bytes(&rec, &p(&["missing"]), TEST_DEPTH_LIMIT);
assert!(matches!(result, Extracted::Missing));
}
#[test]
fn edge_metadata_extracts_data() {
use crate::catalog::{Metadata, RecordType};
let obj = Object::from(BTreeMap::from([
(Strand::from("kind"), Value::String(Strand::from("knows"))),
(Strand::from("score"), Value::Number(Number::Int(7))),
]));
let rec = Record {
metadata: Some(Metadata {
record_type: RecordType::Edge {
variant: Default::default(),
},
aggregation_stats: Vec::new(),
}),
data: Value::Object(obj),
};
let mut bytes = Vec::new();
rec.serialize_revisioned(&mut bytes).unwrap();
let result = extract_field_from_record_bytes(&bytes, &p(&["score"]), TEST_DEPTH_LIMIT);
match result {
Extracted::Found(Value::Number(Number::Int(7))) => {}
other => panic!("expected Found(Int(7)) for edge data, got {:?}", other),
}
}
#[test]
fn aggregation_view_metadata_extracts_data() {
use crate::catalog::aggregation::AggregationStat;
use crate::catalog::{Metadata, RecordType};
let obj =
Object::from(BTreeMap::from([(Strand::from("total"), Value::Number(Number::Int(42)))]));
let rec = Record {
metadata: Some(Metadata {
record_type: RecordType::Table,
aggregation_stats: vec![AggregationStat::Count {
count: 17,
}],
}),
data: Value::Object(obj),
};
let mut bytes = Vec::new();
rec.serialize_revisioned(&mut bytes).unwrap();
let result = extract_field_from_record_bytes(&bytes, &p(&["total"]), TEST_DEPTH_LIMIT);
match result {
Extracted::Found(Value::Number(Number::Int(42))) => {}
other => panic!("expected Found(Int(42)) for aggregation row data, got {:?}", other),
}
}
#[test]
fn empty_path_bails() {
let rec = wire_record_plain_object(Object::default());
let result = extract_field_from_record_bytes(&rec, &[], TEST_DEPTH_LIMIT);
assert!(matches!(result, Extracted::Bail));
}
#[test]
fn descend_value_slice_path_decodes_field() {
let inner =
Object::from(BTreeMap::from([(Strand::from("k"), Value::String(Strand::from("v")))]));
let value = Value::Object(inner);
let mut wire = Vec::new();
value.serialize_revisioned(&mut wire).unwrap();
let result = descend_value_slice_path(&wire, &p(&["k"]));
match result {
Extracted::Found(Value::String(s)) => assert_eq!(s.as_str(), "v"),
other => panic!("unexpected: {:?}", other),
}
}
#[test]
fn descend_record_value_path_round_trips_all_supported_types() {
let obj = Object::from(BTreeMap::from([
(Strand::from("alpha"), Value::Number(Number::Int(1))),
(Strand::from("middle"), Value::Number(Number::Int(2))),
(Strand::from("zeta"), Value::Number(Number::Int(3))),
]));
let rec = wire_record_plain_object(obj);
for (key, expected) in [("alpha", 1), ("middle", 2), ("zeta", 3)] {
let result = descend_record_value_path(&rec, &p(&[key]));
match result {
Extracted::Found(Value::Number(Number::Int(v))) => assert_eq!(v, expected),
other => panic!("unexpected for {key}: {other:?}"),
}
}
}
#[test]
fn scan_sparse_needles_against_wide_indexed_object_returns_correct_slots() {
let mut entries = BTreeMap::new();
for i in 0..16u64 {
entries.insert(Strand::from(format!("k{i:02}")), Value::Number(Number::Int(i as i64 * 10)));
}
let obj = Object::from(entries);
let rec = wire_record_plain_object(obj);
let needles: &[&[u8]] = &[b"k03", b"k12"];
let result = scan_slots_decoded(&rec, &[], needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 2);
assert!(matches!(values[0], Value::Number(Number::Int(30))));
assert!(matches!(values[1], Value::Number(Number::Int(120))));
}
other => panic!("expected Found, got {other:?}"),
}
let needles: &[&[u8]] = &[b"k03", b"k12", b"zzz"];
let result = scan_slots_decoded(&rec, &[], needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 3);
assert!(matches!(values[0], Value::Number(Number::Int(30))));
assert!(matches!(values[1], Value::Number(Number::Int(120))));
assert!(matches!(values[2], Value::None));
}
other => panic!("expected Found with sparse-needle miss, got {other:?}"),
}
}
#[test]
fn scan_dense_needles_against_wide_indexed_object_returns_correct_slots() {
let mut entries = BTreeMap::new();
for i in 0..16u64 {
entries.insert(Strand::from(format!("k{i:02}")), Value::Number(Number::Int(i as i64 * 10)));
}
let obj = Object::from(entries);
let rec = wire_record_plain_object(obj);
let needle_strings: Vec<String> = (0..12).map(|i| format!("k{i:02}")).collect();
let needles: Vec<&[u8]> = needle_strings.iter().map(|s| s.as_bytes()).collect();
let result = scan_slots_decoded(&rec, &[], &needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 12);
for (i, v) in values.iter().enumerate() {
assert!(
matches!(v, Value::Number(Number::Int(n)) if *n == (i as i64) * 10),
"slot {i}: {v:?}",
);
}
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn scan_record_root_returns_values_for_present_keys_and_none_for_absent() {
let obj = Object::from(BTreeMap::from([
(Strand::from("a"), Value::Bool(false)),
(Strand::from("m"), Value::Number(Number::Int(7))),
(Strand::from("z"), Value::Bool(true)),
]));
let rec = wire_record_plain_object(obj);
let needles: &[&[u8]] = &[b"a", b"missing", b"z"];
let result = scan_slots_decoded(&rec, &[], needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 3);
assert!(matches!(values[0], Value::Bool(false)));
assert!(matches!(values[1], Value::None));
assert!(matches!(values[2], Value::Bool(true)));
}
other => panic!("expected Found at root, got {other:?}"),
}
}
#[test]
fn scan_record_object_at_path_walks_nested_object() {
let inner = Object::from(BTreeMap::from([
(Strand::from("a"), Value::Number(Number::Int(1))),
(Strand::from("b"), Value::Number(Number::Int(2))),
(Strand::from("c"), Value::Number(Number::Int(3))),
]));
let outer = Object::from(BTreeMap::from([
(Strand::from("outer"), Value::Object(inner)),
(Strand::from("sibling"), Value::Bool(false)),
]));
let rec = wire_record_plain_object(outer);
let needles: &[&[u8]] = &[b"a", b"b", b"missing"];
let path = p(&["outer"]);
let path_refs: Vec<&PathSegment> = path.iter().collect();
let result = scan_slots_decoded(&rec, &path_refs, needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 3);
assert!(matches!(values[0], Value::Number(Number::Int(1))));
assert!(matches!(values[1], Value::Number(Number::Int(2))));
assert!(matches!(values[2], Value::None));
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn scan_record_object_at_path_missing_intermediate_yields_missing() {
let outer = Object::from(BTreeMap::from([(
Strand::from("outer"),
Value::Object(Object::from(BTreeMap::from([(Strand::from("present"), Value::Bool(true))]))),
)]));
let rec = wire_record_plain_object(outer);
let needles: &[&[u8]] = &[b"a"];
let path = p(&["outer", "inner"]);
let path_refs: Vec<&PathSegment> = path.iter().collect();
let result = scan_slots_decoded(&rec, &path_refs, needles);
assert!(matches!(result, SlotScanResult::Missing));
}
#[test]
fn scan_record_object_at_path_non_object_intermediate_bails() {
let outer =
Object::from(BTreeMap::from([(Strand::from("outer"), Value::Number(Number::Int(7)))]));
let rec = wire_record_plain_object(outer);
let needles: &[&[u8]] = &[b"a"];
let path = p(&["outer", "anything"]);
let path_refs: Vec<&PathSegment> = path.iter().collect();
let result = scan_slots_decoded(&rec, &path_refs, needles);
assert!(matches!(result, SlotScanResult::Bail));
}
#[test]
fn extract_handles_non_ascii_keys() {
let obj = Object::from(BTreeMap::from([
(Strand::from("café"), Value::Number(Number::Int(1))),
(Strand::from("naïve"), Value::Number(Number::Int(2))),
(Strand::from("日本"), Value::Number(Number::Int(3))),
]));
let rec = wire_record_plain_object(obj);
for (key, expected) in [("café", 1i64), ("naïve", 2), ("日本", 3)] {
match extract_field_from_record_bytes(&rec, &p(&[key]), TEST_DEPTH_LIMIT) {
Extracted::Found(Value::Number(Number::Int(n))) => {
assert_eq!(n, expected, "wrong value for key {key:?}")
}
other => panic!("expected Found(Int({expected})) for {key:?}, got {other:?}"),
}
}
let needles: &[&[u8]] = &["café".as_bytes(), "naïve".as_bytes(), "日本".as_bytes()];
let result = scan_slots_decoded(&rec, &[], needles);
match result {
SlotScanResult::Found(scanned) => {
assert_eq!(scanned.len(), 3);
for (got, expected) in scanned.iter().zip([1i64, 2, 3]) {
match got {
Value::Number(Number::Int(n)) => assert_eq!(*n, expected),
other => panic!("expected Int({expected}), got {other:?}"),
}
}
}
other => panic!("expected Found, got {other:?}"),
}
}
fn wire_record_nested_x(depth: usize) -> Vec<u8> {
let mut value = Value::Bool(true);
for _ in 0..depth {
let mut entries = BTreeMap::new();
entries.insert(Strand::from("x"), value);
value = Value::Object(Object::from(entries));
}
let Value::Object(obj) = value else {
unreachable!("at least one level wrapped")
};
wire_record_plain_object(obj)
}
#[test]
fn descent_bails_past_depth_limit() {
let depth = 8usize;
let rec = wire_record_nested_x(depth);
let path: Vec<PathSegment> = std::iter::repeat_n(PathSegment::from("x"), depth).collect();
let limit: u32 = (depth as u32) - 1;
let extracted = super::extract_field_from_record_bytes(&rec, &path, limit);
assert!(
matches!(extracted, Extracted::Bail),
"expected Bail when path length {} > depth_limit {}, got {:?}",
path.len(),
limit,
extracted
);
}
#[test]
fn descent_succeeds_at_exact_depth_limit() {
let depth = 8usize;
let rec = wire_record_nested_x(depth);
let path: Vec<PathSegment> = std::iter::repeat_n(PathSegment::from("x"), depth).collect();
let limit: u32 = depth as u32;
let extracted = super::extract_field_from_record_bytes(&rec, &path, limit);
match extracted {
Extracted::Found(Value::Bool(true)) => {}
other => {
panic!("expected Found(true) at depth == depth_limit == {}, got {:?}", depth, other)
}
}
}
#[test]
fn descent_bails_on_zero_depth_limit() {
let rec = wire_record_nested_x(1);
let path = p(&["x"]);
let extracted = super::extract_field_from_record_bytes(&rec, &path, 0);
assert!(matches!(extracted, Extracted::Bail));
}
#[test]
fn scan_record_object_at_path_with_empty_path_resolves_against_root() {
let obj = Object::from(BTreeMap::from([
(Strand::from("a"), Value::Bool(false)),
(Strand::from("z"), Value::Bool(true)),
]));
let rec = wire_record_plain_object(obj);
let needles: &[&[u8]] = &[b"a", b"missing", b"z"];
let result = scan_slots_decoded(&rec, &[], needles);
match result {
SlotScanResult::Found(values) => {
assert_eq!(values.len(), 3);
assert!(matches!(values[0], Value::Bool(false)));
assert!(matches!(values[1], Value::None));
assert!(matches!(values[2], Value::Bool(true)));
}
other => panic!("expected Found, got {other:?}"),
}
}
fn equivalence_sample_records() -> Vec<Vec<u8>> {
let mut small = Object::default();
small.0.insert(Strand::from("a"), Value::Bool(true));
small.0.insert(Strand::from("b"), Value::from(7i64));
let mut wide = Object::default();
for i in 0..12u8 {
wide.0.insert(Strand::from(format!("k{i:02}")), Value::from(i as i64));
}
let mut nested_inner = Object::default();
nested_inner.0.insert(Strand::from("city"), Value::String(Strand::from("London")));
let mut nested = Object::default();
nested.0.insert(Strand::from("addr"), Value::Object(nested_inner));
let encode = |data: Value| {
let rec = Record {
metadata: None,
data,
};
let mut out = Vec::new();
rec.serialize_revisioned(&mut out).unwrap();
out
};
let with_meta = {
let mut rec = Record::new(Value::Object(small.clone()));
rec.set_record_type(RecordType::Table);
assert!(rec.metadata.is_some(), "expected metadata to be set");
let mut out = Vec::new();
rec.serialize_revisioned(&mut out).unwrap();
out
};
vec![
encode(Value::Object(small)),
encode(Value::Object(wide)),
encode(Value::Object(nested)),
encode(Value::Object(Object::default())),
encode(Value::Bool(true)),
encode(Value::from(42i64)),
encode(Value::String(Strand::from("hello"))),
encode(Value::None),
with_meta,
]
}
#[test]
fn rev2_record_data_bytes_matches_walker_path() {
for bytes in equivalence_sample_records() {
let fast = rev2_record_data_bytes(&bytes)
.expect("serialiser emits rev-2, so the fast path must recognise it");
let mut reader: &[u8] = &bytes;
let slow = Record::walk_revisioned(&mut reader)
.and_then(|w| w.into_data_bytes())
.expect("walker chain opens a valid record");
assert_eq!(fast, &*slow, "fast-path data bytes diverge from the walker chain");
let via_helper = record_data_bytes(&bytes).expect("helper opens a valid record");
assert_eq!(&*via_helper, fast);
}
}
#[test]
fn record_data_bytes_falls_back_on_non_rev2_prefix() {
let mut bytes = equivalence_sample_records().remove(0);
bytes[0] = 1; assert!(rev2_record_data_bytes(&bytes).is_none(), "fast path must decline non-rev-2");
let _ = record_data_bytes(&bytes); }