fix patch object creation

feat/orm-diffs
Laurin Weger 3 days ago
parent 9c1165653b
commit 5bab0073c0
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 15
      engine/verifier/src/orm/add_remove_triples.rs
  2. 186
      engine/verifier/src/orm/handle_backend_update.rs
  3. 617
      sdk/rust/src/tests/orm_patches.rs

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

@ -101,7 +101,7 @@ impl Verifier {
} }
log_debug!( log_debug!(
"[orm_backend_update], creating patch objects for scopes:\n{}", "[orm_backend_update], creating patch objects for #scopes {}",
scopes.len() scopes.len()
); );
for (scope, shapes) in scopes { for (scope, shapes) in scopes {
@ -131,9 +131,9 @@ impl Verifier {
// The JSON patches to send to JS land. // The JSON patches to send to JS land.
let mut patches: Vec<OrmDiffOp> = vec![]; let mut patches: Vec<OrmDiffOp> = vec![];
// Keep track of created objects by path and if they need an id. // Keep track of objects to create: (path, Option<IRI>)
// Later we created patches from them to ensure the objects exist. // The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers)
let mut paths_of_objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> = let mut objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> =
HashSet::new(); HashSet::new();
// We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...} // We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...}
@ -193,7 +193,7 @@ impl Verifier {
&mut path, &mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None), (OrmDiffOpType::remove, Some(OrmDiffType::object), None, None),
&mut patches, &mut patches,
&mut paths_of_objects_to_create, &mut objects_to_create,
); );
} else { } else {
// The subject is valid or has become valid. // The subject is valid or has become valid.
@ -218,7 +218,7 @@ impl Verifier {
&mut path, &mut path,
diff_op, diff_op,
&mut patches, &mut patches,
&mut paths_of_objects_to_create, &mut objects_to_create,
); );
} }
} }
@ -229,26 +229,29 @@ impl Verifier {
// Create patches for objects that need to be created // Create patches for objects that need to be created
// These are patches with {op: add, valType: object, value: Null, path: ...} // 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 // 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(); let mut sorted_objects: Vec<_> = objects_to_create.iter().collect();
sorted_object_paths.sort_by_key(|(path_segments, _)| path_segments.len()); 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 let escaped_path: Vec<String> = path_segments
.iter() .iter()
.map(|seg| escape_json_pointer(seg)) .map(|seg| escape_json_pointer(seg))
.collect(); .collect();
let json_pointer = format!("/{}", escaped_path.join("/")); let json_pointer = format!("/{}", escaped_path.join("/"));
// Always create the object itself
patches.push(OrmDiffOp { patches.push(OrmDiffOp {
op: OrmDiffOpType::add, op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object), valType: Some(OrmDiffType::object),
path: json_pointer.clone(), path: json_pointer.clone(),
value: None, value: None,
}); });
// If this object has an IRI (it's a real subject), add the id field
if let Some(iri) = maybe_iri { if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp { patches.push(OrmDiffOp {
op: OrmDiffOpType::add, op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object), valType: None,
path: format!("{}/id", json_pointer), path: format!("{}/id", json_pointer),
value: Some(json!(iri)), value: Some(json!(iri)),
}); });
@ -266,6 +269,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() > 0 {
path_to_subject.extend_from_slice(&path[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. /// 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. /// Returns the updated path if a linking predicate is found.
fn build_path_segment_for_parent( fn build_path_segment_for_parent(
@ -321,7 +426,7 @@ fn build_path_to_root_and_create_patches(
Option<String>, // The IRI, if change is an added / removed object. Option<String>, // The IRI, if change is an added / removed object.
), ),
patches: &mut Vec<OrmDiffOp>, 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 the tracked subject is not valid, we don't create patches for it
if tracked_subject.valid != OrmTrackedSubjectValidity::Valid { if tracked_subject.valid != OrmTrackedSubjectValidity::Valid {
@ -331,54 +436,15 @@ fn build_path_to_root_and_create_patches(
// If the tracked subject is newly valid (was not valid before but is now), // 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 // we need to ensure the object is created with an "add object" patch
if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid { if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid {
// Check if we're at a root subject or need to traverse to parents queue_patches_for_newly_valid_subject(
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape { tracked_subject,
// 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,
tracked_subjects, tracked_subjects,
root_shape, root_shape,
&mut new_path.clone(), path,
(OrmDiffOpType::add, Some(OrmDiffType::object), None, None),
patches, 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 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 { if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
@ -399,16 +465,6 @@ fn build_path_to_root_and_create_patches(
value: diff_op.2.clone(), 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; return;
} }
@ -426,7 +482,7 @@ fn build_path_to_root_and_create_patches(
&mut new_path.clone(), &mut new_path.clone(),
diff_op.clone(), diff_op.clone(),
patches, patches,
paths_of_objects_to_create, objects_to_create,
); );
} }
} }

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

Loading…
Cancel
Save