use std::fmt;
use std::str::FromStr;
use crate::filter::Filter;
use crate::name::ResourceName;
use crate::store::{
EdgeEndpoint, EdgeQuery, ObjectStoreReader, Precondition, StoreExec, Transactional,
};
use crate::{Error, Label};
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
pub enum ConformanceLabel {
#[default]
Node,
Other,
}
impl fmt::Display for ConformanceLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ConformanceLabel {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
match s {
"node" => Ok(ConformanceLabel::Node),
"other" => Ok(ConformanceLabel::Other),
other => Err(format!("unknown label: {other}")),
}
}
}
impl Label for ConformanceLabel {
fn as_str(&self) -> &str {
match self {
ConformanceLabel::Node => "node",
ConformanceLabel::Other => "other",
}
}
}
pub fn parent_child_inverse(label: &str) -> Option<String> {
match label {
"parent_of" => Some("child_of".to_string()),
"child_of" => Some("parent_of".to_string()),
_ => None,
}
}
fn rn(s: &str) -> ResourceName {
ResourceName::from_naive_str_split(s)
}
pub async fn cas_update<S: StoreExec<ConformanceLabel>>(store: &S) {
let obj = store
.create(ConformanceLabel::Node, &rn("a"), None, None, None)
.await
.unwrap();
assert_eq!(obj.version, 0, "fresh object starts at version 0");
let updated = store
.update(&obj.id, None, Precondition::Version(0), None)
.await
.unwrap();
assert_eq!(updated.version, 1, "successful CAS bumps the version");
let err = store
.update(&obj.id, None, Precondition::Version(0), None)
.await
.unwrap_err();
assert!(
matches!(err, Error::Conflict),
"stale version must yield Conflict, got {err:?}"
);
let again = store
.update(&obj.id, None, Precondition::Any, None)
.await
.unwrap();
assert_eq!(again.version, 2, "Any overwrites unconditionally");
}
pub async fn rename_semantics<S: StoreExec<ConformanceLabel>>(store: &S) {
let a = store
.create(ConformanceLabel::Node, &rn("a"), None, None, None)
.await
.unwrap();
let b = store
.create(ConformanceLabel::Node, &rn("b"), None, None, None)
.await
.unwrap();
store.add(a.id, b.id, "link", None).await.unwrap();
let err = store
.rename(&a.id, &rn("b"), Precondition::Any)
.await
.unwrap_err();
assert!(
matches!(err, Error::AlreadyExists),
"rename onto an existing name must collide, got {err:?}"
);
let renamed = store
.rename(&a.id, &rn("a2"), Precondition::Version(a.version))
.await
.unwrap();
assert_eq!(renamed.id, a.id, "rename preserves id");
assert_eq!(renamed.name, rn("a2"));
assert!(renamed.version > a.version, "rename bumps the version");
let (edges, _) = store
.query_edges(EdgeQuery::from(a.id, "link"))
.await
.unwrap();
assert_eq!(edges.len(), 1, "rename preserves outgoing associations");
assert_eq!(edges[0].to_id, b.id);
}
pub async fn case_insensitive_names<S: StoreExec<ConformanceLabel>>(store: &S) {
let created = store
.create(ConformanceLabel::Node, &rn("Catalog"), None, None, None)
.await
.unwrap();
let by_lower = store
.get_by_name(ConformanceLabel::Node, &rn("catalog"))
.await
.unwrap();
assert_eq!(by_lower.id, created.id, "get_by_name must fold ASCII case");
let by_upper = store
.get_by_name(ConformanceLabel::Node, &rn("CATALOG"))
.await
.unwrap();
assert_eq!(by_upper.id, created.id, "get_by_name must fold ASCII case");
let err = store
.create(ConformanceLabel::Node, &rn("catalog"), None, None, None)
.await
.unwrap_err();
assert!(
matches!(err, Error::AlreadyExists),
"creating a case-variant of an existing name must collide, got {err:?}"
);
let child = store
.create(
ConformanceLabel::Node,
&rn("Catalog.Schema.table"),
None,
None,
None,
)
.await
.unwrap();
let (listed, _) = store
.list(
ConformanceLabel::Node,
Some(&rn("catalog.schema")),
None,
None,
)
.await
.unwrap();
assert!(
listed.iter().any(|o| o.id == child.id),
"namespace prefix listing must fold ASCII case"
);
}
pub async fn transaction_atomicity<
S: StoreExec<ConformanceLabel> + Transactional<ConformanceLabel>,
>(
store: &S,
) {
let seed = store
.create(ConformanceLabel::Node, &rn("seed"), None, None, None)
.await
.unwrap();
let seed_id = seed.id;
let res: crate::Result<()> = store
.transaction(Box::new(move |tx| {
Box::pin(async move {
tx.delete(&seed_id).await?;
tx.create(ConformanceLabel::Node, &rn("new"), None, None, None)
.await?;
Err(Error::generic("boom"))
})
}))
.await;
assert!(res.is_err(), "the closure error must propagate");
assert!(
store.get(&seed_id).await.is_ok(),
"rollback must restore the deleted seed"
);
assert!(
store
.get_by_name(ConformanceLabel::Node, &rn("new"))
.await
.is_err(),
"rollback must discard the created object"
);
}
pub async fn transaction_commit<
S: StoreExec<ConformanceLabel> + Transactional<ConformanceLabel>,
>(
store: &S,
) {
let res: crate::Result<uuid::Uuid> = store
.transaction(Box::new(|tx| {
Box::pin(async move {
let a = tx
.create(ConformanceLabel::Node, &rn("x"), None, None, None)
.await?;
let b = tx
.create(ConformanceLabel::Node, &rn("y"), None, None, None)
.await?;
tx.add(a.id, b.id, "e", None).await?;
Ok(a.id)
})
}))
.await;
let a_id = res.unwrap();
assert!(
store.get(&a_id).await.is_ok(),
"committed object must persist"
);
let (edges, _) = store.query_edges(EdgeQuery::from(a_id, "e")).await.unwrap();
assert_eq!(edges.len(), 1, "committed edge must persist");
}
pub async fn inverse_edges<S: StoreExec<ConformanceLabel>>(store: &S) {
let p = store
.create(ConformanceLabel::Node, &rn("p"), None, None, None)
.await
.unwrap();
let c = store
.create(ConformanceLabel::Node, &rn("c"), None, None, None)
.await
.unwrap();
store.add(p.id, c.id, "parent_of", None).await.unwrap();
let (fwd, _) = store
.query_edges(EdgeQuery::from(p.id, "parent_of"))
.await
.unwrap();
assert_eq!(fwd.len(), 1, "forward edge present");
let (inv, _) = store
.query_edges(EdgeQuery::from(c.id, "child_of"))
.await
.unwrap();
assert_eq!(inv.len(), 1, "inverse edge maintained on add");
assert_eq!(inv[0].to_id, p.id);
store.remove(p.id, c.id, "parent_of").await.unwrap();
let (inv, _) = store
.query_edges(EdgeQuery::from(c.id, "child_of"))
.await
.unwrap();
assert!(inv.is_empty(), "inverse edge removed on remove");
}
pub async fn sensitive_blob_roundtrip<S: StoreExec<ConformanceLabel>>(store: &S) {
use bytes::Bytes;
let blob = Bytes::from_static(b"opaque-sealed-bytes");
let obj = store
.create(
ConformanceLabel::Node,
&rn("s"),
None,
None,
Some(blob.clone()),
)
.await
.unwrap();
let got = ObjectStoreReader::get(store, &obj.id).await.unwrap();
assert!(
got.properties.is_none(),
"sensitive blob must not surface in properties"
);
assert_eq!(
store.get_sensitive(&obj.id).await.unwrap().as_deref(),
Some(&blob[..]),
"stored blob must round-trip"
);
store
.update(&obj.id, None, Precondition::Any, None)
.await
.unwrap();
assert_eq!(
store.get_sensitive(&obj.id).await.unwrap().as_deref(),
Some(&blob[..]),
"update without a blob must preserve the existing one"
);
let blob2 = Bytes::from_static(b"replacement");
store
.update(&obj.id, None, Precondition::Any, Some(blob2.clone()))
.await
.unwrap();
assert_eq!(
store.get_sensitive(&obj.id).await.unwrap().as_deref(),
Some(&blob2[..]),
"supplying a blob must replace the existing one"
);
let before = ObjectStoreReader::get(store, &obj.id).await.unwrap();
let blob3 = Bytes::from_static(b"rewrapped");
store.set_sensitive(&obj.id, blob3.clone()).await.unwrap();
assert_eq!(
store.get_sensitive(&obj.id).await.unwrap().as_deref(),
Some(&blob3[..]),
"set_sensitive must replace the blob"
);
let after = ObjectStoreReader::get(store, &obj.id).await.unwrap();
assert_eq!(
after.version, before.version,
"set_sensitive must not bump the object version"
);
store.delete(&obj.id).await.unwrap();
assert!(
matches!(store.get_sensitive(&obj.id).await, Ok(None)),
"blob must be gone (Ok(None)) once the object is deleted"
);
}
pub async fn search_object_predicates<S: StoreExec<ConformanceLabel>>(store: &S) {
let payloads = [
serde_json::json!({ "owner": "alice", "size": 10, "tags": ["a", "b"] }),
serde_json::json!({ "owner": "bob", "size": 20, "tags": ["b", "c"] }),
serde_json::json!({ "owner": "carol", "size": 30, "archived": null }),
serde_json::json!({ "owner": "alice", "size": 40, "tags": [] }),
serde_json::json!({ "owner": "Bob", "size": 50, "tags": ["x"] }),
];
for (i, p) in payloads.iter().enumerate() {
store
.create(
ConformanceLabel::Node,
&rn(&format!("o{i}")),
Some(p.clone()),
None,
None,
)
.await
.unwrap();
}
let check = async |filter: Filter| {
let (hits, token) = store
.search(ConformanceLabel::Node, None, &filter, None, None)
.await
.unwrap();
assert!(token.is_none(), "unbounded search returns a single page");
let mut got: Vec<_> = hits.iter().map(|o| o.properties.clone().unwrap()).collect();
let mut want: Vec<_> = payloads
.iter()
.filter(|p| filter.matches(p))
.cloned()
.collect();
got.sort_by_key(|v| v.to_string());
want.sort_by_key(|v| v.to_string());
assert_eq!(
got, want,
"search disagreed with Filter::matches for {filter:?}"
);
};
check(Filter::eq("owner", "alice")).await;
check(Filter::ne("owner", "alice")).await;
check(Filter::gt("size", 20)).await;
check(Filter::ge("size", 20)).await;
check(Filter::lt("size", 20)).await;
check(Filter::le("size", 20)).await;
check(Filter::gt("owner", "M")).await;
check(Filter::lt("owner", "M")).await;
check(Filter::contains("tags", "b")).await;
check(Filter::exists("archived")).await;
check(Filter::exists("tags")).await;
check(Filter::all([
Filter::eq("owner", "alice"),
Filter::gt("size", 15),
]))
.await;
check(Filter::any([
Filter::eq("owner", "bob"),
Filter::eq("owner", "carol"),
]))
.await;
check(Filter::eq("owner", "alice").negate()).await;
check(Filter::eq("owner", "nobody")).await;
check(Filter::all([])).await;
}
pub async fn search_object_pagination_filters_completely<S: StoreExec<ConformanceLabel>>(
store: &S,
) {
for i in 0..6 {
for keep in [true, false] {
let p = serde_json::json!({ "keep": keep, "i": i });
let name = rn(&format!("k{keep}i{i}"));
store
.create(ConformanceLabel::Node, &name, Some(p), None, None)
.await
.unwrap();
}
}
let filter = Filter::eq("keep", true);
let mut seen = Vec::new();
let mut token = None;
loop {
let (page, next) = store
.search(ConformanceLabel::Node, None, &filter, Some(2), token)
.await
.unwrap();
assert!(page.len() <= 2, "respects max_results");
assert!(
page.iter()
.all(|o| o.properties.as_ref().unwrap()["keep"] == true),
"every returned object matches the filter"
);
seen.extend(page.into_iter().map(|o| o.id));
match next {
Some(t) => token = Some(t),
None => break,
}
}
assert_eq!(seen.len(), 6, "every matching object must be paged");
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 6, "no duplicates across pages");
}
pub async fn search_namespace_and_filter<S: StoreExec<ConformanceLabel>>(store: &S) {
for (name, active) in [
("ns.a", true),
("ns.b", true),
("ns.c", true),
("ns.d", false),
("ns.e", false),
("other.f", true),
] {
let p = serde_json::json!({ "active": active });
store
.create(ConformanceLabel::Node, &rn(name), Some(p), None, None)
.await
.unwrap();
}
let ns = rn("ns");
let filter = Filter::eq("active", true);
let mut seen = Vec::new();
let mut token = None;
loop {
let (page, next) = store
.search(ConformanceLabel::Node, Some(&ns), &filter, Some(2), token)
.await
.unwrap();
for o in &page {
assert!(o.name.prefix_matches(&ns), "namespace prefix honored");
assert_eq!(o.properties.as_ref().unwrap()["active"], true);
}
seen.extend(page.into_iter().map(|o| o.id));
match next {
Some(t) => token = Some(t),
None => break,
}
}
assert_eq!(
seen.len(),
3,
"only ns.* objects matching the filter, all of them"
);
}
pub async fn edge_filter_predicates<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let edges = [
serde_json::json!({ "weight": 1, "kind": "x" }),
serde_json::json!({ "weight": 5, "kind": "y" }),
serde_json::json!({ "weight": 9, "kind": "x" }),
];
for (i, e) in edges.iter().enumerate() {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("dst{i}")),
None,
None,
None,
)
.await
.unwrap();
store
.add(src.id, dst.id, "link", Some(e.clone()))
.await
.unwrap();
}
let check = async |filter: Filter| {
let (hits, token) = store
.query_edges(EdgeQuery::from(src.id, "link").filter(&filter))
.await
.unwrap();
assert!(token.is_none());
let mut got: Vec<_> = hits.iter().map(|a| a.properties.clone().unwrap()).collect();
let mut want: Vec<_> = edges
.iter()
.filter(|e| filter.matches(e))
.cloned()
.collect();
got.sort_by_key(|v| v.to_string());
want.sort_by_key(|v| v.to_string());
assert_eq!(
got, want,
"query_edges disagreed with Filter::matches for {filter:?}"
);
};
check(Filter::eq("kind", "x")).await;
check(Filter::gt("weight", 4)).await;
check(Filter::all([
Filter::eq("kind", "x"),
Filter::lt("weight", 5),
]))
.await;
check(Filter::exists("kind")).await;
}
pub async fn edge_filter_pagination_completes<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
for i in 0..6 {
for keep in [true, false] {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("d{keep}{i}")),
None,
None,
None,
)
.await
.unwrap();
let p = serde_json::json!({ "keep": keep });
store.add(src.id, dst.id, "link", Some(p)).await.unwrap();
}
}
let filter = Filter::eq("keep", true);
let mut seen = Vec::new();
let mut token = None;
loop {
let (page, next) = store
.query_edges(
EdgeQuery::from(src.id, "link")
.filter(&filter)
.page(Some(2), token),
)
.await
.unwrap();
assert!(page.len() <= 2);
assert!(
page.iter()
.all(|a| a.properties.as_ref().unwrap()["keep"] == true)
);
seen.extend(page.into_iter().map(|a| a.id));
match next {
Some(t) => token = Some(t),
None => break,
}
}
assert_eq!(seen.len(), 6, "every matching edge must be paged");
seen.sort();
seen.dedup();
assert_eq!(seen.len(), 6, "no duplicates across pages");
}
pub async fn search_fallback_predicates_agree<S: StoreExec<ConformanceLabel>>(store: &S) {
let payloads = [
serde_json::json!({ "tags": ["red", "blue"], "state": "active", "nick": null, "a.b": 1 }),
serde_json::json!({ "tags": ["green"], "state": "archived" }),
serde_json::json!({ "tags": [], "state": "active", "count": 3 }),
serde_json::json!({ "note": "hello world", "state": "active", "a.b": 2 }),
];
for (i, p) in payloads.iter().enumerate() {
store
.create(
ConformanceLabel::Node,
&rn(&format!("f{i}")),
Some(p.clone()),
None,
None,
)
.await
.unwrap();
}
let check = async |filter: Filter| {
let (hits, _) = store
.search(ConformanceLabel::Node, None, &filter, None, None)
.await
.unwrap();
let mut got: Vec<_> = hits.iter().map(|o| o.properties.clone().unwrap()).collect();
let mut want: Vec<_> = payloads
.iter()
.filter(|p| filter.matches(p))
.cloned()
.collect();
got.sort_by_key(|v| v.to_string());
want.sort_by_key(|v| v.to_string());
assert_eq!(
got, want,
"fallback search disagreed with Filter::matches for {filter:?}"
);
};
check(Filter::contains("tags", "blue")).await;
check(Filter::contains("note", "world")).await;
check(Filter::ne("state", "active")).await;
check(Filter::ne("count", 3)).await;
check(Filter::eq("nick", serde_json::Value::Null)).await;
check(Filter::eq("tags", serde_json::json!([]))).await;
check(Filter::all([
Filter::eq("state", "active"),
Filter::contains("tags", "red"),
]))
.await;
check(Filter::any([
Filter::gt("count", 2),
Filter::ne("state", "archived"),
]))
.await;
check(Filter::eq(crate::filter::FieldPath::new(["a.b"]), 1)).await;
check(Filter::exists(crate::filter::FieldPath::new(["a.b"]))).await;
}
pub async fn edge_listing_is_recency_ordered<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let n = 7usize;
for i in 0..n {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("dst{i}")),
None,
None,
None,
)
.await
.unwrap();
store
.add(
src.id,
dst.id,
"link",
Some(serde_json::json!({ "seq": i })),
)
.await
.unwrap();
}
let (all, token) = store
.query_edges(EdgeQuery::from(src.id, "link"))
.await
.unwrap();
assert!(token.is_none());
let seqs: Vec<u64> = all
.iter()
.map(|e| e.properties.as_ref().unwrap()["seq"].as_u64().unwrap())
.collect();
let expected: Vec<u64> = (0..n as u64).rev().collect();
assert_eq!(seqs, expected, "listing must be most-recent-first");
let mut paged = Vec::new();
let mut tok = None;
loop {
let (page, next) = store
.query_edges(EdgeQuery::from(src.id, "link").page(Some(3), tok))
.await
.unwrap();
assert!(page.len() <= 3);
paged.extend(
page.iter()
.map(|e| e.properties.as_ref().unwrap()["seq"].as_u64().unwrap()),
);
match next {
Some(t) => tok = Some(t),
None => break,
}
}
assert_eq!(
paged, expected,
"paged listing must match the unpaged order"
);
}
pub async fn edge_time_window_selects_range<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let mut bounds = Vec::new();
for i in 0..5 {
bounds.push(chrono::Utc::now());
std::thread::sleep(std::time::Duration::from_millis(2));
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("dst{i}")),
None,
None,
None,
)
.await
.unwrap();
store
.add(
src.id,
dst.id,
"link",
Some(serde_json::json!({ "seq": i })),
)
.await
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
}
let seqs = async |q: EdgeQuery<'_, ConformanceLabel>| -> Vec<u64> {
let (edges, _) = store.query_edges(q).await.unwrap();
edges
.iter()
.map(|e| e.properties.as_ref().unwrap()["seq"].as_u64().unwrap())
.collect()
};
assert_eq!(
seqs(EdgeQuery::from(src.id, "link").since(bounds[2])).await,
vec![4, 3, 2],
"since selects edges created at or after the boundary"
);
assert_eq!(
seqs(EdgeQuery::from(src.id, "link").until(bounds[2])).await,
vec![1, 0],
"until excludes edges created at or after the boundary"
);
assert_eq!(
seqs(
EdgeQuery::from(src.id, "link")
.since(bounds[1])
.until(bounds[4])
)
.await,
vec![3, 2, 1],
"half-open window selects the interior edges"
);
assert!(
seqs(EdgeQuery::from(src.id, "link").until(bounds[0]))
.await
.is_empty(),
"window entirely before the first edge is empty"
);
}
pub async fn edge_target_label_pages_completely<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let mut want = 0usize;
for i in 0..12 {
let label = if i % 2 == 0 {
ConformanceLabel::Node
} else {
ConformanceLabel::Other
};
if label == ConformanceLabel::Other {
want += 1;
}
let dst = store
.create(label, &rn(&format!("dst{i}")), None, None, None)
.await
.unwrap();
store.add(src.id, dst.id, "link", None).await.unwrap();
}
let mut seen = Vec::new();
let mut tok = None;
loop {
let (page, next) = store
.query_edges(
EdgeQuery::from(src.id, "link")
.target_label(ConformanceLabel::Other)
.page(Some(2), tok),
)
.await
.unwrap();
assert!(page.len() <= 2);
assert!(
page.iter().all(|e| e.to_label == ConformanceLabel::Other),
"target_label must be honored"
);
seen.extend(page.into_iter().map(|e| e.id));
match next {
Some(t) => tok = Some(t),
None => break,
}
}
seen.sort();
seen.dedup();
assert_eq!(
seen.len(),
want,
"every Other-target edge must be paged, none dropped"
);
}
pub async fn incoming_edges_listed<S: StoreExec<ConformanceLabel>>(store: &S) {
let x = store
.create(ConformanceLabel::Node, &rn("x"), None, None, None)
.await
.unwrap();
for name in ["a", "b", "c"] {
let src = store
.create(ConformanceLabel::Node, &rn(name), None, None, None)
.await
.unwrap();
store.add(src.id, x.id, "link", None).await.unwrap();
}
let (incoming, token) = store
.query_edges(EdgeQuery::into(x.id, "link"))
.await
.unwrap();
assert!(token.is_none());
assert_eq!(incoming.len(), 3, "all incoming edges listed");
assert!(
incoming.iter().all(|e| e.to_id == x.id),
"incoming edges all point at x"
);
}
pub async fn edge_target_id_restriction<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let mut targets = Vec::new();
for i in 0..4 {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("dst{i}")),
None,
None,
None,
)
.await
.unwrap();
store.add(src.id, dst.id, "link", None).await.unwrap();
targets.push(dst.id);
}
let (hits, token) = store
.query_edges(EdgeQuery::from(src.id, "link").target_id(targets[2]))
.await
.unwrap();
assert!(token.is_none());
assert_eq!(hits.len(), 1, "exactly the one edge to the chosen target");
assert_eq!(hits[0].to_id, targets[2]);
}
pub async fn count_edges_matches_list<S: StoreExec<ConformanceLabel>>(store: &S) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
for i in 0..5 {
let label = if i < 2 {
ConformanceLabel::Other
} else {
ConformanceLabel::Node
};
let dst = store
.create(label, &rn(&format!("dst{i}")), None, None, None)
.await
.unwrap();
store.add(src.id, dst.id, "link", None).await.unwrap();
}
let count = store
.count_edges(EdgeEndpoint::From(src.id), "link", None)
.await
.unwrap();
assert_eq!(count, 5, "count matches total edges");
let count_other = store
.count_edges(
EdgeEndpoint::From(src.id),
"link",
Some(ConformanceLabel::Other),
)
.await
.unwrap();
assert_eq!(count_other, 2, "count honors target_label");
}
pub async fn listing_stable_under_concurrent_insert<S: StoreExec<ConformanceLabel>>(store: &S) {
let mut original = Vec::new();
for i in 0..6 {
let o = store
.create(
ConformanceLabel::Node,
&rn(&format!("o{i}")),
None,
None,
None,
)
.await
.unwrap();
original.push(o.id);
}
let (page, token) = store
.list(ConformanceLabel::Node, None, Some(2), None)
.await
.unwrap();
let returned: Vec<_> = page.into_iter().map(|o| o.id).collect();
let mut seen = returned.clone();
let mut token = token;
store.delete(&returned[0]).await.unwrap();
for i in 6..8 {
store
.create(
ConformanceLabel::Node,
&rn(&format!("o{i}")),
None,
None,
None,
)
.await
.unwrap();
}
while let Some(t) = token {
let (page, next) = store
.list(ConformanceLabel::Node, None, Some(2), Some(t))
.await
.unwrap();
seen.extend(page.into_iter().map(|o| o.id));
token = next;
}
for id in &original[1..] {
assert_eq!(
seen.iter().filter(|s| *s == id).count(),
1,
"live object {id} must be paged exactly once despite a delete before the cursor"
);
}
}
pub async fn edge_listing_stable_under_concurrent_insert<S: StoreExec<ConformanceLabel>>(
store: &S,
) {
let src = store
.create(ConformanceLabel::Node, &rn("src"), None, None, None)
.await
.unwrap();
let mut original = Vec::new();
for i in 0..6 {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("d{i}")),
None,
None,
None,
)
.await
.unwrap();
store.add(src.id, dst.id, "link", None).await.unwrap();
}
let (all, _) = store
.query_edges(EdgeQuery::from(src.id, "link"))
.await
.unwrap();
original.extend(all.iter().map(|e| e.id));
assert_eq!(original.len(), 6);
let (page, token) = store
.query_edges(EdgeQuery::from(src.id, "link").page(Some(2), None))
.await
.unwrap();
let mut seen: Vec<_> = page.into_iter().map(|e| e.id).collect();
let mut token = token;
for i in 6..8 {
let dst = store
.create(
ConformanceLabel::Node,
&rn(&format!("d{i}")),
None,
None,
None,
)
.await
.unwrap();
store.add(src.id, dst.id, "link", None).await.unwrap();
}
while let Some(t) = token {
let (page, next) = store
.query_edges(EdgeQuery::from(src.id, "link").page(Some(2), Some(t)))
.await
.unwrap();
seen.extend(page.into_iter().map(|e| e.id));
token = next;
}
for id in &original {
assert_eq!(
seen.iter().filter(|s| *s == id).count(),
1,
"original edge {id} must be paged exactly once under a concurrent insert"
);
}
}
pub async fn object_listing_is_chronological<S: StoreExec<ConformanceLabel>>(store: &S) {
let mut created_ids = Vec::new();
for i in 0..8 {
let o = store
.create(
ConformanceLabel::Node,
&rn(&format!("c{i}")),
None,
None,
None,
)
.await
.unwrap();
created_ids.push(o.id);
}
let (listed, _) = store
.list(ConformanceLabel::Node, None, None, None)
.await
.unwrap();
let listed_ids: Vec<_> = listed.iter().map(|o| o.id).collect();
assert_eq!(
listed_ids, created_ids,
"listing order must match creation order (v7 time-ordered ids)"
);
}
pub async fn page_token_rejected_across_queries<S: StoreExec<ConformanceLabel>>(store: &S) {
for i in 0..4 {
store
.create(
ConformanceLabel::Node,
&rn(&format!("ns.o{i}")),
None,
None,
None,
)
.await
.unwrap();
}
let (_, token) = store
.list(ConformanceLabel::Node, None, Some(2), None)
.await
.unwrap();
let token = token.expect("more than one page, so a token is minted");
let ns = rn("ns");
let err = store
.list(ConformanceLabel::Node, Some(&ns), Some(2), Some(token))
.await
.unwrap_err();
assert!(
matches!(err, Error::InvalidArgument(_)),
"a token from a different query must be rejected, got {err:?}"
);
}
pub async fn legacy_offset_token_rejected<S: StoreExec<ConformanceLabel>>(store: &S) {
store
.create(ConformanceLabel::Node, &rn("o"), None, None, None)
.await
.unwrap();
let err = store
.list(ConformanceLabel::Node, None, Some(1), Some("2".to_string()))
.await
.unwrap_err();
assert!(
matches!(err, Error::InvalidArgument(_)),
"a legacy plain-offset token must be rejected, got {err:?}"
);
}
pub async fn run_all<S, F, G>(fresh: F, with_inverse: G)
where
S: StoreExec<ConformanceLabel> + Transactional<ConformanceLabel>,
F: Fn() -> S,
G: Fn(fn(&str) -> Option<String>) -> S,
{
cas_update(&fresh()).await;
rename_semantics(&fresh()).await;
case_insensitive_names(&fresh()).await;
transaction_atomicity(&fresh()).await;
transaction_commit(&fresh()).await;
sensitive_blob_roundtrip(&fresh()).await;
inverse_edges(&with_inverse(parent_child_inverse)).await;
search_object_predicates(&fresh()).await;
search_object_pagination_filters_completely(&fresh()).await;
search_namespace_and_filter(&fresh()).await;
edge_filter_predicates(&fresh()).await;
edge_filter_pagination_completes(&fresh()).await;
search_fallback_predicates_agree(&fresh()).await;
edge_listing_is_recency_ordered(&fresh()).await;
edge_time_window_selects_range(&fresh()).await;
edge_target_label_pages_completely(&fresh()).await;
incoming_edges_listed(&fresh()).await;
edge_target_id_restriction(&fresh()).await;
count_edges_matches_list(&fresh()).await;
listing_stable_under_concurrent_insert(&fresh()).await;
edge_listing_stable_under_concurrent_insert(&fresh()).await;
object_listing_is_chronological(&fresh()).await;
page_token_rejected_across_queries(&fresh()).await;
legacy_offset_token_rejected(&fresh()).await;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InMemoryStore;
#[tokio::test]
async fn in_memory_store_passes_conformance() {
run_all(
InMemoryStore::<ConformanceLabel>::new,
InMemoryStore::<ConformanceLabel>::with_inverse,
)
.await;
}
}