Compare commits

..

3 Commits

Author SHA1 Message Date
Laurin Weger e63941054a
wip 4 days ago
Laurin Weger 952dce50d9
fix nested fetching and processing of objects becoming valid 4 days ago
Laurin Weger 5bab0073c0
fix patch object creation 4 days ago
  1. 15
      engine/verifier/src/orm/add_remove_triples.rs
  2. 188
      engine/verifier/src/orm/handle_backend_update.rs
  3. 180
      engine/verifier/src/orm/process_changes.rs
  4. 8
      engine/verifier/src/orm/shape_validation.rs
  5. 627
      sdk/rust/src/tests/orm_patches.rs

@ -92,7 +92,7 @@ pub fn add_remove_triples(
// log_debug!("lock acquired on tracked_predicate");
tracked_predicate.current_cardinality += 1;
// Keep track of the changed values too.
// Keep track of the added values here.
let pred_changes: &mut OrmTrackedPredicateChanges = subject_changes
.predicates
.entry(predicate_schema.iri.clone())
@ -163,6 +163,7 @@ pub fn add_remove_triples(
}
}
}
// Process removed triples.
for triple in triples_removed {
let pred_iri = triple.predicate.as_str();
@ -181,9 +182,15 @@ pub fn add_remove_triples(
tracked_predicate.current_cardinality =
tracked_predicate.current_cardinality.saturating_sub(1);
let Some(pred_changes) = subject_changes.predicates.get_mut(pred_iri) else {
continue;
};
// Keep track of removed values here.
let pred_changes: &mut OrmTrackedPredicateChanges = subject_changes
.predicates
.entry(tracked_predicate.schema.iri.clone())
.or_insert_with(|| OrmTrackedPredicateChanges {
tracked_predicate: tracked_predicate_rc.clone(),
values_added: Vec::new(),
values_removed: Vec::new(),
});
let val_removed = oxrdf_term_to_orm_basic_type(&triple.object);
pred_changes.values_removed.push(val_removed.clone());

@ -101,7 +101,7 @@ impl Verifier {
}
log_debug!(
"[orm_backend_update], creating patch objects for scopes:\n{}",
"[orm_backend_update], creating patch objects for #scopes {}",
scopes.len()
);
for (scope, shapes) in scopes {
@ -109,8 +109,10 @@ impl Verifier {
// Apply the changes to tracked subjects.
for shape_arc in shapes {
let shape_iri = shape_arc.iri.clone();
let _ = self.process_changes_for_shape_and_session(
&scope,
&shape_iri,
shape_arc,
session_id,
&triple_inserts,
@ -131,9 +133,9 @@ impl Verifier {
// The JSON patches to send to JS land.
let mut patches: Vec<OrmDiffOp> = vec![];
// Keep track of created objects by path and if they need an id.
// Later we created patches from them to ensure the objects exist.
let mut paths_of_objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> =
// Keep track of objects to create: (path, Option<IRI>)
// The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers)
let mut objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> =
HashSet::new();
// We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...}
@ -193,7 +195,7 @@ impl Verifier {
&mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None),
&mut patches,
&mut paths_of_objects_to_create,
&mut objects_to_create,
);
} else {
// The subject is valid or has become valid.
@ -218,7 +220,7 @@ impl Verifier {
&mut path,
diff_op,
&mut patches,
&mut paths_of_objects_to_create,
&mut objects_to_create,
);
}
}
@ -229,26 +231,29 @@ impl Verifier {
// Create patches for objects that need to be created
// These are patches with {op: add, valType: object, value: Null, path: ...}
// Sort by path length (shorter first) to ensure parent objects are created before children
let mut sorted_object_paths: Vec<_> = paths_of_objects_to_create.iter().collect();
sorted_object_paths.sort_by_key(|(path_segments, _)| path_segments.len());
let mut sorted_objects: Vec<_> = objects_to_create.iter().collect();
sorted_objects.sort_by_key(|(path_segments, _)| path_segments.len());
for (path_segments, maybe_iri) in sorted_object_paths {
for (path_segments, maybe_iri) in sorted_objects {
let escaped_path: Vec<String> = path_segments
.iter()
.map(|seg| escape_json_pointer(seg))
.collect();
let json_pointer = format!("/{}", escaped_path.join("/"));
// Always create the object itself
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: json_pointer.clone(),
value: None,
});
// If this object has an IRI (it's a real subject), add the id field
if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
valType: None,
path: format!("{}/id", json_pointer),
value: Some(json!(iri)),
});
@ -266,6 +271,108 @@ impl Verifier {
}
}
/// Queue patches for a newly valid tracked subject.
/// This handles creating object patches and id field patches for subjects that have become valid.
fn queue_patches_for_newly_valid_subject(
tracked_subject: &OrmTrackedSubject,
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
root_shape: &String,
path: &[String],
patches: &mut Vec<OrmDiffOp>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
// Check if we're at a root subject or need to traverse to parents
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
// Register object for creation.
// Path to object consists of this subject's iri and the path except for the last element.
let mut path_to_subject = vec![tracked_subject.subject_iri.clone()];
if path.len() > 1 {
path_to_subject.extend_from_slice(&path[..path.len() - 1]);
}
// log_debug!("Queuing object creation for path: {:?}", path_to_subject);
// Always create the object itself with its IRI
objects_to_create.insert((
path_to_subject.clone(),
Some(tracked_subject.subject_iri.clone()),
));
} else {
// Not at root: traverse to parents and create object patches along the way
for (_parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
let parent_ts = parent_tracked_subject.read().unwrap();
if let Some(new_path) = build_path_segment_for_parent(tracked_subject, &parent_ts, path)
{
// Check if the parent's predicate is multi-valued and if no siblings were previously valid
let should_create_parent_predicate_object =
check_should_create_parent_predicate_object(tracked_subject, &parent_ts);
if should_create_parent_predicate_object {
// Need to create an intermediate object for the multi-valued predicate
// This is the case for Person -> hasAddress -> (object) -> AddressIri -> AddressObject
// The intermediate (object) doesn't have an IRI
let mut intermediate_path = new_path.clone();
intermediate_path.pop(); // Remove the subject IRI that was added for multi predicates
objects_to_create.insert((intermediate_path, None));
}
// Recurse to the parent first
queue_patches_for_newly_valid_subject(
&parent_ts,
tracked_subjects,
root_shape,
&new_path,
patches,
objects_to_create,
);
// Register this object for creation with its IRI
objects_to_create
.insert((new_path.clone(), Some(tracked_subject.subject_iri.clone())));
}
}
}
}
/// Check if we should create an intermediate object for a multi-valued predicate.
/// Returns true if the parent's predicate is multi-valued and no siblings were previously valid.
fn check_should_create_parent_predicate_object(
tracked_subject: &OrmTrackedSubject,
parent_ts: &OrmTrackedSubject,
) -> bool {
// Find the predicate schema linking parent to this subject
for pred_arc in &parent_ts.shape.predicates {
if let Some(tracked_pred) = parent_ts.tracked_predicates.get(&pred_arc.iri) {
let tp = tracked_pred.read().unwrap();
// Check if this tracked subject is a child of this predicate
let is_child = tp.tracked_children.iter().any(|child| {
let child_read = child.read().unwrap();
child_read.subject_iri == tracked_subject.subject_iri
});
if is_child {
let is_multi = pred_arc.maxCardinality > 1 || pred_arc.maxCardinality == -1;
if is_multi {
// Check if any siblings were previously valid
let any_sibling_was_valid = tp.tracked_children.iter().any(|child| {
let child_read = child.read().unwrap();
child_read.subject_iri != tracked_subject.subject_iri
&& child_read.prev_valid == OrmTrackedSubjectValidity::Valid
});
return !any_sibling_was_valid;
}
return false;
}
}
}
false
}
/// Find the predicate schema linking a parent to a child tracked subject and build the path segment.
/// Returns the updated path if a linking predicate is found.
fn build_path_segment_for_parent(
@ -321,7 +428,7 @@ fn build_path_to_root_and_create_patches(
Option<String>, // The IRI, if change is an added / removed object.
),
patches: &mut Vec<OrmDiffOp>,
paths_of_objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
// If the tracked subject is not valid, we don't create patches for it
if tracked_subject.valid != OrmTrackedSubjectValidity::Valid {
@ -331,54 +438,15 @@ fn build_path_to_root_and_create_patches(
// If the tracked subject is newly valid (was not valid before but is now),
// we need to ensure the object is created with an "add object" patch
if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid {
// Check if we're at a root subject or need to traverse to parents
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
// At root: build the path with the subject IRI
let escaped_path: Vec<String> =
path.iter().map(|seg| escape_json_pointer(seg)).collect();
let json_pointer = format!(
"/{}/{}",
escape_json_pointer(&tracked_subject.subject_iri),
escaped_path.join("/")
);
// Create an "add object" patch to ensure the object exists
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: json_pointer.clone(),
value: None,
});
// Also add the id field for the object
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: None,
path: format!("{}/id", json_pointer),
value: Some(json!(tracked_subject.subject_iri)),
});
} else {
// Not at root: traverse to parents and create object patches along the way
for (_parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
let parent_ts = parent_tracked_subject.read().unwrap();
if let Some(new_path) =
build_path_segment_for_parent(tracked_subject, &parent_ts, path)
{
// Recurse to the parent first
build_path_to_root_and_create_patches(
&parent_ts,
queue_patches_for_newly_valid_subject(
tracked_subject,
tracked_subjects,
root_shape,
&mut new_path.clone(),
(OrmDiffOpType::add, Some(OrmDiffType::object), None, None),
path,
patches,
paths_of_objects_to_create,
objects_to_create,
);
}
}
}
}
// If this subject has no parents or its shape matches the root shape, we've reached the root
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
@ -399,16 +467,6 @@ fn build_path_to_root_and_create_patches(
value: diff_op.2.clone(),
});
// // If a new object is created on a predicate where multiple ones are allowed, create IRI path too.
// if let Some(added_obj_iri) = diff_op.3 {
// patches.push(OrmDiffOp {
// op: diff_op.0.clone(),
// valType: diff_op.1.clone(),
// path: format!("{}/{}", json_pointer, escape_json_pointer(&added_obj_iri)),
// value: diff_op.2.clone(),
// });
// }
return;
}
@ -426,7 +484,7 @@ fn build_path_to_root_and_create_patches(
&mut new_path.clone(),
diff_op.clone(),
patches,
paths_of_objects_to_create,
objects_to_create,
);
}
}

