diff --git a/engine/net/src/orm.rs b/engine/net/src/orm.rs index afbd2f2..d0d1fe5 100644 --- a/engine/net/src/orm.rs +++ b/engine/net/src/orm.rs @@ -24,14 +24,14 @@ pub struct OrmShapeType { } /* == Diff Types == */ -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] pub enum OrmDiffOpType { add, remove, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] pub enum OrmDiffType { set, @@ -66,7 +66,7 @@ pub struct OrmSchemaShape { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] -pub enum OrmSchemaLiteralType { +pub enum OrmSchemaValType { number, string, boolean, @@ -85,7 +85,7 @@ pub enum BasicType { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmSchemaDataType { - pub valType: OrmSchemaLiteralType, + pub valType: OrmSchemaValType, pub literals: Option>, pub shape: Option, } @@ -100,13 +100,23 @@ pub struct OrmSchemaPredicate { pub minCardinality: i32, pub extra: Option, } +impl OrmSchemaPredicate { + pub fn is_multi(&self) -> bool { + self.maxCardinality > 1 || self.maxCardinality == -1 + } + pub fn is_object(&self) -> bool { + self.dataTypes + .iter() + .any(|dt| dt.valType == OrmSchemaValType::shape) + } +} impl Default for OrmSchemaDataType { fn default() -> Self { Self { literals: None, shape: None, - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, } } } diff --git a/engine/verifier/src/orm/add_remove_triples.rs b/engine/verifier/src/orm/add_remove_triples.rs index 7adc46b..4823816 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()) @@ -109,7 +109,7 @@ pub fn add_remove_triples( .schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::literal) + .any(|dt| dt.valType == OrmSchemaValType::literal) { match &mut tracked_predicate.current_literals { Some(lits) => lits.push(obj_term.clone()), @@ -122,7 +122,7 @@ pub fn add_remove_triples( // If predicate is of type shape, register // "parent (predicate) -> child subject" and `child_subject.parents`. for shape_iri in predicate_schema.dataTypes.iter().filter_map(|dt| { - if dt.valType == OrmSchemaLiteralType::shape { + if dt.valType == OrmSchemaValType::shape { dt.shape.clone() } else { None @@ -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()); @@ -193,7 +200,7 @@ pub fn add_remove_triples( .schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::literal) + .any(|dt| dt.valType == OrmSchemaValType::literal) { if let Some(current_literals) = &mut tracked_predicate.current_literals { // Remove obj_val from current_literals in-place @@ -205,7 +212,7 @@ pub fn add_remove_triples( .schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::shape) + .any(|dt| dt.valType == OrmSchemaValType::shape) { // Remove parent from child and child from tracked children. // If predicate is of type shape, register (parent -> child) links so that @@ -215,7 +222,7 @@ pub fn add_remove_triples( .dataTypes .iter() .filter_map(|dt| { - if dt.valType == OrmSchemaLiteralType::shape { + if dt.valType == OrmSchemaValType::shape { dt.shape.clone() } else { None diff --git a/engine/verifier/src/orm/handle_backend_update.rs b/engine/verifier/src/orm/handle_backend_update.rs index b8a2b0e..1ac9b9f 100644 --- a/engine/verifier/src/orm/handle_backend_update.rs +++ b/engine/verifier/src/orm/handle_backend_update.rs @@ -68,8 +68,6 @@ impl Verifier { }) .collect(); - // let mut updates = Vec::new(); - let mut scopes = vec![]; for (scope, subs) in self.orm_subscriptions.iter_mut() { // Remove old subscriptions @@ -86,32 +84,37 @@ impl Verifier { } // prepare to apply updates to tracked subjects and record the changes. - let root_shapes = subs + let root_shapes_and_tracked_subjects = subs .iter() .map(|sub| { - sub.shape_type - .schema - .get(&sub.shape_type.shape) - .unwrap() - .clone() + ( + sub.shape_type + .schema + .get(&sub.shape_type.shape) + .unwrap() + .clone(), + shapes_in_tracked_subjects(&sub.tracked_subjects), + ) }) .collect::>(); - scopes.push((scope.clone(), root_shapes)); + scopes.push((scope.clone(), root_shapes_and_tracked_subjects)); } 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 { + for (scope, shapes_zip) in scopes { let mut orm_changes: OrmChanges = HashMap::new(); // Apply the changes to tracked subjects. - for shape_arc in shapes { + for (root_shape_arc, all_shapes) in shapes_zip { + let shape_iri = root_shape_arc.iri.clone(); let _ = self.process_changes_for_shape_and_session( &scope, - shape_arc, + &shape_iri, + all_shapes, session_id, &triple_inserts, &triple_removes, @@ -131,9 +134,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: ...} @@ -157,6 +160,11 @@ impl Verifier { // Iterate over all changes and create patches for (shape_iri, subject_changes) in &orm_changes { for (subject_iri, change) in subject_changes { + log_debug!( + "Patch creating for subject change {}. #changed preds: {}", + subject_iri, + change.predicates.len() + ); // Get the tracked subject for this (subject, shape) pair let tracked_subject = sub .tracked_subjects @@ -193,12 +201,19 @@ 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. // Process each predicate change for (_pred_iri, pred_change) in &change.predicates { + log_debug!( + " - Predicate changes: {}; #Adds: {}; #Removes {}", + _pred_iri, + pred_change.values_added.len(), + pred_change.values_removed.len() + ); + let tracked_predicate = pred_change.tracked_predicate.read().unwrap(); let pred_name = tracked_predicate.schema.readablePredicate.clone(); @@ -218,7 +233,7 @@ impl Verifier { &mut path, diff_op, &mut patches, - &mut paths_of_objects_to_create, + &mut objects_to_create, ); } } @@ -229,26 +244,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 +284,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() > 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,65 +441,18 @@ 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)>, ) { + log_debug!( + " - build path, ts: {}, path {:?}", + tracked_subject.subject_iri, + path + ); // If the tracked subject is not valid, we don't create patches for it if tracked_subject.valid != OrmTrackedSubjectValidity::Valid { return; } - // 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, - ); - } - } - } - } - // 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 { // Build the final JSON Pointer path @@ -391,7 +464,7 @@ fn build_path_to_root_and_create_patches( escaped_path.join("/") ); - // Create the patch + // Create the patch for the actual value change patches.push(OrmDiffOp { op: diff_op.0.clone(), valType: diff_op.1.clone(), @@ -399,15 +472,19 @@ 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(), - // }); - // } + // If the subject is newly valid, now we have the full path to queue its creation. + if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid { + let mut final_path = vec![tracked_subject.subject_iri.clone()]; + final_path.extend_from_slice(path); + queue_patches_for_newly_valid_subject( + tracked_subject, + tracked_subjects, + root_shape, + &final_path, + patches, + objects_to_create, + ); + } return; } @@ -417,16 +494,17 @@ fn build_path_to_root_and_create_patches( let parent_ts = parent_tracked_subject.read().unwrap(); // Build the path segment for this parent - if let Some(new_path) = build_path_segment_for_parent(tracked_subject, &parent_ts, path) { + if let Some(mut new_path) = build_path_segment_for_parent(tracked_subject, &parent_ts, path) + { // Recurse to the parent build_path_to_root_and_create_patches( &parent_ts, tracked_subjects, root_shape, - &mut new_path.clone(), + &mut new_path, diff_op.clone(), patches, - paths_of_objects_to_create, + objects_to_create, ); } } @@ -526,3 +604,15 @@ fn create_diff_ops_from_predicate_change( // } return ops; } + +fn shapes_in_tracked_subjects( + tracked_subjects: &HashMap>>>, +) -> Vec> { + let mut shapes = vec![]; + for (_subject_iri, tss) in tracked_subjects.iter() { + for (_shape_iri, ts) in tss.iter() { + shapes.push(ts.read().unwrap().shape.clone()); + } + } + shapes +} diff --git a/engine/verifier/src/orm/handle_frontend_update.rs b/engine/verifier/src/orm/handle_frontend_update.rs index 4d49be7..fac4388 100644 --- a/engine/verifier/src/orm/handle_frontend_update.rs +++ b/engine/verifier/src/orm/handle_frontend_update.rs @@ -7,9 +7,11 @@ // notice may not be copied, modified, or distributed except // according to those terms. +use ng_net::orm::{OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaPredicate, OrmSchemaShape}; use ng_oxigraph::oxrdf::Quad; use ng_repo::errors::VerifierError; +use std::sync::{Arc, RwLock}; use std::u64; use futures::SinkExt; @@ -18,6 +20,7 @@ pub use ng_net::orm::{OrmDiff, OrmShapeType}; use ng_repo::log::*; use crate::orm::types::*; +use crate::orm::utils::{decode_json_pointer, json_to_sparql_val}; use crate::verifier::*; impl Verifier { @@ -30,11 +33,11 @@ impl Verifier { scope: &NuriV0, shape_iri: ShapeIri, session_id: u64, - skolemnized_blank_nodes: Vec, - revert_inserts: Vec, - revert_removes: Vec, + _skolemnized_blank_nodes: Vec, + _revert_inserts: Vec, + _revert_removes: Vec, ) -> Result<(), VerifierError> { - let (mut sender, orm_subscription) = + let (mut sender, _orm_subscription) = self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?; // TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes @@ -63,9 +66,8 @@ impl Verifier { diff: OrmDiff, ) -> Result<(), String> { log_info!( - "frontend_update_orm session={} scope={:?} shape={} diff={:?}", + "frontend_update_orm session={} shape={} diff={:?}", session_id, - scope, shape_iri, diff ); @@ -73,14 +75,14 @@ impl Verifier { let (doc_nuri, sparql_update) = { let orm_subscription = self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id)); + let doc_nuri = orm_subscription.nuri.clone(); + + let sparql_update = create_sparql_update_query_for_diff(orm_subscription, diff); - // use orm_subscription as needed - // do the magic, then, find the doc where the query should start and generate the sparql update - let doc_nuri = NuriV0::new_empty(); - let sparql_update: String = String::new(); (doc_nuri, sparql_update) }; + log_debug!("Created SPARQL query for patches:\n{}", sparql_update); match self .process_sparql_update( &doc_nuri, @@ -113,3 +115,315 @@ impl Verifier { } } } + +fn create_sparql_update_query_for_diff( + orm_subscription: &OrmSubscription, + diff: OrmDiff, +) -> String { + // First sort patches. + // - Process delete patches first. + // - Process object creation add operations before rest, to ensure potential blank nodes are created. + let delete_patches: Vec<_> = diff + .iter() + .filter(|patch| patch.op == OrmDiffOpType::remove) + .collect(); + let add_object_patches: Vec<_> = diff + .iter() + .filter(|patch| { + patch.op == OrmDiffOpType::add + && match &patch.valType { + Some(vt) => *vt == OrmDiffType::object, + _ => false, + } + }) + .collect(); + let add_literal_patches: Vec<_> = diff + .iter() + .filter(|patch| { + patch.op == OrmDiffOpType::add + && match &patch.valType { + Some(vt) => *vt != OrmDiffType::object, + _ => true, + } + }) + .collect(); + + // For each diff op, we create a separate INSERT or DELETE block. + let mut sparql_sub_queries: Vec = vec![]; + + // Create delete statements. + // + for del_patch in delete_patches.iter() { + let mut var_counter: i32 = 0; + + let (where_statements, target, _pred_schema) = + create_where_statements_for_patch(&del_patch, &mut var_counter, &orm_subscription); + let (subject_var, target_predicate, target_object) = target; + + let delete_statement; + if let Some(target_object) = target_object { + // Delete the link to exactly one object (IRI referenced in path, i.e. target_object) + delete_statement = format!( + " {} <{}> <{}> .", + subject_var, target_predicate, target_object + ) + } else { + // Delete object or literal referenced by property name. + let delete_val = match &del_patch.value { + // No value specified, that means we are deleting all values for the given subject and predicate (multi-value scenario). + None => { + format!("?{}", var_counter) + // Note: var_counter is not incremented here as it's only used locally + } + // Delete the specific values only. + Some(val) => json_to_sparql_val(&val), // Can be one or more (joined with ", "). + }; + delete_statement = format!(" {} <{}> {} .", subject_var, target_predicate, delete_val); + } + + sparql_sub_queries.push(format!( + "DELETE {{\n{}\n}}\nWHERE\n{{\n {}\n}}", + delete_statement, + where_statements.join(" .\n ") + )); + } + + // Process add object patches (might need blank nodes) + // + for _add_obj_patch in add_object_patches { + // Creating objects without an id field is only supported in one circumstance: + // An object is added to a property which has a max cardinality of one, e.g. `painting.artist`. + // In that case, we create a blank node. + // TODO: We need to set up a list of created blank nodes and where they belong to. + } + + // Process literal add patches + // + for add_patch in add_literal_patches { + let mut var_counter: i32 = 0; + + // Create WHERE statements from path. + let (where_statements, target, pred_schema) = + create_where_statements_for_patch(&add_patch, &mut var_counter, &orm_subscription); + let (subject_var, target_predicate, target_object) = target; + + if let Some(_target_object) = target_object { + // Reference to exactly one object found. This is invalid when inserting literals. + // TODO: Return error? + continue; + } else { + // Add value(s) to + let add_val = match &add_patch.value { + // Delete the specific values only. + Some(val) => json_to_sparql_val(&val), // Can be one or more (joined with ", "). + None => { + // A value must be set. This patch is invalid. + // TODO: Return error? + continue; + } + }; + + // Add SPARQL statement. + + // If the schema only has max one value, + // then `add` can also overwrite values, so we need to delete the previous one + if !pred_schema.unwrap().is_multi() { + let remove_statement = + format!(" {} <{}> ?o{}", subject_var, target_predicate, var_counter); + + let mut wheres = where_statements.clone(); + wheres.push(remove_statement.clone()); + + sparql_sub_queries.push(format!( + "DELETE {{\n{}\n}} WHERE {{\n {}\n}}", + remove_statement, + wheres.join(" .\n ") + )); + // var_counter += 1; // Not necessary because not used afterwards. + } + // The actual INSERT. + let add_statement = format!(" {} <{}> {} .", subject_var, target_predicate, add_val); + sparql_sub_queries.push(format!( + "INSERT {{\n{}\n}} WHERE {{\n {}\n}}", + add_statement, + where_statements.join(". \n ") + )); + } + } + + return sparql_sub_queries.join(";\n"); +} + +fn _get_tracked_subject_from_diff_op( + subject_iri: &String, + orm_subscription: &OrmSubscription, +) -> Arc> { + let tracked_subject = orm_subscription + .tracked_subjects + .get(subject_iri) + .unwrap() + .get(&orm_subscription.shape_type.shape) + .unwrap(); + + return tracked_subject.clone(); +} + +/// Removes the current predicate from the path stack and returns the corresponding IRI. +/// If the +fn find_pred_schema_by_name( + readable_predicate: &String, + subject_schema: &OrmSchemaShape, +) -> Arc { + // Find predicate by readable name in subject schema. + for pred_schema in subject_schema.predicates.iter() { + if pred_schema.readablePredicate == *readable_predicate { + return pred_schema.clone(); + } + } + panic!("No predicate found in schema for name"); +} + +/// Creates sparql WHERE statements to navigate to the JSON pointer path in our ORM mapping. +/// Returns tuple of +/// - The WHERE statements as Vec +/// - The Option subject, predicate, Option of the path's ending (to be used for DELETE) +/// - The Option predicate schema of the tail of the target property. +fn create_where_statements_for_patch( + patch: &OrmDiffOp, + var_counter: &mut i32, + orm_subscription: &OrmSubscription, +) -> ( + Vec, + (String, String, Option), + Option>, +) { + let mut body_statements: Vec = vec![]; + let mut where_statements: Vec = vec![]; + + let mut path: Vec = patch + .path + .split("/") + .map(|s| decode_json_pointer(&s.to_string())) + .collect(); + + // Handle special case: The whole object is deleted. + if path.len() == 1 { + let root_iri = &path[0]; + body_statements.push(format!("<{}> ?p ?o", root_iri)); + where_statements.push(format!("<{}> ?p ?o", root_iri)); + return ( + where_statements, + (format!("<{}>", root_iri), "?p".to_string(), None), + None, + ); + } + + let subj_schema: &Arc = orm_subscription + .shape_type + .schema + .get(&orm_subscription.shape_type.shape) + .unwrap(); + + let mut current_subj_schema: Arc = subj_schema.clone(); + + // The root IRI might change, if the parent path segment was an IRI. + let root_iri = path.remove(0); + let mut subject_ref = format!("<{}>", root_iri); + + while path.len() > 0 { + let pred_name = path.remove(0); + let pred_schema = find_pred_schema_by_name(&pred_name, ¤t_subj_schema); + + if path.len() == 0 { + return ( + where_statements, + (subject_ref, pred_schema.iri.clone(), None), + Some(pred_schema), + ); + } + + where_statements.push(format!( + "{} <{}> ?o{}", + subject_ref, pred_schema.iri, var_counter, + )); + + // Update the subject_ref for traversal (e.g. ?o1 . ?o1 Cat); + subject_ref = format!("?o{}", var_counter); + *var_counter = *var_counter + 1; + + if !pred_schema.is_object() { + panic!( + "Predicate schema is not of type shape. Schema: {}, subject_ref: {}", + pred_schema.iri, subject_ref + ); + } + if pred_schema.is_multi() { + let object_iri = path.remove(0); + // Path ends on an object IRI, which we return here as well. + if path.len() == 0 { + return ( + where_statements, + (subject_ref, pred_schema.iri.clone(), Some(object_iri)), + Some(pred_schema), + ); + } + + current_subj_schema = + get_first_valid_child_schema(&object_iri, &pred_schema, &orm_subscription); + + // Since we have new IRI that we can use as root, we replace the current one with it. + subject_ref = format!("<{object_iri}>"); + // And can clear all, now unnecessary where statements. + where_statements.clear(); + } else { + // Set to child subject schema. + // TODO: Actually, we should get the tracked subject and check for the correct shape there. + // As long as there is only one allowed shape or the first one is valid, this is fine. + current_subj_schema = get_first_child_schema(&pred_schema, &orm_subscription); + } + } + // Can't happen. + panic!(); +} + +fn get_first_valid_child_schema( + subject_iri: &String, + pred_schema: &OrmSchemaPredicate, + orm_subscription: &OrmSubscription, +) -> Arc { + for data_type in pred_schema.dataTypes.iter() { + let Some(schema_shape) = data_type.shape.as_ref() else { + continue; + }; + + let tracked_subject = orm_subscription + .tracked_subjects + .get(subject_iri) + .unwrap() + .get(schema_shape) + .unwrap(); + + if tracked_subject.read().unwrap().valid == OrmTrackedSubjectValidity::Valid { + return orm_subscription + .shape_type + .schema + .get(schema_shape) + .unwrap() + .clone(); + } + } + // TODO: Panicking might be too aggressive. + panic!("No valid child schema found."); +} + +fn get_first_child_schema( + pred_schema: &OrmSchemaPredicate, + orm_subscription: &OrmSubscription, +) -> Arc { + return orm_subscription + .shape_type + .schema + .get(pred_schema.dataTypes[0].shape.as_ref().unwrap()) + .unwrap() + .clone(); +} diff --git a/engine/verifier/src/orm/materialize.rs b/engine/verifier/src/orm/materialize.rs index 00efe18..69d14da 100644 --- a/engine/verifier/src/orm/materialize.rs +++ b/engine/verifier/src/orm/materialize.rs @@ -103,16 +103,13 @@ impl Verifier { let mut return_vals: Value = Value::Array(vec![]); let return_val_vec = return_vals.as_array_mut().unwrap(); - // log_debug!( - // "Tracked subjects:\n{:?}\n", - // orm_subscription.tracked_subjects, - // ); + log_debug!("\nMaterializing: {}", shape_type.shape); // For each valid change struct, we build an orm object. // The way we get the changes from the tracked subjects is a bit hacky, sorry. for (subject_iri, tracked_subjects_by_shape) in &orm_subscription.tracked_subjects { if let Some(tracked_subject) = tracked_subjects_by_shape.get(&shape_type.shape) { let ts = tracked_subject.read().unwrap(); - log_info!("changes for: {:?} valid: {:?}\n", ts.subject_iri, ts.valid); + log_info!(" - changes for: {:?} valid: {:?}", ts.subject_iri, ts.valid); if ts.valid == OrmTrackedSubjectValidity::Valid { if let Some(change) = changes @@ -164,7 +161,7 @@ pub(crate) fn materialize_orm_object( if pred_schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::shape) + .any(|dt| dt.valType == OrmSchemaValType::shape) { // We have a nested type. diff --git a/engine/verifier/src/orm/process_changes.rs b/engine/verifier/src/orm/process_changes.rs index b3f698f..aa36b8b 100644 --- a/engine/verifier/src/orm/process_changes.rs +++ b/engine/verifier/src/orm/process_changes.rs @@ -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 { - 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, + root_shape_iri: &String, + shapes: Vec>, session_id: u64, triples_added: &[Triple], triples_removed: &[Triple], @@ -140,8 +101,9 @@ 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![])); + for shape in shapes { + 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 +118,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> = HashMap::new(); @@ -175,6 +129,7 @@ impl Verifier { shape.iri ); + // For each modified subject, apply changes to tracked subjects and validate. for subject_iri in &modified_subject_iris { let validation_key = (shape.iri.clone(), subject_iri.to_string()); @@ -185,8 +140,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 +186,74 @@ 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 { - log_debug!("not applying triples again for subject {subject_iri}"); + } + + // 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); + // need_eval contains elements in reverse priority (last element to be validated first) + // TODO: Improve order by distinguishing between parents, children and self to be re-evaluated. + 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 +272,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( - nuri, - Some(&root_shape_iri), - Some(&session_id), - ); - // Extract schema and shape Arc before mutable borrow - let schema = orm_subscription.shape_type.schema.clone(); + // 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), + ); + 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,7 +300,8 @@ impl Verifier { // Recursively process nested objects. self.process_changes_for_shape_and_session( nuri, - shape_arc.clone(), + &root_shape_iri, + [shape_arc.clone()].to_vec(), session_id, &new_triples, &vec![], @@ -323,23 +330,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 { + 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].to_vec(), + session_id, + triples_added, + triples_removed, + &mut orm_changes, + data_already_fetched, + )?; + } + + Ok(orm_changes) } pub fn get_first_orm_subscription_for( diff --git a/engine/verifier/src/orm/query.rs b/engine/verifier/src/orm/query.rs index 46aec7c..70d893d 100644 --- a/engine/verifier/src/orm/query.rs +++ b/engine/verifier/src/orm/query.rs @@ -7,15 +7,14 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use lazy_static::lazy_static; use ng_repo::errors::VerifierError; -use regex::Regex; use std::collections::HashSet; pub use ng_net::orm::{OrmDiff, OrmShapeType}; use crate::orm::types::*; +use crate::orm::utils::{escape_literal, is_iri}; use crate::verifier::*; use ng_net::orm::*; use ng_oxigraph::oxigraph::sparql::{Query, QueryResults}; @@ -67,15 +66,6 @@ impl Verifier { } } -/// Heuristic: -/// Consider a string an IRI if it contains alphanumeric characters and then a colon within the first 13 characters -pub fn is_iri(s: &str) -> bool { - lazy_static! { - static ref IRI_REGEX: Regex = Regex::new(r"^[A-Za-z][A-Za-z0-9+\.\-]{1,12}:").unwrap(); - } - IRI_REGEX.is_match(s) -} - pub fn literal_to_sparql_str(var: OrmSchemaDataType) -> Vec { match var.literals { None => [].to_vec(), @@ -155,7 +145,7 @@ pub fn shape_type_to_sparql( // Predicate constraints might have more than one acceptable nested shape. Traverse each. for datatype in &predicate.dataTypes { - if datatype.valType == OrmSchemaLiteralType::shape { + if datatype.valType == OrmSchemaValType::shape { let shape_iri = &datatype.shape.clone().unwrap(); let nested_shape = schema.get(shape_iri).unwrap(); @@ -305,18 +295,3 @@ pub fn shape_type_to_sparql( construct_body, where_body )) } -/// SPARQL literal escape: backslash, quotes, newlines, tabs. -fn escape_literal(lit: &str) -> String { - let mut out = String::with_capacity(lit.len() + 4); - for c in lit.chars() { - match c { - '\\' => out.push_str("\\\\"), - '\"' => out.push_str("\\\""), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - _ => out.push(c), - } - } - return out; -} diff --git a/engine/verifier/src/orm/shape_validation.rs b/engine/verifier/src/orm/shape_validation.rs index 041cf58..6465f00 100644 --- a/engine/verifier/src/orm/shape_validation.rs +++ b/engine/verifier/src/orm/shape_validation.rs @@ -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() { @@ -152,7 +154,7 @@ impl Verifier { } else if p_schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::literal) + .any(|dt| dt.valType == OrmSchemaValType::literal) { // If we have literals, check if all required literals are present. // At least one datatype must match. @@ -196,7 +198,7 @@ impl Verifier { } else if p_schema .dataTypes .iter() - .any(|dt| dt.valType == OrmSchemaLiteralType::shape) + .any(|dt| dt.valType == OrmSchemaValType::shape) { // If we have a nested shape, we need to check if the nested objects are tracked and valid. let tracked_children = tracked_pred.as_ref().map(|tp| { @@ -307,19 +309,19 @@ impl Verifier { // Check 3.5) Data types correct. } else { // Check if the data type is correct. - let allowed_types: Vec<&OrmSchemaLiteralType> = + let allowed_types: Vec<&OrmSchemaValType> = p_schema.dataTypes.iter().map(|dt| &dt.valType).collect(); // For each new value, check that data type is in allowed_types. for val_added in p_change.iter().map(|pc| &pc.values_added).flatten() { let matches = match val_added { BasicType::Bool(_) => allowed_types .iter() - .any(|t| **t == OrmSchemaLiteralType::boolean), + .any(|t| **t == OrmSchemaValType::boolean), BasicType::Num(_) => allowed_types .iter() - .any(|t| **t == OrmSchemaLiteralType::number), + .any(|t| **t == OrmSchemaValType::number), BasicType::Str(_) => allowed_types.iter().any(|t| { - **t == OrmSchemaLiteralType::string || **t == OrmSchemaLiteralType::iri + **t == OrmSchemaValType::string || **t == OrmSchemaValType::iri }), }; if !matches { @@ -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 { diff --git a/engine/verifier/src/orm/utils.rs b/engine/verifier/src/orm/utils.rs index 80b804d..f5e1ce8 100644 --- a/engine/verifier/src/orm/utils.rs +++ b/engine/verifier/src/orm/utils.rs @@ -13,6 +13,9 @@ use ng_repo::types::OverlayId; use std::collections::HashMap; use std::collections::HashSet; +use lazy_static::lazy_static; +use regex::Regex; + pub use ng_net::orm::{OrmDiff, OrmShapeType}; use ng_net::{app_protocol::*, orm::*}; use ng_oxigraph::oxrdf::Triple; @@ -65,6 +68,51 @@ pub fn escape_json_pointer(path_segment: &String) -> String { path_segment.replace("~", "~0").replace("/", "~1") } -pub fn decode_join_pointer(path: &String) -> String { +pub fn decode_json_pointer(path: &String) -> String { path.replace("~1", "/").replace("~0", "~") } + +/// SPARQL literal escape: backslash, quotes, newlines, tabs. +pub fn escape_literal(lit: &str) -> String { + let mut out = String::with_capacity(lit.len() + 4); + for c in lit.chars() { + match c { + '\\' => out.push_str("\\\\"), + '\"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(c), + } + } + return out; +} + +pub fn json_to_sparql_val(json: &serde_json::Value) -> String { + match json { + serde_json::Value::Array(arr) => arr + .iter() + .map(|val| json_to_sparql_val(val)) + .collect::>() + .join(", "), + serde_json::Value::Bool(bool) => match bool { + true => "true".to_string(), + false => "false".to_string(), + }, + serde_json::Value::Number(num) => num.to_string(), + serde_json::Value::String(str) => match is_iri(str) { + true => format!("<{}>", str), + false => format!("\"{}\"", str), + }, + _ => panic!(), + } +} + +/// Heuristic: +/// Consider a string an IRI if it contains alphanumeric characters and then a colon within the first 13 characters +pub fn is_iri(s: &str) -> bool { + lazy_static! { + static ref IRI_REGEX: Regex = Regex::new(r"^[A-Za-z][A-Za-z0-9+\.\-]{1,12}:").unwrap(); + } + IRI_REGEX.is_match(s) +} diff --git a/sdk/rust/src/local_broker.rs b/sdk/rust/src/local_broker.rs index d43a849..f1d2877 100644 --- a/sdk/rust/src/local_broker.rs +++ b/sdk/rust/src/local_broker.rs @@ -17,7 +17,7 @@ use async_std::sync::{Arc, Condvar, Mutex, RwLock}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use lazy_static::lazy_static; -use ng_net::orm::OrmShapeType; +use ng_net::orm::{OrmDiff, OrmShapeType}; use ng_oxigraph::oxrdf::Triple; use once_cell::sync::Lazy; use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str}; @@ -2764,6 +2764,18 @@ pub async fn orm_start( app_request_stream(request).await } +pub async fn orm_update( + scope: NuriV0, + shape_type_name: String, + diff: OrmDiff, + session_id: u64, +) -> Result<(), NgError> { + let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff); + request.set_session_id(session_id); + app_request(request).await?; + Ok(()) +} + pub async fn doc_sparql_construct( session_id: u64, sparql: String, diff --git a/sdk/rust/src/tests/mod.rs b/sdk/rust/src/tests/mod.rs index caac2a7..a389234 100644 --- a/sdk/rust/src/tests/mod.rs +++ b/sdk/rust/src/tests/mod.rs @@ -15,8 +15,9 @@ use crate::local_broker::{doc_create, doc_sparql_update}; #[doc(hidden)] pub mod orm_creation; +pub mod orm_apply_patches; #[doc(hidden)] -pub mod orm_patches; +pub mod orm_create_patches; #[doc(hidden)] pub mod create_or_open_wallet; @@ -51,7 +52,7 @@ pub(crate) fn assert_json_eq(expected: &mut Value, actual: &mut Value) { let diff = serde_json_diff::values(expected.clone(), actual.clone()); if let Some(diff_) = diff { log_err!( - "Expected and actual ORM JSON mismatch.\nDiff: {:?}\nExpected: {}\nActual: {}", + "Expected and actual JSON mismatch.\nDiff: {:?}\nExpected: {}\nActual: {}", diff_, expected, actual diff --git a/sdk/rust/src/tests/orm_apply_patches.rs b/sdk/rust/src/tests/orm_apply_patches.rs new file mode 100644 index 0000000..1392355 --- /dev/null +++ b/sdk/rust/src/tests/orm_apply_patches.rs @@ -0,0 +1,959 @@ +// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers +// All rights reserved. +// Licensed under the Apache License, Version 2.0 +// +// or the MIT license , +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +use crate::local_broker::{doc_sparql_construct, orm_start, orm_update}; +use crate::tests::create_doc_with_data; +use crate::tests::create_or_open_wallet::create_or_open_wallet; +use async_std::stream::StreamExt; +use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; +use ng_net::orm::{ + BasicType, OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaDataType, OrmSchemaPredicate, + OrmSchemaShape, OrmSchemaValType, OrmShapeType, +}; + +use ng_repo::log_info; +use serde_json::json; +use std::collections::HashMap; +use std::sync::Arc; + +#[async_std::test] +async fn test_orm_apply_patches() { + // 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 1: Add single literal value + test_patch_add_single_literal(session_id).await; + + // Test 2: Remove single literal value + test_patch_remove_single_literal(session_id).await; + + // Test 3: Replace single literal value + test_patch_replace_single_literal(session_id).await; + + // Test 4: Add to multi-value literal array + test_patch_add_to_array(session_id).await; + + // Test 5: Remove from multi-value literal array + test_patch_remove_from_array(session_id).await; + + // // Test 6: Nested object - modify nested literal + test_patch_nested_literal(session_id).await; + + // Test 7: Multi-level nesting + test_patch_multilevel_nested(session_id).await; +} + +/// Test adding a single literal value via ORM patch +async fn test_patch_add_single_literal(session_id: u64) { + log_info!("\n\n=== TEST: Add Single Literal ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person . +} +"# + .to_string(), + ) + .await; + + // Define the ORM schema + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + extra: Some(false), + iri: "http://example.org/name".to_string(), + readablePredicate: "name".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state (person without name) + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Add name + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::add, + path: "urn:test:person1/name".to_string(), + valType: None, + value: Some(json!("Alice")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_name = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/name" && t.object.to_string().contains("Alice") + }); + assert!(has_name, "Name was not added to the graph"); + + log_info!("✓ Test passed: Add single literal"); +} + +/// Test removing a single literal value via ORM patch +async fn test_patch_remove_single_literal(session_id: u64) { + log_info!("\n\n=== TEST: Remove Single Literal ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Bob" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + extra: Some(false), + readablePredicate: "name".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state (person without name) + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Remove name + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::remove, + path: "urn:test:person2/name".to_string(), + valType: None, + value: Some(json!("Bob")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_name = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/name" && t.object.to_string().contains("Bob") + }); + assert!(!has_name, "Name was not removed from the graph"); + + log_info!("✓ Test passed: Remove single literal"); +} + +/// Test replacing a single literal value via ORM patch (remove + add) +async fn test_patch_replace_single_literal(session_id: u64) { + log_info!("\n\n=== TEST: Replace Single Literal ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Charlie" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + extra: Some(false), + readablePredicate: "name".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state (person without name) + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Replace name (remove old, add new) + let diff = vec![ + // OrmDiffOp { + // op: OrmDiffOpType::remove, + // path: "urn:test:person3/name".to_string(), + // valType: None, + // value: Some(json!("Charlie")), + // }, + OrmDiffOp { + op: OrmDiffOpType::add, + path: "urn:test:person3/name".to_string(), + valType: None, + value: Some(json!("Charles")), + }, + ]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_old_name = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/name" + && t.object.to_string().contains("Charlie") + }); + let has_new_name = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/name" + && t.object.to_string().contains("Charles") + }); + + assert!(!has_old_name, "Old name was not removed"); + assert!(has_new_name, "New name was not added"); + + log_info!("✓ Test passed: Replace single literal"); +} + +/// Test adding to a multi-value array via ORM patch +async fn test_patch_add_to_array(session_id: u64) { + log_info!("\n\n=== TEST: Add to Array ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:hobby "Reading" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/hobby".to_string(), + extra: Some(false), + readablePredicate: "hobby".to_string(), + minCardinality: 0, + maxCardinality: -1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state (person without name) + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Add hobby + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::add, + valType: Some(OrmDiffType::set), + path: "urn:test:person4/hobby".to_string(), + value: Some(json!("Swimming")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let hobby_count = triples + .iter() + .filter(|t| t.predicate.as_str() == "http://example.org/hobby") + .count(); + + assert_eq!(hobby_count, 2, "Should have 2 hobbies"); + + log_info!("✓ Test passed: Add to array"); +} + +/// Test removing from a multi-value array via ORM patch +async fn test_patch_remove_from_array(session_id: u64) { + log_info!("\n\n=== TEST: Remove from Array ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:hobby "Reading", "Swimming", "Cooking" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/hobby".to_string(), + readablePredicate: "hobby".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: -1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Remove hobby + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::remove, + path: "urn:test:person5/hobby".to_string(), + valType: None, + value: Some(json!("Swimming")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let hobby_count = triples + .iter() + .filter(|t| t.predicate.as_str() == "http://example.org/hobby") + .count(); + let has_swimming = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/hobby" + && t.object.to_string().contains("Swimming") + }); + + assert_eq!(hobby_count, 2, "Should have 2 hobbies left"); + assert!(!has_swimming, "Swimming should be removed"); + + log_info!("✓ Test passed: Remove from array"); +} + +/// Test modifying a nested object's literal via ORM patch +async fn test_patch_nested_literal(session_id: u64) { + log_info!("\n\n=== TEST: Nested Literal Modification ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Dave" ; + ex:address . + + a ex:Address ; + ex:street "Main St" ; + ex:city "Springfield" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + readablePredicate: "name".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/address".to_string(), + readablePredicate: "address".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::shape, + shape: Some("http://example.org/Address".to_string()), + literals: None, + }], + }), + ], + }), + ); + schema.insert( + "http://example.org/Address".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Address".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Address".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/street".to_string(), + extra: Some(false), + readablePredicate: "street".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/city".to_string(), + readablePredicate: "city".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Change city in nested address + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::add, + path: "urn:test:person6/address/city".to_string(), + valType: None, + value: Some(json!("Shelbyville")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_old_city = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/city" + && t.object.to_string().contains("Springfield") + }); + let has_new_city = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/city" + && t.object.to_string().contains("Shelbyville") + }); + + assert!(!has_old_city, "Old city should be removed"); + assert!(has_new_city, "New city should be added"); + + log_info!("✓ Test passed: Nested literal modification"); +} + +/// Test multi-level nested object modifications via ORM patch +async fn test_patch_multilevel_nested(session_id: u64) { + log_info!("\n\n=== TEST: Multi-level Nested Modification ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Eve" ; + ex:company . + + a ex:Company ; + ex:companyName "Acme Corp" ; + ex:headquarter . + + a ex:Address ; + ex:street "Business Blvd" ; + ex:city "Metropolis" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + extra: Some(false), + readablePredicate: "name".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/company".to_string(), + extra: Some(false), + readablePredicate: "company".to_string(), + minCardinality: 0, + maxCardinality: -1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::shape, + shape: Some("http://example.org/Company".to_string()), + literals: None, + }], + }), + ], + }), + ); + schema.insert( + "http://example.org/Company".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Company".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Company".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/companyName".to_string(), + readablePredicate: "companyName".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/headquarter".to_string(), + readablePredicate: "headquarter".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::shape, + shape: Some("http://example.org/Address".to_string()), + literals: None, + }], + }), + ], + }), + ); + schema.insert( + "http://example.org/Address".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Address".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Address".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/street".to_string(), + readablePredicate: "street".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/city".to_string(), + readablePredicate: "city".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Change street in company's headquarter address (3 levels deep) + let diff = vec![OrmDiffOp { + op: OrmDiffOpType::add, + path: "urn:test:person7/company/urn:test:company1/headquarter/street".to_string(), + valType: None, + value: Some(json!("Rich Street")), + }]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_old_street = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/street" + && t.object.to_string().contains("Business Blvd") + }); + let has_new_street = triples.iter().any(|t| { + t.predicate.as_str() == "http://example.org/street" + && t.object.to_string().contains("Rich Street") + }); + + assert!(!has_old_street, "Old street should be removed"); + assert!(has_new_street, "New street should be added"); + + log_info!("✓ Test passed: Multi-level nested modification"); +} diff --git a/sdk/rust/src/tests/orm_patches.rs b/sdk/rust/src/tests/orm_create_patches.rs similarity index 67% rename from sdk/rust/src/tests/orm_patches.rs rename to sdk/rust/src/tests/orm_create_patches.rs index 1cb7938..6c93847 100644 --- a/sdk/rust/src/tests/orm_patches.rs +++ b/sdk/rust/src/tests/orm_create_patches.rs @@ -7,13 +7,13 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use crate::local_broker::{doc_create, doc_sparql_construct, doc_sparql_update, orm_start}; +use crate::local_broker::{doc_sparql_update, orm_start}; use crate::tests::create_or_open_wallet::create_or_open_wallet; use crate::tests::{assert_json_eq, create_doc_with_data}; use async_std::stream::StreamExt; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::orm::{ - BasicType, OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaPredicate, OrmSchemaShape, + BasicType, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType, OrmShapeType, }; @@ -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; @@ -89,7 +92,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/TestObject".to_string(), )]), @@ -100,7 +103,7 @@ INSERT DATA { OrmSchemaPredicate { iri: "http://example.org/arr".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -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", @@ -258,7 +267,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/TestObject".to_string(), )]), @@ -269,7 +278,7 @@ INSERT DATA { OrmSchemaPredicate { iri: "http://example.org/arr".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -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, @@ -392,12 +404,12 @@ INSERT DATA { readablePredicate: "multiNest".to_string(), dataTypes: vec![ OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/MultiNestShape1".to_string()), }, OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/MultiNestShape2".to_string()), }, @@ -411,7 +423,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "singleNest".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/SingleNestShape".to_string()), }], @@ -432,7 +444,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -452,7 +464,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -472,7 +484,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -592,39 +604,30 @@ INSERT DATA { } } -/* - - -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,32 +636,49 @@ 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::string, + valType: OrmSchemaValType::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: OrmSchemaValType::string, literals: None, shape: None, }], } .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, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/PersonShape".to_string()), }], @@ -669,188 +689,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, + valType: OrmSchemaValType::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: OrmSchemaValType::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: OrmSchemaValType::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())]), + valType: OrmSchemaValType::literal, + 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: OrmSchemaValType::string, literals: None, - shape: Some("http://example.org/ClaireShape".to_string()), + shape: None, }], } .into(), @@ -858,32 +777,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 +788,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 +798,241 @@ 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\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": "add", + "value": "red", + "path": "/urn:test:house1/rootColor", + }, + // Modified Alice's 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\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(); } diff --git a/sdk/rust/src/tests/orm_creation.rs b/sdk/rust/src/tests/orm_creation.rs index 7e29b96..9d2efec 100644 --- a/sdk/rust/src/tests/orm_creation.rs +++ b/sdk/rust/src/tests/orm_creation.rs @@ -13,8 +13,8 @@ use crate::tests::{assert_json_eq, create_doc_with_data}; use async_std::stream::StreamExt; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::orm::{ - BasicType, OrmSchema, OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaPredicate, - OrmSchemaShape, OrmShapeType, + BasicType, OrmSchema, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType, + OrmShapeType, }; use ng_repo::log_info; @@ -304,7 +304,7 @@ INSERT DATA { minCardinality: 0, maxCardinality: -1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, shape: Some("http://example.org/Person".to_string()), literals: None, }], @@ -555,7 +555,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/TestObject".to_string(), )]), @@ -566,7 +566,7 @@ INSERT DATA { OrmSchemaPredicate { iri: "http://example.org/arr".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -658,7 +658,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "opt".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::boolean, + valType: OrmSchemaValType::boolean, literals: None, shape: None, }], @@ -741,7 +741,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "lit1".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("lit 1".to_string())]), shape: None, }], @@ -754,7 +754,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "lit2".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("lit 2".to_string())]), shape: None, }], @@ -841,12 +841,12 @@ INSERT DATA { readablePredicate: "strOrNum".to_string(), dataTypes: vec![ OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }, OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }, @@ -951,7 +951,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "str".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -964,7 +964,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "nestedWithExtra".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/NestedShapeWithExtra".to_string()), }], @@ -977,7 +977,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "nestedWithoutExtra".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/NestedShapeWithoutExtra".to_string()), }], @@ -999,7 +999,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1012,7 +1012,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1034,7 +1034,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1047,7 +1047,7 @@ INSERT DATA { maxCardinality: 1, minCardinality: 1, dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1147,7 +1147,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "name".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1160,7 +1160,7 @@ INSERT DATA { minCardinality: 0, readablePredicate: "knows".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/PersonShape".to_string()), }], @@ -1293,7 +1293,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/Alice".to_string(), )]), @@ -1309,12 +1309,12 @@ INSERT DATA { readablePredicate: "knows".to_string(), dataTypes: vec![ OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/BobShape".to_string()), }, OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/ClaireShape".to_string()), }, @@ -1337,7 +1337,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("http://example.org/Bob".to_string())]), shape: None, }], @@ -1350,7 +1350,7 @@ INSERT DATA { minCardinality: 0, readablePredicate: "knows".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/ClaireShape".to_string()), }], @@ -1371,7 +1371,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/Claire".to_string(), )]), @@ -1471,7 +1471,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/Person".to_string(), )]), @@ -1486,7 +1486,7 @@ INSERT DATA { minCardinality: 0, readablePredicate: "cats".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some("http://example.org/CatShape".to_string()), }], @@ -1507,7 +1507,7 @@ INSERT DATA { minCardinality: 1, readablePredicate: "type".to_string(), dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]), shape: None, }], @@ -1574,7 +1574,7 @@ fn create_big_schema() -> OrmSchema { predicates: vec![ Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str( "http://example.org/TestObject".to_string(), )]), @@ -1588,7 +1588,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1600,7 +1600,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1612,7 +1612,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::boolean, + valType: OrmSchemaValType::boolean, literals: None, shape: None, }], @@ -1624,7 +1624,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1636,7 +1636,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some( "http://example.org/TestObject||http://example.org/objectValue" @@ -1651,7 +1651,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::shape, + valType: OrmSchemaValType::shape, literals: None, shape: Some( "http://example.org/TestObject||http://example.org/anotherObject" @@ -1667,12 +1667,12 @@ fn create_big_schema() -> OrmSchema { Arc::new(OrmSchemaPredicate { dataTypes: vec![ OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }, OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }, @@ -1686,12 +1686,12 @@ fn create_big_schema() -> OrmSchema { Arc::new(OrmSchemaPredicate { dataTypes: vec![ OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("lit1".to_string())]), shape: None, }, OrmSchemaDataType { - valType: OrmSchemaLiteralType::literal, + valType: OrmSchemaValType::literal, literals: Some(vec![BasicType::Str("lit2".to_string())]), shape: None, }, @@ -1714,7 +1714,7 @@ fn create_big_schema() -> OrmSchema { predicates: vec![ Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1726,7 +1726,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1748,7 +1748,7 @@ fn create_big_schema() -> OrmSchema { predicates: vec![ Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::string, + valType: OrmSchemaValType::string, literals: None, shape: None, }], @@ -1760,7 +1760,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }], @@ -1772,7 +1772,7 @@ fn create_big_schema() -> OrmSchema { }), Arc::new(OrmSchemaPredicate { dataTypes: vec![OrmSchemaDataType { - valType: OrmSchemaLiteralType::number, + valType: OrmSchemaValType::number, literals: None, shape: None, }],