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");
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 {
@ -131,9 +131,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 +193,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 +218,7 @@ impl Verifier {
&mut path,
diff_op,
&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
// 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 +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.
/// Returns the updated path if a linking predicate is found.
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.
),
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 +436,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 +465,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 +482,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,
);
}
}

@ -23,7 +23,7 @@ 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;
@ -34,7 +34,10 @@ async fn test_orm_path_creation() {
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