diff --git a/engine/verifier/src/orm/add_remove_triples.rs b/engine/verifier/src/orm/add_remove_triples.rs index 7adc46b..e7fe05b 100644 --- a/engine/verifier/src/orm/add_remove_triples.rs +++ b/engine/verifier/src/orm/add_remove_triples.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()); diff --git a/engine/verifier/src/orm/handle_backend_update.rs b/engine/verifier/src/orm/handle_backend_update.rs index b8a2b0e..ce35c19 100644 --- a/engine/verifier/src/orm/handle_backend_update.rs +++ b/engine/verifier/src/orm/handle_backend_update.rs @@ -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 = 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, Option)> = + // Keep track of objects to create: (path, Option) + // The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers) + let mut objects_to_create: HashSet<(Vec, Option)> = 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 = 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>>>, + root_shape: &String, + path: &[String], + patches: &mut Vec, + objects_to_create: &mut HashSet<(Vec, Option)>, +) { + // 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, // The IRI, if change is an added / removed object. ), patches: &mut Vec, - paths_of_objects_to_create: &mut HashSet<(Vec, Option)>, + objects_to_create: &mut HashSet<(Vec, Option)>, ) { // If the tracked subject is not valid, we don't create patches for it if tracked_subject.valid != OrmTrackedSubjectValidity::Valid { @@ -331,53 +436,14 @@ 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 = - 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, - root_shape, - &mut new_path.clone(), - (OrmDiffOpType::add, Some(OrmDiffType::object), None, None), - patches, - paths_of_objects_to_create, - ); - } - } - } + queue_patches_for_newly_valid_subject( + tracked_subject, + tracked_subjects, + root_shape, + path, + patches, + objects_to_create, + ); } // If this subject has no parents or its shape matches the root shape, we've reached the root @@ -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, ); } } diff --git a/sdk/rust/src/tests/orm_patches.rs b/sdk/rust/src/tests/orm_patches.rs index 1cb7938..a96367b 100644 --- a/sdk/rust/src/tests/orm_patches.rs +++ b/sdk/rust/src/tests/orm_patches.rs @@ -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 . + 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: INSERT DATA { - # Valid - - ex:knows , ; - ex:name "Alice" . - - ex:knows ; - ex:name "Bob" . - - ex:name "Claire" . - - # Invalid because claire2 is invalid - - ex:knows , ; - ex:name "Alice" . - # Invalid because claire2 is invalid - - ex:knows ; + + a ex:House ; + ex:rootColor "blue" ; + ex:inhabitants , . + + + a ex:Person ; + ex:name "Alice" ; + ex:hasCat . + + + a ex:Person ; ex:name "Bob" . - # Invalid because name is missing. - - ex:missingName "Claire missing" . + + + 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,87 @@ 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: -INSERT DATA { - # Valid - - a ex:Alice ; - ex:knows , . - - a ex:Bob ; - ex:knows . - - a ex:Claire . - - # Invalid because claire is invalid - - a ex:Alice ; - ex:knows , . - # Invalid because claire is invalid - - a ex:Bob ; - ex:knows . - # Invalid, wrong type. - - 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, + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "name".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + } + .into(), + OrmSchemaPredicate { + iri: "http://example.org/hasCat".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()), - }, - ], + readablePredicate: "cat".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + literals: None, + shape: Some("http://example.org/CatShape".to_string()), + }], } .into(), ], } .into(), ); + + // Cat shape schema.insert( - "http://example.org/BobShape".to_string(), + "http://example.org/CatShape".to_string(), OrmSchemaShape { - iri: "http://example.org/BobShape".to_string(), + 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), + 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/Bob".to_string())]), + literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]), shape: None, }], } .into(), OrmSchemaPredicate { - iri: "http://example.org/knows".to_string(), + iri: "http://example.org/catName".to_string(), extra: Some(false), - maxCardinality: -1, - minCardinality: 0, - readablePredicate: "knows".to_string(), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "name".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaLiteralType::string, literals: None, - shape: Some("http://example.org/ClaireShape".to_string()), + shape: None, }], } .into(), @@ -858,32 +779,10 @@ INSERT DATA { } .into(), ); - schema.insert( - "http://example.org/ClaireShape".to_string(), - OrmSchemaShape { - iri: "http://example.org/ClaireShape".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/Claire".to_string(), - )]), - 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(); - log_info!( - "ORM JSON arrived for nested3 (person) test\n: {:?}", - serde_json::to_string(&orm_json).unwrap() - ); - - // 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" - } - } - }, - "urn:test:claire": { - "type": "http://example.org/Claire" - } - } - } - ]); - - let mut actual_mut = orm_json.clone(); - assert_json_eq(&mut expected, &mut actual_mut); - break; } - cancel_fn(); -} -async fn test_orm_nested_4(session_id: u64) { - let doc_nuri = create_doc_with_data( + log_info!( + "\n=== TEST 1: INSERT - Adding new person with cat, modifying existing properties ===\n" + ); + + // 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: +DELETE DATA { + ex:rootColor "blue" . + ex:name "Alice" . +} +; INSERT DATA { - # Valid - + + ex:rootColor "red" ; + ex:inhabitants . + + + ex:name "Alicia" . + + + ex:hasCat . + + a ex:Person ; - ex:hasCat , . - - a ex:Cat . - - a ex:Cat . + ex:name "Charlie" ; + ex:hasCat . + + + a ex:Cat ; + ex:catName "Mittens" . + + + a ex:Cat ; + ex:catName "Fluffy" . } "# .to_string(), + Some(doc_nuri.clone()), ) - .await; + .await + .expect("INSERT SPARQL update failed"); - // 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(), - ], + while let Some(app_response) = receiver.next().await { + let patches = match app_response { + AppResponse::V0(v) => match v { + AppResponseV0::OrmUpdate(json) => Some(json), + _ => None, + }, } - .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()], + .unwrap(); + + log_info!("INSERT patches arrived:\n"); + for patch in patches.iter() { + log_info!("{:?}", patch); } - .into(), - ); - let shape_type = OrmShapeType { - schema, - shape: "http://example.org/PersonShape".to_string(), - }; + 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 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"); + let mut actual = json!(patches); + assert_json_eq(&mut expected, &mut actual); + + break; + } + + 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: +DELETE DATA { + + ex:rootColor "red" ; + ex:inhabitants . + + + ex:hasCat . + + + a ex:Person ; + ex:name "Charlie" ; + ex:hasCat . + + + a ex:Cat ; + ex:catName "Whiskers" . + + + ex:catName "Mittens" . + + + a ex:Cat ; + ex:catName "Fluffy" . +} +; +INSERT DATA { + + ex:catName "Mr. Mittens" . +} +"# + .to_string(), + Some(doc_nuri.clone()), + ) + .await + .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" - }, - "urn:test:kitten2": { - "type": "http://example.org/Cat" - } - }, - } + "op": "remove", + "path": "/urn:test:house1/rootColor", + }, + // 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(); }