use nodedb_array::schema::ArraySchemaBuilder;
use nodedb_array::schema::attr_spec::{AttrSpec, AttrType};
use nodedb_array::schema::dim_spec::{DimSpec, DimType};
use nodedb_array::types::cell_value::value::CellValue;
use nodedb_array::types::coord::value::CoordValue;
use nodedb_array::types::domain::{Domain, DomainBound};
use nodedb_lite::PagedbStorageMem;
use nodedb_lite::engine::array::ArrayEngineState;
use nodedb_lite::engine::array::engine::CellWrite;
use nodedb_types::OPEN_UPPER;
use std::sync::Arc;
fn schema() -> nodedb_array::schema::ArraySchema {
ArraySchemaBuilder::new("g")
.dim(DimSpec::new(
"x",
DimType::Int64,
Domain::new(DomainBound::Int64(0), DomainBound::Int64(63)),
))
.attr(AttrSpec::new("v", AttrType::Int64, true))
.tile_extents(vec![8])
.build()
.unwrap()
}
async fn open_engine(storage: &Arc<PagedbStorageMem>) -> ArrayEngineState {
ArrayEngineState::open(storage).await.unwrap()
}
#[tokio::test]
async fn create_put_slice_roundtrip() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let mut engine = open_engine(&storage).await;
engine
.create_array(&storage, "grid", schema(), None, None)
.await
.unwrap();
engine
.put_cell(
&storage,
"grid",
CellWrite {
coord: vec![CoordValue::Int64(1)],
attrs: vec![CellValue::Int64(100)],
system_from_ms: 1_000,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine
.put_cell(
&storage,
"grid",
CellWrite {
coord: vec![CoordValue::Int64(5)],
attrs: vec![CellValue::Int64(200)],
system_from_ms: 1_000,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine.flush(&storage, "grid").await.unwrap();
let cells = engine
.slice(
&storage,
"grid",
vec![None], i64::MAX,
)
.await
.unwrap();
assert_eq!(cells.len(), 2, "expected 2 live cells from slice");
let vals: std::collections::HashSet<i64> = cells
.iter()
.map(|c| match c.attrs[0] {
CellValue::Int64(v) => v,
_ => panic!("unexpected attr type"),
})
.collect();
assert!(vals.contains(&100));
assert!(vals.contains(&200));
}
#[tokio::test]
async fn bitemporal_as_of_system_time() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let mut engine = open_engine(&storage).await;
engine
.create_array(&storage, "bt", schema(), None, None)
.await
.unwrap();
engine
.put_cell(
&storage,
"bt",
CellWrite {
coord: vec![CoordValue::Int64(1)],
attrs: vec![CellValue::Int64(10)],
system_from_ms: 100,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine
.put_cell(
&storage,
"bt",
CellWrite {
coord: vec![CoordValue::Int64(1)],
attrs: vec![CellValue::Int64(20)],
system_from_ms: 200,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine.flush(&storage, "bt").await.unwrap();
let result_150 = engine
.read_coord(&storage, "bt", &[CoordValue::Int64(1)], 150)
.await
.unwrap();
assert!(result_150.is_some(), "expected a result AS-OF 150");
let val_150 = match result_150.unwrap().attrs[0] {
CellValue::Int64(v) => v,
_ => panic!("unexpected attr type"),
};
assert_eq!(val_150, 10, "AS-OF 150 should see v1 (value=10)");
let result_300 = engine
.read_coord(&storage, "bt", &[CoordValue::Int64(1)], 300)
.await
.unwrap();
assert!(result_300.is_some(), "expected a result AS-OF 300");
let val_300 = match result_300.unwrap().attrs[0] {
CellValue::Int64(v) => v,
_ => panic!("unexpected attr type"),
};
assert_eq!(val_300, 20, "AS-OF 300 should see v2 (value=20)");
}
#[tokio::test]
async fn restart_durability() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
{
let mut engine = open_engine(&storage).await;
engine
.create_array(&storage, "durable", schema(), None, None)
.await
.unwrap();
engine
.put_cell(
&storage,
"durable",
CellWrite {
coord: vec![CoordValue::Int64(3)],
attrs: vec![CellValue::Int64(42)],
system_from_ms: 500,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine.flush(&storage, "durable").await.unwrap();
}
let mut engine2 = open_engine(&storage).await;
let result = engine2
.read_coord(&storage, "durable", &[CoordValue::Int64(3)], i64::MAX)
.await
.unwrap();
assert!(result.is_some(), "data must survive engine restart");
assert_eq!(
result.unwrap().attrs[0],
CellValue::Int64(42),
"value must match after restart"
);
let cells = engine2
.slice(&storage, "durable", vec![None], i64::MAX)
.await
.unwrap();
assert_eq!(cells.len(), 1);
}
#[tokio::test]
async fn tombstone_coord_not_found() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let mut engine = open_engine(&storage).await;
engine
.create_array(&storage, "del", schema(), None, None)
.await
.unwrap();
engine
.put_cell(
&storage,
"del",
CellWrite {
coord: vec![CoordValue::Int64(7)],
attrs: vec![CellValue::Int64(99)],
system_from_ms: 10,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine
.delete_cell("del", vec![CoordValue::Int64(7)], 20)
.unwrap();
engine.flush(&storage, "del").await.unwrap();
let result = engine
.read_coord(&storage, "del", &[CoordValue::Int64(7)], i64::MAX)
.await
.unwrap();
assert!(
result.is_none(),
"tombstoned coord must return None (got {result:?})"
);
let before = engine
.read_coord(&storage, "del", &[CoordValue::Int64(7)], 15)
.await
.unwrap();
assert!(
before.is_some(),
"coord must be visible AS-OF before the tombstone"
);
}
#[tokio::test]
async fn gdpr_erasure() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let mut engine = open_engine(&storage).await;
engine
.create_array(&storage, "gdpr", schema(), None, None)
.await
.unwrap();
engine
.put_cell(
&storage,
"gdpr",
CellWrite {
coord: vec![CoordValue::Int64(2)],
attrs: vec![CellValue::Int64(55)],
system_from_ms: 100,
valid_from_ms: 0,
valid_until_ms: OPEN_UPPER,
},
)
.await
.unwrap();
engine.flush(&storage, "gdpr").await.unwrap();
engine
.gdpr_erase_cell(&storage, "gdpr", vec![CoordValue::Int64(2)], 200)
.await
.unwrap();
let result = engine
.read_coord(&storage, "gdpr", &[CoordValue::Int64(2)], i64::MAX)
.await
.unwrap();
assert!(
result.is_none(),
"GDPR-erased coord must return None (got {result:?})"
);
drop(engine);
let mut engine2 = open_engine(&storage).await;
let result2 = engine2
.read_coord(&storage, "gdpr", &[CoordValue::Int64(2)], i64::MAX)
.await
.unwrap();
assert!(
result2.is_none(),
"GDPR erasure must persist across restart"
);
let cells = engine2
.slice(&storage, "gdpr", vec![None], i64::MAX)
.await
.unwrap();
assert!(
cells.is_empty(),
"slice must return no live cells after GDPR erasure"
);
}