Merge branch 'feat/orm-diffs' into refactor

feat/orm-diffs
Niko PLP 2 days ago
commit 1282b9e006
  1. 20
      engine/net/src/orm.rs
  2. 25
      engine/verifier/src/orm/add_remove_triples.rs
  3. 266
      engine/verifier/src/orm/handle_backend_update.rs
  4. 334
      engine/verifier/src/orm/handle_frontend_update.rs
  5. 9
      engine/verifier/src/orm/materialize.rs
  6. 197
      engine/verifier/src/orm/process_changes.rs
  7. 29
      engine/verifier/src/orm/query.rs
  8. 20
      engine/verifier/src/orm/shape_validation.rs
  9. 50
      engine/verifier/src/orm/utils.rs
  10. 14
      sdk/rust/src/local_broker.rs
  11. 5
      sdk/rust/src/tests/mod.rs
  12. 959
      sdk/rust/src/tests/orm_apply_patches.rs
  13. 667
      sdk/rust/src/tests/orm_create_patches.rs
  14. 88
      sdk/rust/src/tests/orm_creation.rs

@ -24,14 +24,14 @@ pub struct OrmShapeType {
} }
/* == Diff Types == */ /* == Diff Types == */
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmDiffOpType { pub enum OrmDiffOpType {
add, add,
remove, remove,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmDiffType { pub enum OrmDiffType {
set, set,
@ -66,7 +66,7 @@ pub struct OrmSchemaShape {
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmSchemaLiteralType { pub enum OrmSchemaValType {
number, number,
string, string,
boolean, boolean,
@ -85,7 +85,7 @@ pub enum BasicType {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaDataType { pub struct OrmSchemaDataType {
pub valType: OrmSchemaLiteralType, pub valType: OrmSchemaValType,
pub literals: Option<Vec<BasicType>>, pub literals: Option<Vec<BasicType>>,
pub shape: Option<String>, pub shape: Option<String>,
} }
@ -100,13 +100,23 @@ pub struct OrmSchemaPredicate {
pub minCardinality: i32, pub minCardinality: i32,
pub extra: Option<bool>, pub extra: Option<bool>,
} }
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 { impl Default for OrmSchemaDataType {
fn default() -> Self { fn default() -> Self {
Self { Self {
literals: None, literals: None,
shape: None, shape: None,
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
} }
} }
} }

@ -92,7 +92,7 @@ pub fn add_remove_triples(
// log_debug!("lock acquired on tracked_predicate"); // log_debug!("lock acquired on tracked_predicate");
tracked_predicate.current_cardinality += 1; tracked_predicate.current_cardinality += 1;
// Keep track of the changed values too. // Keep track of the added values here.
let pred_changes: &mut OrmTrackedPredicateChanges = subject_changes let pred_changes: &mut OrmTrackedPredicateChanges = subject_changes
.predicates .predicates
.entry(predicate_schema.iri.clone()) .entry(predicate_schema.iri.clone())
@ -109,7 +109,7 @@ pub fn add_remove_triples(
.schema .schema
.dataTypes .dataTypes
.iter() .iter()
.any(|dt| dt.valType == OrmSchemaLiteralType::literal) .any(|dt| dt.valType == OrmSchemaValType::literal)
{ {
match &mut tracked_predicate.current_literals { match &mut tracked_predicate.current_literals {
Some(lits) => lits.push(obj_term.clone()), Some(lits) => lits.push(obj_term.clone()),
@ -122,7 +122,7 @@ pub fn add_remove_triples(
// If predicate is of type shape, register // If predicate is of type shape, register
// "parent (predicate) -> child subject" and `child_subject.parents`. // "parent (predicate) -> child subject" and `child_subject.parents`.
for shape_iri in predicate_schema.dataTypes.iter().filter_map(|dt| { for shape_iri in predicate_schema.dataTypes.iter().filter_map(|dt| {
if dt.valType == OrmSchemaLiteralType::shape { if dt.valType == OrmSchemaValType::shape {
dt.shape.clone() dt.shape.clone()
} else { } else {
None None
@ -163,6 +163,7 @@ pub fn add_remove_triples(
} }
} }
} }
// Process removed triples. // Process removed triples.
for triple in triples_removed { for triple in triples_removed {
let pred_iri = triple.predicate.as_str(); let pred_iri = triple.predicate.as_str();
@ -181,9 +182,15 @@ pub fn add_remove_triples(
tracked_predicate.current_cardinality = tracked_predicate.current_cardinality =
tracked_predicate.current_cardinality.saturating_sub(1); tracked_predicate.current_cardinality.saturating_sub(1);
let Some(pred_changes) = subject_changes.predicates.get_mut(pred_iri) else { // Keep track of removed values here.
continue; let pred_changes: &mut OrmTrackedPredicateChanges = subject_changes
}; .predicates
.entry(tracked_predicate.schema.iri.clone())
.or_insert_with(|| OrmTrackedPredicateChanges {
tracked_predicate: tracked_predicate_rc.clone(),
values_added: Vec::new(),
values_removed: Vec::new(),
});
let val_removed = oxrdf_term_to_orm_basic_type(&triple.object); let val_removed = oxrdf_term_to_orm_basic_type(&triple.object);
pred_changes.values_removed.push(val_removed.clone()); pred_changes.values_removed.push(val_removed.clone());
@ -193,7 +200,7 @@ pub fn add_remove_triples(
.schema .schema
.dataTypes .dataTypes
.iter() .iter()
.any(|dt| dt.valType == OrmSchemaLiteralType::literal) .any(|dt| dt.valType == OrmSchemaValType::literal)
{ {
if let Some(current_literals) = &mut tracked_predicate.current_literals { if let Some(current_literals) = &mut tracked_predicate.current_literals {
// Remove obj_val from current_literals in-place // Remove obj_val from current_literals in-place
@ -205,7 +212,7 @@ pub fn add_remove_triples(
.schema .schema
.dataTypes .dataTypes
.iter() .iter()
.any(|dt| dt.valType == OrmSchemaLiteralType::shape) .any(|dt| dt.valType == OrmSchemaValType::shape)
{ {
// Remove parent from child and child from tracked children. // Remove parent from child and child from tracked children.
// If predicate is of type shape, register (parent -> child) links so that // If predicate is of type shape, register (parent -> child) links so that
@ -215,7 +222,7 @@ pub fn add_remove_triples(
.dataTypes .dataTypes
.iter() .iter()
.filter_map(|dt| { .filter_map(|dt| {
if dt.valType == OrmSchemaLiteralType::shape { if dt.valType == OrmSchemaValType::shape {
dt.shape.clone() dt.shape.clone()
} else { } else {
None None

@ -68,8 +68,6 @@ impl Verifier {
}) })
.collect(); .collect();
// let mut updates = Vec::new();
let mut scopes = vec![]; let mut scopes = vec![];
for (scope, subs) in self.orm_subscriptions.iter_mut() { for (scope, subs) in self.orm_subscriptions.iter_mut() {
// Remove old subscriptions // Remove old subscriptions
@ -86,32 +84,37 @@ impl Verifier {
} }
// prepare to apply updates to tracked subjects and record the changes. // prepare to apply updates to tracked subjects and record the changes.
let root_shapes = subs let root_shapes_and_tracked_subjects = subs
.iter() .iter()
.map(|sub| { .map(|sub| {
sub.shape_type (
.schema sub.shape_type
.get(&sub.shape_type.shape) .schema
.unwrap() .get(&sub.shape_type.shape)
.clone() .unwrap()
.clone(),
shapes_in_tracked_subjects(&sub.tracked_subjects),
)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
scopes.push((scope.clone(), root_shapes)); scopes.push((scope.clone(), root_shapes_and_tracked_subjects));
} }
log_debug!( log_debug!(
"[orm_backend_update], creating patch objects for scopes:\n{}", "[orm_backend_update], creating patch objects for #scopes {}",
scopes.len() scopes.len()
); );
for (scope, shapes) in scopes { for (scope, shapes_zip) in scopes {
let mut orm_changes: OrmChanges = HashMap::new(); let mut orm_changes: OrmChanges = HashMap::new();
// Apply the changes to tracked subjects. // 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( let _ = self.process_changes_for_shape_and_session(
&scope, &scope,
shape_arc, &shape_iri,
all_shapes,
session_id, session_id,
&triple_inserts, &triple_inserts,
&triple_removes, &triple_removes,
@ -131,9 +134,9 @@ impl Verifier {
// The JSON patches to send to JS land. // The JSON patches to send to JS land.
let mut patches: Vec<OrmDiffOp> = vec![]; let mut patches: Vec<OrmDiffOp> = vec![];
// Keep track of created objects by path and if they need an id. // Keep track of objects to create: (path, Option<IRI>)
// Later we created patches from them to ensure the objects exist. // The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers)
let mut paths_of_objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> = let mut objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> =
HashSet::new(); HashSet::new();
// We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...} // We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...}
@ -157,6 +160,11 @@ impl Verifier {
// Iterate over all changes and create patches // Iterate over all changes and create patches
for (shape_iri, subject_changes) in &orm_changes { for (shape_iri, subject_changes) in &orm_changes {
for (subject_iri, change) in subject_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 // Get the tracked subject for this (subject, shape) pair
let tracked_subject = sub let tracked_subject = sub
.tracked_subjects .tracked_subjects
@ -193,12 +201,19 @@ impl Verifier {
&mut path, &mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None), (OrmDiffOpType::remove, Some(OrmDiffType::object), None, None),
&mut patches, &mut patches,
&mut paths_of_objects_to_create, &mut objects_to_create,
); );
} else { } else {
// The subject is valid or has become valid. // The subject is valid or has become valid.
// Process each predicate change // Process each predicate change
for (_pred_iri, pred_change) in &change.predicates { 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 = let tracked_predicate =
pred_change.tracked_predicate.read().unwrap(); pred_change.tracked_predicate.read().unwrap();
let pred_name = tracked_predicate.schema.readablePredicate.clone(); let pred_name = tracked_predicate.schema.readablePredicate.clone();
@ -218,7 +233,7 @@ impl Verifier {
&mut path, &mut path,
diff_op, diff_op,
&mut patches, &mut patches,
&mut paths_of_objects_to_create, &mut objects_to_create,
); );
} }
} }
@ -229,26 +244,29 @@ impl Verifier {
// Create patches for objects that need to be created // Create patches for objects that need to be created
// These are patches with {op: add, valType: object, value: Null, path: ...} // These are patches with {op: add, valType: object, value: Null, path: ...}
// Sort by path length (shorter first) to ensure parent objects are created before children // Sort by path length (shorter first) to ensure parent objects are created before children
let mut sorted_object_paths: Vec<_> = paths_of_objects_to_create.iter().collect(); let mut sorted_objects: Vec<_> = objects_to_create.iter().collect();
sorted_object_paths.sort_by_key(|(path_segments, _)| path_segments.len()); sorted_objects.sort_by_key(|(path_segments, _)| path_segments.len());
for (path_segments, maybe_iri) in sorted_object_paths { for (path_segments, maybe_iri) in sorted_objects {
let escaped_path: Vec<String> = path_segments let escaped_path: Vec<String> = path_segments
.iter() .iter()
.map(|seg| escape_json_pointer(seg)) .map(|seg| escape_json_pointer(seg))
.collect(); .collect();
let json_pointer = format!("/{}", escaped_path.join("/")); let json_pointer = format!("/{}", escaped_path.join("/"));
// Always create the object itself
patches.push(OrmDiffOp { patches.push(OrmDiffOp {
op: OrmDiffOpType::add, op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object), valType: Some(OrmDiffType::object),
path: json_pointer.clone(), path: json_pointer.clone(),
value: None, value: None,
}); });
// If this object has an IRI (it's a real subject), add the id field
if let Some(iri) = maybe_iri { if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp { patches.push(OrmDiffOp {
op: OrmDiffOpType::add, op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object), valType: None,
path: format!("{}/id", json_pointer), path: format!("{}/id", json_pointer),
value: Some(json!(iri)), value: Some(json!(iri)),
}); });
@ -266,6 +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<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
root_shape: &String,
path: &[String],
patches: &mut Vec<OrmDiffOp>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
// Check if we're at a root subject or need to traverse to parents
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
// Register object for creation.
// Path to object consists of this subject's iri and the path except for the last element.
let mut path_to_subject = vec![tracked_subject.subject_iri.clone()];
if path.len() > 1 {
path_to_subject.extend_from_slice(&path[..path.len() - 1]);
}
// log_debug!("Queuing object creation for path: {:?}", path_to_subject);
// Always create the object itself with its IRI
objects_to_create.insert((
path_to_subject.clone(),
Some(tracked_subject.subject_iri.clone()),
));
} else {
// Not at root: traverse to parents and create object patches along the way
for (_parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
let parent_ts = parent_tracked_subject.read().unwrap();
if let Some(new_path) = build_path_segment_for_parent(tracked_subject, &parent_ts, path)
{
// Check if the parent's predicate is multi-valued and if no siblings were previously valid
let should_create_parent_predicate_object =
check_should_create_parent_predicate_object(tracked_subject, &parent_ts);
if should_create_parent_predicate_object {
// Need to create an intermediate object for the multi-valued predicate
// This is the case for Person -> hasAddress -> (object) -> AddressIri -> AddressObject
// The intermediate (object) doesn't have an IRI
let mut intermediate_path = new_path.clone();
intermediate_path.pop(); // Remove the subject IRI that was added for multi predicates
objects_to_create.insert((intermediate_path, None));
}
// Recurse to the parent first
queue_patches_for_newly_valid_subject(
&parent_ts,
tracked_subjects,
root_shape,
&new_path,
patches,
objects_to_create,
);
// Register this object for creation with its IRI
objects_to_create
.insert((new_path.clone(), Some(tracked_subject.subject_iri.clone())));
}
}
}
}
/// Check if we should create an intermediate object for a multi-valued predicate.
/// Returns true if the parent's predicate is multi-valued and no siblings were previously valid.
fn check_should_create_parent_predicate_object(
tracked_subject: &OrmTrackedSubject,
parent_ts: &OrmTrackedSubject,
) -> bool {
// Find the predicate schema linking parent to this subject
for pred_arc in &parent_ts.shape.predicates {
if let Some(tracked_pred) = parent_ts.tracked_predicates.get(&pred_arc.iri) {
let tp = tracked_pred.read().unwrap();
// Check if this tracked subject is a child of this predicate
let is_child = tp.tracked_children.iter().any(|child| {
let child_read = child.read().unwrap();
child_read.subject_iri == tracked_subject.subject_iri
});
if is_child {
let is_multi = pred_arc.maxCardinality > 1 || pred_arc.maxCardinality == -1;
if is_multi {
// Check if any siblings were previously valid
let any_sibling_was_valid = tp.tracked_children.iter().any(|child| {
let child_read = child.read().unwrap();
child_read.subject_iri != tracked_subject.subject_iri
&& child_read.prev_valid == OrmTrackedSubjectValidity::Valid
});
return !any_sibling_was_valid;
}
return false;
}
}
}
false
}
/// Find the predicate schema linking a parent to a child tracked subject and build the path segment. /// Find the predicate schema linking a parent to a child tracked subject and build the path segment.
/// Returns the updated path if a linking predicate is found. /// Returns the updated path if a linking predicate is found.
fn build_path_segment_for_parent( fn build_path_segment_for_parent(
@ -321,65 +441,18 @@ fn build_path_to_root_and_create_patches(
Option<String>, // The IRI, if change is an added / removed object. Option<String>, // The IRI, if change is an added / removed object.
), ),
patches: &mut Vec<OrmDiffOp>, patches: &mut Vec<OrmDiffOp>,
paths_of_objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>, objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) { ) {
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 the tracked subject is not valid, we don't create patches for it
if tracked_subject.valid != OrmTrackedSubjectValidity::Valid { if tracked_subject.valid != OrmTrackedSubjectValidity::Valid {
return; 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<String> =
path.iter().map(|seg| escape_json_pointer(seg)).collect();
let json_pointer = format!(
"/{}/{}",
escape_json_pointer(&tracked_subject.subject_iri),
escaped_path.join("/")
);
// Create an "add object" patch to ensure the object exists
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: json_pointer.clone(),
value: None,
});
// Also add the id field for the object
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: None,
path: format!("{}/id", json_pointer),
value: Some(json!(tracked_subject.subject_iri)),
});
} else {
// Not at root: traverse to parents and create object patches along the way
for (_parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
let parent_ts = parent_tracked_subject.read().unwrap();
if let Some(new_path) =
build_path_segment_for_parent(tracked_subject, &parent_ts, path)
{
// Recurse to the parent first
build_path_to_root_and_create_patches(
&parent_ts,
tracked_subjects,
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 this subject has no parents or its shape matches the root shape, we've reached the root
if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape { if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape {
// Build the final JSON Pointer path // Build the final JSON Pointer path
@ -391,7 +464,7 @@ fn build_path_to_root_and_create_patches(
escaped_path.join("/") escaped_path.join("/")
); );
// Create the patch // Create the patch for the actual value change
patches.push(OrmDiffOp { patches.push(OrmDiffOp {
op: diff_op.0.clone(), op: diff_op.0.clone(),
valType: diff_op.1.clone(), valType: diff_op.1.clone(),
@ -399,15 +472,19 @@ fn build_path_to_root_and_create_patches(
value: diff_op.2.clone(), value: diff_op.2.clone(),
}); });
// // If a new object is created on a predicate where multiple ones are allowed, create IRI path too. // If the subject is newly valid, now we have the full path to queue its creation.
// if let Some(added_obj_iri) = diff_op.3 { if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid {
// patches.push(OrmDiffOp { let mut final_path = vec![tracked_subject.subject_iri.clone()];
// op: diff_op.0.clone(), final_path.extend_from_slice(path);
// valType: diff_op.1.clone(), queue_patches_for_newly_valid_subject(
// path: format!("{}/{}", json_pointer, escape_json_pointer(&added_obj_iri)), tracked_subject,
// value: diff_op.2.clone(), tracked_subjects,
// }); root_shape,
// } &final_path,
patches,
objects_to_create,
);
}
return; return;
} }
@ -417,16 +494,17 @@ fn build_path_to_root_and_create_patches(
let parent_ts = parent_tracked_subject.read().unwrap(); let parent_ts = parent_tracked_subject.read().unwrap();
// Build the path segment for this parent // 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 // Recurse to the parent
build_path_to_root_and_create_patches( build_path_to_root_and_create_patches(
&parent_ts, &parent_ts,
tracked_subjects, tracked_subjects,
root_shape, root_shape,
&mut new_path.clone(), &mut new_path,
diff_op.clone(), diff_op.clone(),
patches, patches,
paths_of_objects_to_create, objects_to_create,
); );
} }
} }
@ -526,3 +604,15 @@ fn create_diff_ops_from_predicate_change(
// } // }
return ops; return ops;
} }
fn shapes_in_tracked_subjects(
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
) -> Vec<Arc<OrmSchemaShape>> {
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
}

@ -7,9 +7,11 @@
// notice may not be copied, modified, or distributed except // notice may not be copied, modified, or distributed except
// according to those terms. // according to those terms.
use ng_net::orm::{OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaPredicate, OrmSchemaShape};
use ng_oxigraph::oxrdf::Quad; use ng_oxigraph::oxrdf::Quad;
use ng_repo::errors::VerifierError; use ng_repo::errors::VerifierError;
use std::sync::{Arc, RwLock};
use std::u64; use std::u64;
use futures::SinkExt; use futures::SinkExt;
@ -18,6 +20,7 @@ pub use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_repo::log::*; use ng_repo::log::*;
use crate::orm::types::*; use crate::orm::types::*;
use crate::orm::utils::{decode_json_pointer, json_to_sparql_val};
use crate::verifier::*; use crate::verifier::*;
impl Verifier { impl Verifier {
@ -30,11 +33,11 @@ impl Verifier {
scope: &NuriV0, scope: &NuriV0,
shape_iri: ShapeIri, shape_iri: ShapeIri,
session_id: u64, session_id: u64,
skolemnized_blank_nodes: Vec<Quad>, _skolemnized_blank_nodes: Vec<Quad>,
revert_inserts: Vec<Quad>, _revert_inserts: Vec<Quad>,
revert_removes: Vec<Quad>, _revert_removes: Vec<Quad>,
) -> Result<(), VerifierError> { ) -> 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))?; self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?;
// TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes // TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes
@ -63,9 +66,8 @@ impl Verifier {
diff: OrmDiff, diff: OrmDiff,
) -> Result<(), String> { ) -> Result<(), String> {
log_info!( log_info!(
"frontend_update_orm session={} scope={:?} shape={} diff={:?}", "frontend_update_orm session={} shape={} diff={:?}",
session_id, session_id,
scope,
shape_iri, shape_iri,
diff diff
); );
@ -73,14 +75,14 @@ impl Verifier {
let (doc_nuri, sparql_update) = { let (doc_nuri, sparql_update) = {
let orm_subscription = let orm_subscription =
self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id)); 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) (doc_nuri, sparql_update)
}; };
log_debug!("Created SPARQL query for patches:\n{}", sparql_update);
match self match self
.process_sparql_update( .process_sparql_update(
&doc_nuri, &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<String> = 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 <subject> <predicate>
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<RwLock<OrmTrackedSubject>> {
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<ng_net::orm::OrmSchemaPredicate> {
// 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<String>
/// - The Option subject, predicate, Option<Object> 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, String, Option<String>),
Option<Arc<OrmSchemaPredicate>>,
) {
let mut body_statements: Vec<String> = vec![];
let mut where_statements: Vec<String> = vec![];
let mut path: Vec<String> = 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<OrmSchemaShape> = orm_subscription
.shape_type
.schema
.get(&orm_subscription.shape_type.shape)
.unwrap();
let mut current_subj_schema: Arc<OrmSchemaShape> = 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, &current_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. <bob> <hasCat> ?o1 . ?o1 <type> 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<OrmSchemaShape> {
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<OrmSchemaShape> {
return orm_subscription
.shape_type
.schema
.get(pred_schema.dataTypes[0].shape.as_ref().unwrap())
.unwrap()
.clone();
}

@ -103,16 +103,13 @@ impl Verifier {
let mut return_vals: Value = Value::Array(vec![]); let mut return_vals: Value = Value::Array(vec![]);
let return_val_vec = return_vals.as_array_mut().unwrap(); let return_val_vec = return_vals.as_array_mut().unwrap();
// log_debug!( log_debug!("\nMaterializing: {}", shape_type.shape);
// "Tracked subjects:\n{:?}\n",
// orm_subscription.tracked_subjects,
// );
// For each valid change struct, we build an orm object. // 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. // 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 { 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) { if let Some(tracked_subject) = tracked_subjects_by_shape.get(&shape_type.shape) {
let ts = tracked_subject.read().unwrap(); 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 ts.valid == OrmTrackedSubjectValidity::Valid {
if let Some(change) = changes if let Some(change) = changes
@ -164,7 +161,7 @@ pub(crate) fn materialize_orm_object(
if pred_schema if pred_schema
.dataTypes .dataTypes
.iter() .iter()
.any(|dt| dt.valType == OrmSchemaLiteralType::shape) .any(|dt| dt.valType == OrmSchemaValType::shape)
{ {
// We have a nested type. // We have a nested type.

@ -81,54 +81,15 @@ impl Verifier {
Ok(merged) Ok(merged)
} }
/// Helper to call process_changes_for_shape for all subscriptions on nuri's document.
fn process_changes_for_nuri_and_session(
self: &mut Self,
nuri: &NuriV0,
session_id: u64,
triples_added: &[Triple],
triples_removed: &[Triple],
data_already_fetched: bool,
) -> Result<OrmChanges, NgError> {
let mut orm_changes = HashMap::new();
let shapes: Vec<_> = self
.orm_subscriptions
.get(nuri)
.unwrap()
.iter()
.map(|sub| {
sub.shape_type
.schema
.get(&sub.shape_type.shape)
.unwrap()
.clone()
})
.collect();
for root_shape in shapes {
self.process_changes_for_shape_and_session(
nuri,
root_shape,
session_id,
triples_added,
triples_removed,
&mut orm_changes,
data_already_fetched,
)?;
}
Ok(orm_changes)
}
/// Add and remove the triples from the tracked subjects, /// Add and remove the triples from the tracked subjects,
/// re-validate, and update `changes` containing the updated data. /// re-validate, and update `changes` containing the updated data.
/// Works by queuing changes by shape and subjects on a stack. /// Works by queuing changes by shape and subjects on a stack.
/// Nested objects are added to the stack /// Nested objects are added to the stack
pub(crate) fn process_changes_for_shape_and_session( pub(crate) fn process_changes_for_shape_and_session(
self: &mut Self, &mut self,
nuri: &NuriV0, nuri: &NuriV0,
root_shape: Arc<OrmSchemaShape>, root_shape_iri: &String,
shapes: Vec<Arc<OrmSchemaShape>>,
session_id: u64, session_id: u64,
triples_added: &[Triple], triples_added: &[Triple],
triples_removed: &[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. // Track (shape_iri, subject_iri) pairs currently being validated to prevent cycles and double evaluation.
let mut currently_validating: HashSet<(String, String)> = HashSet::new(); let mut currently_validating: HashSet<(String, String)> = HashSet::new();
// Add root shape for first validation run. // Add root shape for first validation run.
let root_shape_iri = root_shape.iri.clone(); for shape in shapes {
shape_validation_stack.push((root_shape, vec![])); shape_validation_stack.push((shape, vec![]));
}
// Process queue of shapes and subjects to validate. // Process queue of shapes and subjects to validate.
// For a given shape, we evaluate every subject against that shape. // For a given shape, we evaluate every subject against that shape.
@ -156,14 +118,6 @@ impl Verifier {
.chain(removed_triples_by_subject.keys()) .chain(removed_triples_by_subject.keys())
.collect(); .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. // Variable to collect nested objects that need validation.
let mut nested_objects_to_eval: HashMap<ShapeIri, Vec<(SubjectIri, bool)>> = let mut nested_objects_to_eval: HashMap<ShapeIri, Vec<(SubjectIri, bool)>> =
HashMap::new(); HashMap::new();
@ -175,6 +129,7 @@ impl Verifier {
shape.iri shape.iri
); );
// For each modified subject, apply changes to tracked subjects and validate.
for subject_iri in &modified_subject_iris { for subject_iri in &modified_subject_iris {
let validation_key = (shape.iri.clone(), subject_iri.to_string()); let validation_key = (shape.iri.clone(), subject_iri.to_string());
@ -185,8 +140,13 @@ impl Verifier {
subject_iri, subject_iri,
shape.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) = if let Some(tracked_shapes) =
orm_subscription.tracked_subjects.get(*subject_iri) 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. // Apply all triples for that subject to the tracked (shape, subject) pair.
// Record the changes. // 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 { if !change.data_applied {
log_debug!( log_debug!(
"Adding triples to change tracker for subject {}", "Adding triples to change tracker for subject {}",
subject_iri subject_iri
); );
if let Err(e) = add_remove_triples( if let Err(e) = add_remove_triples(
shape.clone(), shape.clone(),
subject_iri, subject_iri,
triples_added_for_subj, triples_added_for_subj,
triples_removed_for_subj, triples_removed_for_subj,
&mut orm_subscription, orm_subscription,
change, change,
) { ) {
log_err!("apply_changes_from_triples add/remove error: {:?}", e); log_err!("apply_changes_from_triples add/remove error: {:?}", e);
panic!(); panic!();
} }
change.data_applied = true; 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. // Validate the subject.
let need_eval = // need_eval contains elements in reverse priority (last element to be validated first)
Self::update_subject_validity(change, &shape, &mut orm_subscription); // 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. // We add the need_eval to be processed next after loop.
// Filter out subjects already in the validation stack to prevent double evaluation. // 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 // Now, we queue all non-evaluated objects
for (shape_iri, objects_to_eval) in &nested_objects_to_eval { for (shape_iri, objects_to_eval) in &nested_objects_to_eval {
let orm_subscription = self.get_first_orm_subscription_for( // Extract schema and shape Arc first (before any borrows)
nuri, let schema = {
Some(&root_shape_iri), let orm_sub = self.get_first_orm_subscription_for(
Some(&session_id), nuri,
); Some(&root_shape_iri),
// Extract schema and shape Arc before mutable borrow Some(&session_id),
let schema = orm_subscription.shape_type.schema.clone(); );
orm_sub.shape_type.schema.clone()
};
let shape_arc = schema.get(shape_iri).unwrap().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). // 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. // Recursively process nested objects.
self.process_changes_for_shape_and_session( self.process_changes_for_shape_and_session(
nuri, nuri,
shape_arc.clone(), &root_shape_iri,
[shape_arc.clone()].to_vec(),
session_id, session_id,
&new_triples, &new_triples,
&vec![], &vec![],
@ -323,23 +330,47 @@ impl Verifier {
Ok(()) Ok(())
} }
/// Helper to get orm subscriptions for nuri, shapes and sessions. /// Helper to call process_changes_for_shape for all subscriptions on nuri's document.
pub fn get_orm_subscriptions_for( fn process_changes_for_nuri_and_session(
&self, self: &mut Self,
nuri: &NuriV0, nuri: &NuriV0,
shape: Option<&ShapeIri>, session_id: u64,
session_id: Option<&u64>, triples_added: &[Triple],
) -> Vec<&OrmSubscription> { triples_removed: &[Triple],
self.orm_subscriptions.get(nuri).unwrap(). data_already_fetched: bool,
// Filter shapes, if present. ) -> Result<OrmChanges, NgError> {
iter().filter(|s| match shape { let mut orm_changes = HashMap::new();
Some(sh) => *sh == s.shape_type.shape,
None => true let shapes: Vec<_> = self
// Filter session ids if present. .orm_subscriptions
}).filter(|s| match session_id { .get(nuri)
Some(id) => *id == s.session_id, .unwrap()
None => true .iter()
}).collect() .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( pub fn get_first_orm_subscription_for(

@ -7,15 +7,14 @@
// notice may not be copied, modified, or distributed except // notice may not be copied, modified, or distributed except
// according to those terms. // according to those terms.
use lazy_static::lazy_static;
use ng_repo::errors::VerifierError; use ng_repo::errors::VerifierError;
use regex::Regex;
use std::collections::HashSet; use std::collections::HashSet;
pub use ng_net::orm::{OrmDiff, OrmShapeType}; pub use ng_net::orm::{OrmDiff, OrmShapeType};
use crate::orm::types::*; use crate::orm::types::*;
use crate::orm::utils::{escape_literal, is_iri};
use crate::verifier::*; use crate::verifier::*;
use ng_net::orm::*; use ng_net::orm::*;
use ng_oxigraph::oxigraph::sparql::{Query, QueryResults}; 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<String> { pub fn literal_to_sparql_str(var: OrmSchemaDataType) -> Vec<String> {
match var.literals { match var.literals {
None => [].to_vec(), 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. // Predicate constraints might have more than one acceptable nested shape. Traverse each.
for datatype in &predicate.dataTypes { for datatype in &predicate.dataTypes {
if datatype.valType == OrmSchemaLiteralType::shape { if datatype.valType == OrmSchemaValType::shape {
let shape_iri = &datatype.shape.clone().unwrap(); let shape_iri = &datatype.shape.clone().unwrap();
let nested_shape = schema.get(shape_iri).unwrap(); let nested_shape = schema.get(shape_iri).unwrap();
@ -305,18 +295,3 @@ pub fn shape_type_to_sparql(
construct_body, where_body 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;
}

@ -33,7 +33,6 @@ impl Verifier {
}; };
let mut tracked_subject = tracked_subject.write().unwrap(); let mut tracked_subject = tracked_subject.write().unwrap();
let previous_validity = tracked_subject.prev_valid.clone(); 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. // 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![]; 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. // 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) Schedule children for deletion
// 1.1.1) Set all children to `untracked` that don't have other parents. // 1.1.1) Set all children to `untracked` that don't have other parents.
for tracked_predicate in tracked_subject.tracked_predicates.values() { for tracked_predicate in tracked_subject.tracked_predicates.values() {
@ -152,7 +154,7 @@ impl Verifier {
} else if p_schema } else if p_schema
.dataTypes .dataTypes
.iter() .iter()
.any(|dt| dt.valType == OrmSchemaLiteralType::literal) .any(|dt| dt.valType == OrmSchemaValType::literal)
{ {
// If we have literals, check if all required literals are present. // If we have literals, check if all required literals are present.
// At least one datatype must match. // At least one datatype must match.
@ -196,7 +198,7 @@ impl Verifier {
} else if p_schema } else if p_schema
.dataTypes .dataTypes
.iter() .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. // 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| { let tracked_children = tracked_pred.as_ref().map(|tp| {
@ -307,19 +309,19 @@ impl Verifier {
// Check 3.5) Data types correct. // Check 3.5) Data types correct.
} else { } else {
// Check if the data type is correct. // 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(); p_schema.dataTypes.iter().map(|dt| &dt.valType).collect();
// For each new value, check that data type is in allowed_types. // 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() { for val_added in p_change.iter().map(|pc| &pc.values_added).flatten() {
let matches = match val_added { let matches = match val_added {
BasicType::Bool(_) => allowed_types BasicType::Bool(_) => allowed_types
.iter() .iter()
.any(|t| **t == OrmSchemaLiteralType::boolean), .any(|t| **t == OrmSchemaValType::boolean),
BasicType::Num(_) => allowed_types BasicType::Num(_) => allowed_types
.iter() .iter()
.any(|t| **t == OrmSchemaLiteralType::number), .any(|t| **t == OrmSchemaValType::number),
BasicType::Str(_) => allowed_types.iter().any(|t| { BasicType::Str(_) => allowed_types.iter().any(|t| {
**t == OrmSchemaLiteralType::string || **t == OrmSchemaLiteralType::iri **t == OrmSchemaValType::string || **t == OrmSchemaValType::iri
}), }),
}; };
if !matches { if !matches {
@ -342,6 +344,8 @@ impl Verifier {
}; };
} }
// == End of validation part. Next, process side-effects ==
tracked_subject.valid = new_validity.clone(); tracked_subject.valid = new_validity.clone();
if new_validity == OrmTrackedSubjectValidity::Invalid { if new_validity == OrmTrackedSubjectValidity::Invalid {

@ -13,6 +13,9 @@ use ng_repo::types::OverlayId;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use lazy_static::lazy_static;
use regex::Regex;
pub use ng_net::orm::{OrmDiff, OrmShapeType}; pub use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_net::{app_protocol::*, orm::*}; use ng_net::{app_protocol::*, orm::*};
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;
@ -65,6 +68,51 @@ pub fn escape_json_pointer(path_segment: &String) -> String {
path_segment.replace("~", "~0").replace("/", "~1") 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", "~") 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::<Vec<String>>()
.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)
}

@ -17,7 +17,7 @@ use async_std::sync::{Arc, Condvar, Mutex, RwLock};
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use ng_net::orm::OrmShapeType; use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str}; use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
@ -2764,6 +2764,18 @@ pub async fn orm_start(
app_request_stream(request).await 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( pub async fn doc_sparql_construct(
session_id: u64, session_id: u64,
sparql: String, sparql: String,

@ -15,8 +15,9 @@ use crate::local_broker::{doc_create, doc_sparql_update};
#[doc(hidden)] #[doc(hidden)]
pub mod orm_creation; pub mod orm_creation;
pub mod orm_apply_patches;
#[doc(hidden)] #[doc(hidden)]
pub mod orm_patches; pub mod orm_create_patches;
#[doc(hidden)] #[doc(hidden)]
pub mod create_or_open_wallet; 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()); let diff = serde_json_diff::values(expected.clone(), actual.clone());
if let Some(diff_) = diff { if let Some(diff_) = diff {
log_err!( log_err!(
"Expected and actual ORM JSON mismatch.\nDiff: {:?}\nExpected: {}\nActual: {}", "Expected and actual JSON mismatch.\nDiff: {:?}\nExpected: {}\nActual: {}",
diff_, diff_,
expected, expected,
actual actual

@ -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
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// 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: <http://example.org/>
INSERT DATA {
<urn:test:person1> 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: <http://example.org/>
INSERT DATA {
<urn:test:person2> 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: <http://example.org/>
INSERT DATA {
<urn:test:person3> 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: <http://example.org/>
INSERT DATA {
<urn:test:person4> 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: <http://example.org/>
INSERT DATA {
<urn:test:person5> 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: <http://example.org/>
INSERT DATA {
<urn:test:person6> a ex:Person ;
ex:name "Dave" ;
ex:address <urn:test:address1> .
<urn:test:address1> 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: <http://example.org/>
INSERT DATA {
<urn:test:person7> a ex:Person ;
ex:name "Eve" ;
ex:company <urn:test:company1> .
<urn:test:company1> a ex:Company ;
ex:companyName "Acme Corp" ;
ex:headquarter <urn:test:address2> .
<urn:test:address2> 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");
}

@ -7,13 +7,13 @@
// notice may not be copied, modified, or distributed except // notice may not be copied, modified, or distributed except
// according to those terms. // 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::create_or_open_wallet::create_or_open_wallet;
use crate::tests::{assert_json_eq, create_doc_with_data}; use crate::tests::{assert_json_eq, create_doc_with_data};
use async_std::stream::StreamExt; use async_std::stream::StreamExt;
use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0};
use ng_net::orm::{ use ng_net::orm::{
BasicType, OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaPredicate, OrmSchemaShape, BasicType, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType,
OrmShapeType, OrmShapeType,
}; };
@ -23,18 +23,21 @@ use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
#[async_std::test] #[async_std::test]
async fn test_orm_path_creation() { async fn test_orm_patch_creation() {
// Setup wallet and document // Setup wallet and document
let (_wallet, session_id) = create_or_open_wallet().await; let (_wallet, session_id) = create_or_open_wallet().await;
// Tests below all in this test, to prevent waiting times through wallet creation. // Tests below all in this test, to prevent waiting times through wallet creation.
// === // // ===
test_patch_add_array(session_id).await; // test_patch_add_array(session_id).await;
test_patch_remove_array(session_id).await; // test_patch_remove_array(session_id).await;
// // === // // ===
// test_orm_with_optional(session_id).await; // test_patch_add_nested_1(session_id).await;
// ===
test_patch_nested_house_inhabitants(session_id).await;
// // === // // ===
// test_orm_literal(session_id).await; // test_orm_literal(session_id).await;
@ -89,7 +92,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/TestObject".to_string(), "http://example.org/TestObject".to_string(),
)]), )]),
@ -100,7 +103,7 @@ INSERT DATA {
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/arr".to_string(), iri: "http://example.org/arr".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -153,6 +156,7 @@ INSERT DATA {
ex:arr 3 . ex:arr 3 .
<urn:test:numArrayObj4> <urn:test:numArrayObj4>
a ex:TestObject ;
ex:arr 0 . ex:arr 0 .
} }
"# "#
@ -196,7 +200,6 @@ INSERT DATA {
"value": [3.0], "value": [3.0],
"path": "/urn:test:numArrayObj3/numArray", "path": "/urn:test:numArrayObj3/numArray",
}, },
// TODO: The two below are not added.
{ {
"op": "add", "op": "add",
"valType": "object", "valType": "object",
@ -209,6 +212,12 @@ INSERT DATA {
"path": "/urn:test:numArrayObj4/id", "path": "/urn:test:numArrayObj4/id",
"valType": Value::Null, "valType": Value::Null,
}, },
{
"op": "add",
"value": "http://example.org/TestObject",
"path": "/urn:test:numArrayObj4/type",
"valType": Value::Null,
},
{ {
"op": "add", "op": "add",
"valType": "set", "valType": "set",
@ -258,7 +267,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/TestObject".to_string(), "http://example.org/TestObject".to_string(),
)]), )]),
@ -269,7 +278,7 @@ INSERT DATA {
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/arr".to_string(), iri: "http://example.org/arr".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: 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) { async fn test_patch_add_nested_1(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,
@ -392,12 +404,12 @@ INSERT DATA {
readablePredicate: "multiNest".to_string(), readablePredicate: "multiNest".to_string(),
dataTypes: vec![ dataTypes: vec![
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/MultiNestShape1".to_string()), shape: Some("http://example.org/MultiNestShape1".to_string()),
}, },
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/MultiNestShape2".to_string()), shape: Some("http://example.org/MultiNestShape2".to_string()),
}, },
@ -411,7 +423,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "singleNest".to_string(), readablePredicate: "singleNest".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/SingleNestShape".to_string()), shape: Some("http://example.org/SingleNestShape".to_string()),
}], }],
@ -432,7 +444,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -452,7 +464,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -472,7 +484,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -592,39 +604,30 @@ INSERT DATA {
} }
} }
/* /// Test nested modifications with House -> Person -> Cat hierarchy
async fn test_patch_nested_house_inhabitants(session_id: u64) {
Old things
*/
async fn test_orm_nested_2(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,
r#" r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
INSERT DATA { INSERT DATA {
# Valid <urn:test:house1>
<urn:test:alice> a ex:House ;
ex:knows <urn:test:bob>, <urn:test:claire> ; ex:rootColor "blue" ;
ex:name "Alice" . ex:inhabitants <urn:test:person1>, <urn:test:person2> .
<urn:test:bob>
ex:knows <urn:test:claire> ; <urn:test:person1>
ex:name "Bob" . a ex:Person ;
<urn:test:claire> ex:name "Alice" ;
ex:name "Claire" . ex:hasCat <urn:test:cat1> .
# Invalid because claire2 is invalid <urn:test:person2>
<urn:test:alice2> a ex:Person ;
ex:knows <urn:test:bob2>, <urn:test:claire2> ;
ex:name "Alice" .
# Invalid because claire2 is invalid
<urn:test:bob2>
ex:knows <urn:test:claire2> ;
ex:name "Bob" . ex:name "Bob" .
# Invalid because name is missing.
<urn:test:claire2> <urn:test:cat1>
ex:missingName "Claire missing" . a ex:Cat ;
ex:catName "Whiskers" .
} }
"# "#
.to_string(), .to_string(),
@ -633,32 +636,49 @@ INSERT DATA {
// Define the ORM schema // Define the ORM schema
let mut schema = HashMap::new(); let mut schema = HashMap::new();
// House shape
schema.insert( schema.insert(
"http://example.org/PersonShape".to_string(), "http://example.org/HouseShape".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/PersonShape".to_string(), iri: "http://example.org/HouseShape".to_string(),
predicates: vec![ predicates: vec![
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/name".to_string(), iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None, extra: Some(false),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "name".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { 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, literals: None,
shape: None, shape: None,
}], }],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(), iri: "http://example.org/inhabitants".to_string(),
extra: Some(false), extra: Some(false),
maxCardinality: -1, maxCardinality: -1,
minCardinality: 0, minCardinality: 1,
readablePredicate: "knows".to_string(), readablePredicate: "inhabitants".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/PersonShape".to_string()), shape: Some("http://example.org/PersonShape".to_string()),
}], }],
@ -669,188 +689,87 @@ INSERT DATA {
.into(), .into(),
); );
let shape_type = OrmShapeType { // Person shape
schema,
shape: "http://example.org/PersonShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id)
.await
.expect("orm_start");
while let Some(app_response) = receiver.next().await {
let orm_json = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json),
_ => None,
},
}
.unwrap();
log_info!(
"ORM JSON arrived for nested2 (person) test\n: {:?}",
orm_json
);
// Expected: alice and bob with their nested knows relationships
// claire2 is invalid (missing name), so alice2's knows chain is incomplete
let mut expected = json!([
{
"id": "urn:test:alice",
"name": "Alice",
"knows": {
"urn:test:bob": {
"name": "Bob",
"knows": {
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
{
"id": "urn:test:bob",
"name": "Bob",
"knows": {
"urn:test:claire": {
"name": "Claire",
"knows": {}
}
}
},
{
"id": "urn:test:claire",
"name": "Claire",
"knows": {}
}
]);
let mut actual_mut = orm_json.clone();
log_info!(
"JSON for nested2\n{}",
serde_json::to_string(&actual_mut).unwrap()
);
assert_json_eq(&mut expected, &mut actual_mut);
break;
}
cancel_fn();
}
async fn test_orm_nested_3(session_id: u64) {
let doc_nuri = create_doc_with_data(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
# Valid
<urn:test:alice>
a ex:Alice ;
ex:knows <urn:test:bob>, <urn:test:claire> .
<urn:test:bob>
a ex:Bob ;
ex:knows <urn:test:claire> .
<urn:test:claire>
a ex:Claire .
# Invalid because claire is invalid
<urn:test:alice2>
a ex:Alice ;
ex:knows <urn:test:bob2>, <urn:test:claire2> .
# Invalid because claire is invalid
<urn:test:bob2>
a ex:Bob ;
ex:knows <urn:test:claire2> .
# Invalid, wrong type.
<urn:test:claire2>
a ex:Claire2 .
}
"#
.to_string(),
)
.await;
// Define the ORM schema
let mut schema = HashMap::new();
schema.insert( schema.insert(
"http://example.org/AliceShape".to_string(), "http://example.org/PersonShape".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/AliceShape".to_string(), iri: "http://example.org/PersonShape".to_string(),
predicates: vec![ predicates: vec![
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None, extra: Some(false),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/Alice".to_string(), "http://example.org/Person".to_string(),
)]), )]),
shape: None, shape: None,
}], }],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(), iri: "http://example.org/name".to_string(),
extra: Some(false), extra: Some(false),
maxCardinality: -1, 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, minCardinality: 0,
readablePredicate: "knows".to_string(), readablePredicate: "cat".to_string(),
dataTypes: vec![ dataTypes: vec![OrmSchemaDataType {
OrmSchemaDataType { valType: OrmSchemaValType::shape,
valType: OrmSchemaLiteralType::shape, literals: None,
literals: None, shape: Some("http://example.org/CatShape".to_string()),
shape: Some("http://example.org/BobShape".to_string()), }],
},
OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/ClaireShape".to_string()),
},
],
} }
.into(), .into(),
], ],
} }
.into(), .into(),
); );
// Cat shape
schema.insert( schema.insert(
"http://example.org/BobShape".to_string(), "http://example.org/CatShape".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/BobShape".to_string(), iri: "http://example.org/CatShape".to_string(),
predicates: vec![ predicates: vec![
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(true), extra: Some(false),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Bob".to_string())]), literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]),
shape: None, shape: None,
}], }],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(), iri: "http://example.org/catName".to_string(),
extra: Some(false), extra: Some(false),
maxCardinality: -1, maxCardinality: 1,
minCardinality: 0, minCardinality: 1,
readablePredicate: "knows".to_string(), readablePredicate: "name".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: Some("http://example.org/ClaireShape".to_string()), shape: None,
}], }],
} }
.into(), .into(),
@ -858,32 +777,10 @@ INSERT DATA {
} }
.into(), .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 { let shape_type = OrmShapeType {
schema, schema,
shape: "http://example.org/AliceShape".to_string(), shape: "http://example.org/HouseShape".to_string(),
}; };
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
@ -891,8 +788,9 @@ INSERT DATA {
.await .await
.expect("orm_start"); .expect("orm_start");
// Get initial state
while let Some(app_response) = receiver.next().await { while let Some(app_response) = receiver.next().await {
let orm_json = match app_response { let _ = match app_response {
AppResponse::V0(v) => match v { AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json), AppResponseV0::OrmInitial(json) => Some(json),
_ => None, _ => None,
@ -900,160 +798,241 @@ INSERT DATA {
} }
.unwrap(); .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; break;
} }
cancel_fn();
}
async fn test_orm_nested_4(session_id: u64) { log_info!(
let doc_nuri = create_doc_with_data( "\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, session_id,
r#" r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
DELETE DATA {
<urn:test:house1> ex:rootColor "blue" .
<urn:test:person1> ex:name "Alice" .
}
;
INSERT DATA { INSERT DATA {
# Valid <urn:test:house1>
<urn:test:alice> ex:rootColor "red" ;
ex:inhabitants <urn:test:person3> .
<urn:test:person1>
ex:name "Alicia" .
<urn:test:person2>
ex:hasCat <urn:test:cat2> .
<urn:test:person3>
a ex:Person ; a ex:Person ;
ex:hasCat <urn:test:kitten1>, <urn:test:kitten2> . ex:name "Charlie" ;
<urn:test:kitten1> ex:hasCat <urn:test:cat3> .
a ex:Cat .
<urn:test:kitten2> <urn:test:cat2>
a ex:Cat . a ex:Cat ;
ex:catName "Mittens" .
<urn:test:cat3>
a ex:Cat ;
ex:catName "Fluffy" .
} }
"# "#
.to_string(), .to_string(),
Some(doc_nuri.clone()),
) )
.await; .await
.expect("INSERT SPARQL update failed");
// Define the ORM schema while let Some(app_response) = receiver.next().await {
let mut schema = HashMap::new(); let patches = match app_response {
schema.insert( AppResponse::V0(v) => match v {
"http://example.org/PersonShape".to_string(), AppResponseV0::OrmUpdate(json) => Some(json),
OrmSchemaShape { _ => None,
iri: "http://example.org/PersonShape".to_string(), },
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: None,
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str(
"http://example.org/Person".to_string(),
)]),
shape: None,
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/hasCat".to_string(),
extra: Some(false),
maxCardinality: -1,
minCardinality: 0,
readablePredicate: "cats".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/CatShape".to_string()),
}],
}
.into(),
],
} }
.into(), .unwrap();
);
schema.insert( log_info!("INSERT patches arrived:\n");
"http://example.org/CatShape".to_string(), for patch in patches.iter() {
OrmSchemaShape { log_info!("{:?}", patch);
iri: "http://example.org/CatShape".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(true),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]),
shape: None,
}],
}
.into()],
} }
.into(),
);
let shape_type = OrmShapeType { let mut expected = json!([
schema, // Modified house color
shape: "http://example.org/PersonShape".to_string(), {
}; "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 actual = json!(patches);
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id) assert_json_eq(&mut expected, &mut actual);
.await
.expect("orm_start"); 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: <http://example.org/>
DELETE DATA {
<urn:test:house1>
ex:rootColor "red" ;
ex:inhabitants <urn:test:person3> .
<urn:test:person1>
ex:hasCat <urn:test:cat1> .
<urn:test:person3>
a ex:Person ;
ex:name "Charlie" ;
ex:hasCat <urn:test:cat3> .
<urn:test:cat1>
a ex:Cat ;
ex:catName "Whiskers" .
<urn:test:cat2>
ex:catName "Mittens" .
<urn:test:cat3>
a ex:Cat ;
ex:catName "Fluffy" .
}
;
INSERT DATA {
<urn:test:cat2>
ex:catName "Mr. Mittens" .
}
"#
.to_string(),
Some(doc_nuri.clone()),
)
.await
.expect("DELETE SPARQL update failed");
while let Some(app_response) = receiver.next().await { while let Some(app_response) = receiver.next().await {
let orm_json = match app_response { let patches = match app_response {
AppResponse::V0(v) => match v { AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json), AppResponseV0::OrmUpdate(json) => Some(json),
_ => None, _ => None,
}, },
} }
.unwrap(); .unwrap();
log_info!("DELETE patches arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
}
let mut expected = json!([ let mut expected = json!([
// Remove house color
{ {
"id": "urn:test:alice", "op": "remove",
"type": "http://example.org/Person", "path": "/urn:test:house1/rootColor",
"cats": { },
"urn:test:kitten1": { // Alice loses her cat
"type": "http://example.org/Cat" {
}, "op": "remove",
"urn:test:kitten2": { "valType": "object",
"type": "http://example.org/Cat" "path": "/urn:test:house1/inhabitants/urn:test:person1/cat",
} },
}, // Bob's cat name changes
} {
"op": "remove",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/name",
},
{
"op": "add",
"value": "Mr. Mittens",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/name",
},
// Charlie and his cat are removed
{
"op": "remove",
"valType": "object",
"path": "/urn:test:house1/inhabitants/urn:test:person3",
},
]); ]);
let mut actual_mut = orm_json.clone(); let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual);
assert_json_eq(&mut expected, &mut actual_mut);
break; break;
} }
cancel_fn();
} }

@ -13,8 +13,8 @@ use crate::tests::{assert_json_eq, create_doc_with_data};
use async_std::stream::StreamExt; use async_std::stream::StreamExt;
use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0};
use ng_net::orm::{ use ng_net::orm::{
BasicType, OrmSchema, OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaPredicate, BasicType, OrmSchema, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType,
OrmSchemaShape, OrmShapeType, OrmShapeType,
}; };
use ng_repo::log_info; use ng_repo::log_info;
@ -304,7 +304,7 @@ INSERT DATA {
minCardinality: 0, minCardinality: 0,
maxCardinality: -1, maxCardinality: -1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
shape: Some("http://example.org/Person".to_string()), shape: Some("http://example.org/Person".to_string()),
literals: None, literals: None,
}], }],
@ -555,7 +555,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/TestObject".to_string(), "http://example.org/TestObject".to_string(),
)]), )]),
@ -566,7 +566,7 @@ INSERT DATA {
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/arr".to_string(), iri: "http://example.org/arr".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -658,7 +658,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "opt".to_string(), readablePredicate: "opt".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::boolean, valType: OrmSchemaValType::boolean,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -741,7 +741,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "lit1".to_string(), readablePredicate: "lit1".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("lit 1".to_string())]), literals: Some(vec![BasicType::Str("lit 1".to_string())]),
shape: None, shape: None,
}], }],
@ -754,7 +754,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "lit2".to_string(), readablePredicate: "lit2".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("lit 2".to_string())]), literals: Some(vec![BasicType::Str("lit 2".to_string())]),
shape: None, shape: None,
}], }],
@ -841,12 +841,12 @@ INSERT DATA {
readablePredicate: "strOrNum".to_string(), readablePredicate: "strOrNum".to_string(),
dataTypes: vec![ dataTypes: vec![
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}, },
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}, },
@ -951,7 +951,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "str".to_string(), readablePredicate: "str".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -964,7 +964,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "nestedWithExtra".to_string(), readablePredicate: "nestedWithExtra".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/NestedShapeWithExtra".to_string()), shape: Some("http://example.org/NestedShapeWithExtra".to_string()),
}], }],
@ -977,7 +977,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "nestedWithoutExtra".to_string(), readablePredicate: "nestedWithoutExtra".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/NestedShapeWithoutExtra".to_string()), shape: Some("http://example.org/NestedShapeWithoutExtra".to_string()),
}], }],
@ -999,7 +999,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1012,7 +1012,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1034,7 +1034,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1047,7 +1047,7 @@ INSERT DATA {
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1147,7 +1147,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "name".to_string(), readablePredicate: "name".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1160,7 +1160,7 @@ INSERT DATA {
minCardinality: 0, minCardinality: 0,
readablePredicate: "knows".to_string(), readablePredicate: "knows".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/PersonShape".to_string()), shape: Some("http://example.org/PersonShape".to_string()),
}], }],
@ -1293,7 +1293,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/Alice".to_string(), "http://example.org/Alice".to_string(),
)]), )]),
@ -1309,12 +1309,12 @@ INSERT DATA {
readablePredicate: "knows".to_string(), readablePredicate: "knows".to_string(),
dataTypes: vec![ dataTypes: vec![
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/BobShape".to_string()), shape: Some("http://example.org/BobShape".to_string()),
}, },
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/ClaireShape".to_string()), shape: Some("http://example.org/ClaireShape".to_string()),
}, },
@ -1337,7 +1337,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Bob".to_string())]), literals: Some(vec![BasicType::Str("http://example.org/Bob".to_string())]),
shape: None, shape: None,
}], }],
@ -1350,7 +1350,7 @@ INSERT DATA {
minCardinality: 0, minCardinality: 0,
readablePredicate: "knows".to_string(), readablePredicate: "knows".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/ClaireShape".to_string()), shape: Some("http://example.org/ClaireShape".to_string()),
}], }],
@ -1371,7 +1371,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/Claire".to_string(), "http://example.org/Claire".to_string(),
)]), )]),
@ -1471,7 +1471,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/Person".to_string(), "http://example.org/Person".to_string(),
)]), )]),
@ -1486,7 +1486,7 @@ INSERT DATA {
minCardinality: 0, minCardinality: 0,
readablePredicate: "cats".to_string(), readablePredicate: "cats".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some("http://example.org/CatShape".to_string()), shape: Some("http://example.org/CatShape".to_string()),
}], }],
@ -1507,7 +1507,7 @@ INSERT DATA {
minCardinality: 1, minCardinality: 1,
readablePredicate: "type".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]), literals: Some(vec![BasicType::Str("http://example.org/Cat".to_string())]),
shape: None, shape: None,
}], }],
@ -1574,7 +1574,7 @@ fn create_big_schema() -> OrmSchema {
predicates: vec![ predicates: vec![
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str( literals: Some(vec![BasicType::Str(
"http://example.org/TestObject".to_string(), "http://example.org/TestObject".to_string(),
)]), )]),
@ -1588,7 +1588,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1600,7 +1600,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1612,7 +1612,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::boolean, valType: OrmSchemaValType::boolean,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1624,7 +1624,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1636,7 +1636,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some( shape: Some(
"http://example.org/TestObject||http://example.org/objectValue" "http://example.org/TestObject||http://example.org/objectValue"
@ -1651,7 +1651,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaValType::shape,
literals: None, literals: None,
shape: Some( shape: Some(
"http://example.org/TestObject||http://example.org/anotherObject" "http://example.org/TestObject||http://example.org/anotherObject"
@ -1667,12 +1667,12 @@ fn create_big_schema() -> OrmSchema {
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![ dataTypes: vec![
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}, },
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}, },
@ -1686,12 +1686,12 @@ fn create_big_schema() -> OrmSchema {
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![ dataTypes: vec![
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("lit1".to_string())]), literals: Some(vec![BasicType::Str("lit1".to_string())]),
shape: None, shape: None,
}, },
OrmSchemaDataType { OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaValType::literal,
literals: Some(vec![BasicType::Str("lit2".to_string())]), literals: Some(vec![BasicType::Str("lit2".to_string())]),
shape: None, shape: None,
}, },
@ -1714,7 +1714,7 @@ fn create_big_schema() -> OrmSchema {
predicates: vec![ predicates: vec![
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1726,7 +1726,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1748,7 +1748,7 @@ fn create_big_schema() -> OrmSchema {
predicates: vec![ predicates: vec![
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string, valType: OrmSchemaValType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1760,7 +1760,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],
@ -1772,7 +1772,7 @@ fn create_big_schema() -> OrmSchema {
}), }),
Arc::new(OrmSchemaPredicate { Arc::new(OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaValType::number,
literals: None, literals: None,
shape: None, shape: None,
}], }],

Loading…
Cancel
Save