@ -81,54 +81,15 @@ impl Verifier {
Ok(merged)
}
/// Helper to call process_changes_for_shape for all subscriptions on nuri's document.
fn process_changes_for_nuri_and_session(
self: &mut Self,
nuri: &NuriV0,
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple],
data_already_fetched: bool,
) -> Result<OrmChanges, NgError> {
let mut orm_changes = HashMap::new();
let shapes: Vec<_> = self
.orm_subscriptions
.get(nuri)
.unwrap()
.iter()
.map(|sub| {
sub.shape_type
.schema
.get(&sub.shape_type.shape)
.unwrap()
.clone()
})
.collect();
for root_shape in shapes {
self.process_changes_for_shape_and_session(
nuri,
root_shape,
session_id,
triples_added,
triples_removed,
&mut orm_changes,
data_already_fetched,
)?;
}
Ok(orm_changes)
}
/// Add and remove the triples from the tracked subjects,
/// re-validate, and update `changes` containing the updated data.
/// Works by queuing changes by shape and subjects on a stack.
/// Nested objects are added to the stack
pub(crate) fn process_changes_for_shape_and_session(
self: &mut Self,
&mut self,
nuri: &NuriV0,
root_shape: Arc<OrmSchemaShape>,
root_shape_iri: &String,
shape: Arc<OrmSchemaShape>,
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple],
@ -140,8 +101,7 @@ impl Verifier {
// Track (shape_iri, subject_iri) pairs currently being validated to prevent cycles and double evaluation.
let mut currently_validating: HashSet<(String, String)> = HashSet::new();
// Add root shape for first validation run.
let root_shape_iri = root_shape.iri.clone();
shape_validation_stack.push((root_shape, vec![]));
shape_validation_stack.push((shape, vec![]));
// Process queue of shapes and subjects to validate.
// For a given shape, we evaluate every subject against that shape.
@ -156,14 +116,6 @@ impl Verifier {
.chain(removed_triples_by_subject.keys())
.collect();
let mut orm_subscription = self
.orm_subscriptions
.get_mut(nuri)
.unwrap()
.iter_mut()
.find(|sub| sub.session_id == session_id && sub.shape_type.shape == root_shape_iri)
.unwrap();
// Variable to collect nested objects that need validation.
let mut nested_objects_to_eval: HashMap<ShapeIri, Vec<(SubjectIri, bool)>> =
HashMap::new();
@ -185,8 +137,13 @@ impl Verifier {
subject_iri,
shape.iri
);
// Mark as invalid due to cycle
// TODO: We could handle this by handling nested references as IRIs.
// Find tracked and mark as invalid.
let orm_subscription = &mut self.get_first_orm_subscription_for(
nuri,
Some(&root_shape_iri),
Some(&session_id),
);
if let Some(tracked_shapes) =
orm_subscription.tracked_subjects.get(*subject_iri)
{
@ -226,30 +183,72 @@ impl Verifier {
// Apply all triples for that subject to the tracked (shape, subject) pair.
// Record the changes.
{
let orm_subscription = self
.orm_subscriptions
.get_mut(nuri)
.unwrap()
.iter_mut()
.find(|sub| {
sub.shape_type.shape == *root_shape_iri && sub.session_id == session_id
})
.unwrap();
// Update tracked subjects and modify change objects.
if !change.data_applied {
log_debug!(
"Adding triples to change tracker for subject {}",
subject_iri
);
if let Err(e) = add_remove_triples(
shape.clone(),
subject_iri,
triples_added_for_subj,
triples_removed_for_subj,
&mut orm_subscription,
orm_subscription,
change,
) {
log_err!("apply_changes_from_triples add/remove error: {:?}", e);
panic!();
}
change.data_applied = true;
} else {
}
// Check if this is the first evaluation round - In that case, set old validity to new one.
// if the object was already validated, don't do so again.
{
let tracked_subject = &mut orm_subscription
.tracked_subjects
.get(*subject_iri)
.unwrap()
.get(&shape.iri)
.unwrap()
.write()
.unwrap();
// First run
if !change.data_applied
&& tracked_subject.valid != OrmTrackedSubjectValidity::Pending
{
tracked_subject.prev_valid = tracked_subject.valid.clone();
}
if change.data_applied {
log_debug!("not applying triples again for subject {subject_iri}");
// Has this subject already been validated?
if change.data_applied
&& tracked_subject.valid != OrmTrackedSubjectValidity::Pending
{
log_debug!("Not evaluating subject again {subject_iri}");
continue;
}
}
}
// Validate the subject.
let need_eval =
Self::update_subject_validity(change, &shape, &mut orm_subscription);
let need_eval = Self::update_subject_validity(change, &shape, orm_subscription);
// We add the need_eval to be processed next after loop.
// Filter out subjects already in the validation stack to prevent double evaluation.
@ -268,13 +267,15 @@ impl Verifier {
// Now, we queue all non-evaluated objects
for (shape_iri, objects_to_eval) in &nested_objects_to_eval {
let orm_subscription = self.get_first_orm_subscription_for(
// Extract schema and shape Arc first (before any borrows)
let schema = {
let orm_sub = self.get_first_orm_subscription_for(
nuri,
Some(&root_shape_iri),
Some(&session_id),
);
// Extract schema and shape Arc before mutable borrow
let schema = orm_subscription.shape_type.schema.clone();
orm_sub.shape_type.schema.clone()
};
let shape_arc = schema.get(shape_iri).unwrap().clone();
// Data might need to be fetched (if it has not been during initialization or nested shape fetch).
@ -294,6 +295,7 @@ impl Verifier {
// Recursively process nested objects.
self.process_changes_for_shape_and_session(
nuri,
&root_shape_iri,
shape_arc.clone(),
session_id,
&new_triples,
@ -323,23 +325,47 @@ impl Verifier {
Ok(())
}
/// Helper to get orm subscriptions for nuri, shapes and sessions.
pub fn get_orm_subscriptions_for(
&self,
/// Helper to call process_changes_for_shape for all subscriptions on nuri's document.
fn process_changes_for_nuri_and_session(
self: &mut Self,
nuri: &NuriV0,
shape: Option<&ShapeIri>,
session_id: Option<&u64>,
) -> Vec<&OrmSubscription> {
self.orm_subscriptions.get(nuri).unwrap().
// Filter shapes, if present.
iter().filter(|s| match shape {
Some(sh) => *sh == s.shape_type.shape,
None => true
// Filter session ids if present.
}).filter(|s| match session_id {
Some(id) => *id == s.session_id,
None => true
}).collect()
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple],
data_already_fetched: bool,
) -> Result<OrmChanges, NgError> {
let mut orm_changes = HashMap::new();
let shapes: Vec<_> = self
.orm_subscriptions
.get(nuri)
.unwrap()
.iter()
.map(|sub| {
sub.shape_type
.schema
.get(&sub.shape_type.shape)
.unwrap()
.clone()
})
.collect();
for root_shape in shapes {
let shape_iri = root_shape.iri.clone();
// Now we can safely call the method with self
self.process_changes_for_shape_and_session(
nuri,
&shape_iri,
root_shape,
session_id,
triples_added,
triples_removed,
&mut orm_changes,
data_already_fetched,
)?;
}
Ok(orm_changes)
}
pub fn get_first_orm_subscription_for(

@ -33,7 +33,6 @@ impl Verifier {
};
let mut tracked_subject = tracked_subject.write().unwrap();
let previous_validity = tracked_subject.prev_valid.clone();
tracked_subject.prev_valid = tracked_subject.valid.clone();
// Keep track of objects that need to be validated against a shape to fetch and validate.
let mut need_evaluation: Vec<(String, String, bool)> = vec![];
@ -45,7 +44,10 @@ impl Verifier {
);
// Check 1) Check if this object is untracked and we need to remove children and ourselves.
if previous_validity == OrmTrackedSubjectValidity::Untracked {
if previous_validity == OrmTrackedSubjectValidity::Untracked
// If .valid is pending, this part was executed before in this validation round.
&& tracked_subject.valid != OrmTrackedSubjectValidity::Pending
{
// 1.1) Schedule children for deletion
// 1.1.1) Set all children to `untracked` that don't have other parents.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
@ -342,6 +344,8 @@ impl Verifier {
};
}
// == End of validation part. Next, process side-effects ==
tracked_subject.valid = new_validity.clone();
if new_validity == OrmTrackedSubjectValidity::Invalid {

@ -23,18 +23,21 @@ use serde_json::Value;
use std::collections::HashMap;
#[async_std::test]
async fn test_orm_path_creation() {
async fn test_orm_patch_creation() {
// Setup wallet and document
let (_wallet, session_id) = create_or_open_wallet().await;
// Tests below all in this test, to prevent waiting times through wallet creation.
// ===
test_patch_add_array(session_id).await;
test_patch_remove_array(session_id).await;
// // ===
// test_patch_add_array(session_id).await;
// test_patch_remove_array(session_id).await;
// // ===
// test_orm_with_optional(session_id).await;
// test_patch_add_nested_1(session_id).await;
// ===
test_patch_nested_house_inhabitants(session_id).await;
// // ===
// test_orm_literal(session_id).await;
@ -153,6 +156,7 @@ INSERT DATA {
ex:arr 3 .
<urn:test:numArrayObj4>
a ex:TestObject ;
ex:arr 0 .
}
"#
@ -196,7 +200,6 @@ INSERT DATA {
"value": [3.0],
"path": "/urn:test:numArrayObj3/numArray",
},
// TODO: The two below are not added.
{
"op": "add",
"valType": "object",
@ -209,6 +212,12 @@ INSERT DATA {
"path": "/urn:test:numArrayObj4/id",
"valType": Value::Null,
},
{
"op": "add",
"value": "http://example.org/TestObject",
"path": "/urn:test:numArrayObj4/type",
"valType": Value::Null,
},
{
"op": "add",
"valType": "set",
@ -353,6 +362,9 @@ DELETE DATA {
}
}
/// Tests edge case that is an open TODO about a modified nested object
/// that changes so that another allowed shape becomes valid.
/// See handle_backend_update's TODO comment.
async fn test_patch_add_nested_1(session_id: u64) {
let doc_nuri = create_doc_with_data(
session_id,
@ -592,39 +604,32 @@ INSERT DATA {
}
}
/*
// Temporary file - content to be appended to orm_patches.rs
Old things
*/
async fn test_orm_nested_2(session_id: u64) {
/// Test nested modifications with House -> Person -> Cat hierarchy
async fn test_patch_nested_house_inhabitants(session_id: u64) {
let doc_nuri = create_doc_with_data(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
# Valid
<urn:test:alice>
ex:knows <urn:test:bob>, <urn:test:claire> ;
ex:name "Alice" .
<urn:test:bob>
ex:knows <urn:test:claire> ;
ex:name "Bob" .
<urn:test:claire>
ex:name "Claire" .
# Invalid because claire2 is invalid
<urn:test:alice2>
ex:knows <urn:test:bob2>, <urn:test:claire2> ;
ex:name "Alice" .
# Invalid because claire2 is invalid
<urn:test:bob2>
ex:knows <urn:test:claire2> ;
<urn:test:house1>
a ex:House ;
ex:rootColor "blue" ;
ex:inhabitants <urn:test:person1>, <urn:test:person2> .
<urn:test:person1>
a ex:Person ;
ex:name "Alice" ;
ex:hasCat <urn:test:cat1> .
<urn:test:person2>
a ex:Person ;
ex:name "Bob" .
# Invalid because name is missing.
<urn:test:claire2>
ex:missingName "Claire missing" .
<urn:test:cat1>
a ex:Cat ;
ex:catName "Whiskers" .
}
"#
.to_string(),
@ -633,17 +638,34 @@ INSERT DATA {
// Define the ORM schema
let mut schema = HashMap::new();
// House shape
schema.insert(
"http://example.org/PersonShape".to_string(),
"http://example.org/HouseShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/PersonShape".to_string(),
iri: "http://example.org/HouseShape".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://example.org/name".to_string(),
extra: None,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "name".to_string(),
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str(
"http://example.org/House".to_string(),
)]),
shape: None,
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/rootColor".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 0,
readablePredicate: "rootColor".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
@ -652,11 +674,11 @@ INSERT DATA {
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(),
iri: "http://example.org/inhabitants".to_string(),
extra: Some(false),
maxCardinality: -1,
minCardinality: 0,
readablePredicate: "knows".to_string(),
minCardinality: 1,
readablePredicate: "inhabitants".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
@ -669,188 +691,50 @@ INSERT DATA {
.into(),
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/PersonShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id)
.await
.expect("orm_start");
while let Some(app_response) = receiver.next().await {
let orm_json = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json),
_ => None,
},
}
.unwrap();
log_info!(
"ORM JSON arrived for nested2 (person) test\n: {:?}",
orm_json
);
// Expected: alice and bob with their nested knows relationships
// claire2 is invalid (missing name), so alice2's knows chain is incomplete
let mut expected = json!([
{
"id": "urn:test:alice",
"name": "Alice",
"knows": {
"urn:test:bob": {
"name": "Bob",
"knows": {
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
{
"id": "urn:test:bob",
"name": "Bob",
"knows": {
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
{
"id": "urn:test:claire",
"name": "Claire",
"knows": {}
}
]);
let mut actual_mut = orm_json.clone();
log_info!(
"JSON for nested2\n{}",
serde_json::to_string(&actual_mut).unwrap()
);
assert_json_eq(&mut expected, &mut actual_mut);
break;
}
cancel_fn();
}
async fn test_orm_nested_3(session_id: u64) {
let doc_nuri = create_doc_with_data(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
# Valid
<urn:test:alice>
a ex:Alice ;
ex:knows <urn:test:bob>, <urn:test:claire> .
<urn:test:bob>
a ex:Bob ;
ex:knows <urn:test:claire> .
<urn:test:claire>
a ex:Claire .
# Invalid because claire is invalid
<urn:test:alice2>
a ex:Alice ;
ex:knows <urn:test:bob2>, <urn:test:claire2> .
# Invalid because claire is invalid
<urn:test:bob2>
a ex:Bob ;
ex:knows <urn:test:claire2> .
# Invalid, wrong type.
<urn:test:claire2>
a ex:Claire2 .
}
"#
.to_string(),
)
.await;
// Define the ORM schema
let mut schema = HashMap::new();
// Person shape
schema.insert(
"http://example.org/AliceShape".to_string(),
"http://example.org/PersonShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/AliceShape".to_string(),
iri: "http://example.org/PersonShape".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None,
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str(
"http://example.org/Alice".to_string(),
"http://example.org/Person".to_string(),
)]),
shape: None,
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(),
iri: "http://example.org/name".to_string(),
extra: Some(false),
maxCardinality: -1,
minCardinality: 0,
readablePredicate: "knows".to_string(),
dataTypes: vec![
OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/BobShape".to_string()),
},
OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/ClaireShape".to_string()),
},
],
}
.into(),
],
}
.into(),
);
schema.insert(
"http://example.org/BobShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/BobShape".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(true),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
readablePredicate: "name".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Bob".to_string())]),
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(),
iri: "http://example.org/hasCat".to_string(),
extra: Some(false),
maxCardinality: -1,
maxCardinality: 1,
minCardinality: 0,
readablePredicate: "knows".to_string(),
readablePredicate: "cat".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/ClaireShape".to_string()),
shape: Some("http://example.org/CatShape".to_string()),
}],
}
.into(),
@ -858,32 +742,47 @@ INSERT DATA {
}
.into(),
);
// Cat shape
schema.insert(
"http://example.org/ClaireShape".to_string(),
"http://example.org/CatShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/ClaireShape".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://example.org/CatShape".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None,
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str(
"http://example.org/Claire".to_string(),
)]),
literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]),
shape: None,
}],
}
.into()],
.into(),
OrmSchemaPredicate {
iri: "http://example.org/catName".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "name".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
}
.into(),
],
}
.into(),
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/AliceShape".to_string(),
shape: "http://example.org/HouseShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
@ -891,8 +790,9 @@ INSERT DATA {
.await
.expect("orm_start");
// Get initial state
while let Some(app_response) = receiver.next().await {
let orm_json = match app_response {
let _ = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json),
_ => None,
@ -900,160 +800,249 @@ INSERT DATA {
}
.unwrap();
break;
}
log_info!(
"ORM JSON arrived for nested3 (person) test\n: {:?}",
serde_json::to_string(&orm_json).unwrap()
"\n=== TEST 1: INSERT - Adding new person with cat, modifying existing properties ===\n"
);
// Expected: alice with knows relationships to bob and claire
// alice2 is incomplete because claire2 has wrong type
let mut expected = json!([
{
"id": "urn:test:alice",
"type": "http://example.org/Alice",
"knows": {
"urn:test:bob": {
"type": "http://example.org/Bob",
"knows": {
"urn:test:claire": {
"type": "http://example.org/Claire"
}
}
// INSERT: Add a new person with a cat, modify house color, modify existing person's name, add cat to Bob
doc_sparql_update(
session_id,
r#"
PREFIX ex: <http://example.org/>
DELETE DATA {
<urn:test:house1> ex:rootColor "blue" .
<urn:test:person1> ex:name "Alice" .
}
;
INSERT DATA {
<urn:test:house1>
ex:rootColor "red" ;
ex:inhabitants <urn:test:person3> .
<urn:test:person1>
ex:name "Alicia" .
<urn:test:person2>
ex:hasCat <urn:test:cat2> .
<urn:test:person3>
a ex:Person ;
ex:name "Charlie" ;
ex:hasCat <urn:test:cat3> .
<urn:test:cat2>
a ex:Cat ;
ex:catName "Mittens" .
<urn:test:cat3>
a ex:Cat ;
ex:catName "Fluffy" .
}
"#
.to_string(),
Some(doc_nuri.clone()),
)
.await
.expect("INSERT SPARQL update failed");
while let Some(app_response) = receiver.next().await {
let patches = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmUpdate(json) => Some(json),
_ => None,
},
"urn:test:claire": {
"type": "http://example.org/Claire"
}
}
.unwrap();
log_info!("INSERT patches arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
}
let mut expected = json!([
// Modified house color
{
"op": "remove",
"path": "/urn:test:house1/rootColor",
},
{
"op": "add",
"value": "red",
"path": "/urn:test:house1/rootColor",
},
// Modified Alice's name
{
"op": "remove",
"path": "/urn:test:house1/inhabitants/urn:test:person1/name",
},
{
"op": "add",
"value": "Alicia",
"path": "/urn:test:house1/inhabitants/urn:test:person1/name",
},
// Bob gets a cat
{
"op": "add",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat",
},
{
"op": "add",
"value": "urn:test:cat2",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/id",
},
{
"op": "add",
"value": "http://example.org/Cat",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/type",
},
{
"op": "add",
"value": "Mittens",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/name",
},
// New person Charlie with cat
{
"op": "add",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person3",
},
{
"op": "add",
"value": "urn:test:person3",
"path": "/urn:test:house1/inhabitants/urn:test:person3/id",
},
{
"op": "add",
"value": "http://example.org/Person",
"path": "/urn:test:house1/inhabitants/urn:test:person3/type",
},
{
"op": "add",
"value": "Charlie",
"path": "/urn:test:house1/inhabitants/urn:test:person3/name",
},
{
"op": "add",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat",
},
{
"op": "add",
"value": "urn:test:cat3",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat/id",
},
{
"op": "add",
"value": "http://example.org/Cat",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat/type",
},
{
"op": "add",
"value": "Fluffy",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat/name",
},
]);
let mut actual_mut = orm_json.clone();
assert_json_eq(&mut expected, &mut actual_mut);
let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual);
break;
}
cancel_fn();
}
async fn test_orm_nested_4(session_id: u64) {
let doc_nuri = create_doc_with_data(
log_info!("\n=== TEST 2: DELETE - Removing cat, person, and modifying properties ===\n");
// DELETE: Remove Whiskers, remove Charlie and his cat, modify cat name, remove house color
doc_sparql_update(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
# Valid
<urn:test:alice>
DELETE DATA {
<urn:test:house1>
ex:rootColor "red" ;
ex:inhabitants <urn:test:person3> .
<urn:test:person1>
ex:hasCat <urn:test:cat1> .
<urn:test:person3>
a ex:Person ;
ex:hasCat <urn:test:kitten1>, <urn:test:kitten2> .
<urn:test:kitten1>
a ex:Cat .
<urn:test:kitten2>
a ex:Cat .
ex:name "Charlie" ;
ex:hasCat <urn:test:cat3> .
<urn:test:cat1>
a ex:Cat ;
ex:catName "Whiskers" .
<urn:test:cat2>
ex:catName "Mittens" .
<urn:test:cat3>
a ex:Cat ;
ex:catName "Fluffy" .
}
;
INSERT DATA {
<urn:test:cat2>
ex:catName "Mr. Mittens" .
}
"#
.to_string(),
Some(doc_nuri.clone()),
)
.await;
// Define the ORM schema
let mut schema = HashMap::new();
schema.insert(
"http://example.org/PersonShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/PersonShape".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None,
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str(
"http://example.org/Person".to_string(),
)]),
shape: None,
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/hasCat".to_string(),
extra: Some(false),
maxCardinality: -1,
minCardinality: 0,
readablePredicate: "cats".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/CatShape".to_string()),
}],
}
.into(),
],
}
.into(),
);
schema.insert(
"http://example.org/CatShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/CatShape".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(true),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]),
shape: None,
}],
}
.into()],
}
.into(),
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/PersonShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id)
.await
.expect("orm_start");
.expect("DELETE SPARQL update failed");
while let Some(app_response) = receiver.next().await {
let orm_json = match app_response {
let patches = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json),
AppResponseV0::OrmUpdate(json) => Some(json),
_ => None,
},
}
.unwrap();
log_info!("DELETE patches arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
}
let mut expected = json!([
// Remove house color
{
"id": "urn:test:alice",
"type": "http://example.org/Person",
"cats": {
"urn:test:kitten1": {
"type": "http://example.org/Cat"
"op": "remove",
"path": "/urn:test:house1/rootColor",
},
"urn:test:kitten2": {
"type": "http://example.org/Cat"
}
// Alice loses her cat
{
"op": "remove",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person1/cat",
},
// Bob's cat name changes
{
"op": "remove",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/name",
},
{
"op": "add",
"value": "Mr. Mittens",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/name",
},
// Charlie and his cat are removed
{
"op": "remove",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person3",
},
}
]);
let mut actual_mut = orm_json.clone();
assert_json_eq(&mut expected, &mut actual_mut);
let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual);
break;
}
cancel_fn();
}

Loading…
Cancel
Save