working JSON Patch -> SPARQL

feat/orm-diffs
Laurin Weger 2 days ago
parent 318fcf5624
commit 1d3730497b
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 2
      engine/net/src/orm.rs
  2. 227
      engine/verifier/src/orm/handle_frontend_update.rs
  3. 14
      sdk/rust/src/local_broker.rs
  4. 3
      sdk/rust/src/tests/mod.rs
  5. 959
      sdk/rust/src/tests/orm_apply_patches.rs
  6. 8
      sdk/rust/src/tests/orm_create_patches.rs

@ -31,7 +31,7 @@ pub enum OrmDiffOpType {
remove,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum OrmDiffType {
set,

@ -11,8 +11,6 @@ use ng_net::orm::{OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaPredicate, Orm
use ng_oxigraph::oxrdf::Quad;
use ng_repo::errors::VerifierError;
use std::cmp::Ordering;
use std::fmt::format;
use std::sync::{Arc, RwLock};
use std::u64;
@ -35,11 +33,11 @@ impl Verifier {
scope: &NuriV0,
shape_iri: ShapeIri,
session_id: u64,
skolemnized_blank_nodes: Vec<Quad>,
revert_inserts: Vec<Quad>,
revert_removes: Vec<Quad>,
_skolemnized_blank_nodes: Vec<Quad>,
_revert_inserts: Vec<Quad>,
_revert_removes: Vec<Quad>,
) -> Result<(), VerifierError> {
let (mut sender, orm_subscription) =
let (mut sender, _orm_subscription) =
self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?;
// TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes
@ -68,9 +66,8 @@ impl Verifier {
diff: OrmDiff,
) -> Result<(), String> {
log_info!(
"frontend_update_orm session={} scope={:?} shape={} diff={:?}",
"frontend_update_orm session={} shape={} diff={:?}",
session_id,
scope,
shape_iri,
diff
);
@ -85,6 +82,7 @@ impl Verifier {
(doc_nuri, sparql_update)
};
log_debug!("Created SPARQL query for patches:\n{}", sparql_update);
match self
.process_sparql_update(
&doc_nuri,
@ -125,77 +123,138 @@ fn create_sparql_update_query_for_diff(
// First sort patches.
// - Process delete patches first.
// - Process object creation add operations before rest, to ensure potential blank nodes are created.
let mut delete_patches: Vec<_> = diff
let delete_patches: Vec<_> = diff
.iter()
.filter(|patch| patch.op == OrmDiffOpType::remove)
.collect();
let mut add_patches: Vec<_> = diff
let add_object_patches: Vec<_> = diff
.iter()
.filter(|patch| patch.op == OrmDiffOpType::add)
.filter(|patch| {
patch.op == OrmDiffOpType::add
&& match &patch.valType {
Some(vt) => *vt == OrmDiffType::object,
_ => false,
}
})
.collect();
// Put Object creations first and...
add_patches.sort_by(|patch1, patch2| match patch1.valType {
Some(OrmDiffType::object) => Ordering::Less,
_ => Ordering::Equal,
});
// ...shorter paths first
add_patches.sort_by(|patch1, patch2| {
patch1
.path
.split("/")
.count()
.cmp(&patch2.path.split("/").count())
});
// Use a counter to generate unique variable names.
fn get_new_var_name(counter: &mut i32) -> String {
let name = format!("v{}", counter);
*counter += 1;
name
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 sparql_sub_queries: Vec<String> = vec![];
let mut sparql_sub_queries: Vec<String> = vec![];
// Create delete statements.
let delete_statements: Vec<String> = vec![]; // The parts in the Delete block.
//
for del_patch in delete_patches.iter() {
let mut var_counter: i32 = 0;
let (where_statements, target) =
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 {
let delete_val = match del_patch.value {
// 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 => {
let val = format!("?{}", var_counter);
var_counter += 1;
val
format!("?{}", var_counter)
// Note: var_counter is not incremented here as it's only used locally
}
Some(val) => json_to_sparql_val(&val),
// 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)
delete_statement = format!(" {} <{}> {} .", subject_var, target_predicate, delete_val);
}
sparql_sub_queries.push(format!(
"DELETE DATA {{\n{}\nWHERE\n{{\n{}\n}}",
"DELETE {{\n{}\n}}\nWHERE\n{{\n {}\n}}",
delete_statement,
where_statements.join("\n ")
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 "None";
return sparql_sub_queries.join(";\n");
}
fn get_tracked_subject_from_diff_op(
fn _get_tracked_subject_from_diff_op(
subject_iri: &String,
orm_subscription: &OrmSubscription,
) -> Arc<RwLock<OrmTrackedSubject>> {
@ -225,13 +284,19 @@ fn find_pred_schema_by_name(
}
/// Creates sparql WHERE statements to navigate to the JSON pointer path in our ORM mapping.
/// Returns the statements as Vec<String>
/// and the subject, predicate, Option<Object> of the path's ending (to be used for DELETE / DELETE).
/// 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>)) {
) -> (
Vec<String>,
(String, String, Option<String>),
Option<Arc<OrmSchemaPredicate>>,
) {
let mut body_statements: Vec<String> = vec![];
let mut where_statements: Vec<String> = vec![];
@ -242,13 +307,18 @@ fn create_where_statements_for_patch(
.collect();
// Handle special case: The whole object is deleted.
if path.len() == 0 {
let mut root_iri = path.remove(0);
body_statements.push(format!("<{}> ?p ?o .", root_iri));
where_statements.push(format!("<{}> ?p ?o .", root_iri));
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 mut subj_schema: &Arc<OrmSchemaShape> = orm_subscription
let subj_schema: &Arc<OrmSchemaShape> = orm_subscription
.shape_type
.schema
.get(&orm_subscription.shape_type.shape)
@ -264,44 +334,59 @@ fn create_where_statements_for_patch(
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{} .",
"{} <{}> ?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_multi() && pred_schema.is_object() {
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_subject_schema(&object_iri, &pred_schema, &orm_subscription);
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.
// And can clear all, now unnecessary where statements.
where_statements.clear();
}
if path.len() == 0 {
return (
where_statements,
(subject_ref, pred_schema.iri.clone(), None),
);
} 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_subject_schema(
fn get_first_valid_child_schema(
subject_iri: &String,
pred_schema: &OrmSchemaPredicate,
orm_subscription: &OrmSubscription,
@ -328,5 +413,17 @@ fn get_first_valid_subject_schema(
}
}
// TODO: Panicking might be too aggressive.
panic!();
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();
}

@ -17,7 +17,7 @@ use async_std::sync::{Arc, Condvar, Mutex, RwLock};
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use lazy_static::lazy_static;
use ng_net::orm::OrmShapeType;
use ng_net::orm::{OrmDiff, OrmShapeType};
use ng_oxigraph::oxrdf::Triple;
use once_cell::sync::Lazy;
use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
@ -2764,6 +2764,18 @@ pub async fn orm_start(
app_request_stream(request).await
}
pub async fn orm_update(
scope: NuriV0,
shape_type_name: String,
diff: OrmDiff,
session_id: u64,
) -> Result<(), NgError> {
let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff);
request.set_session_id(session_id);
app_request(request).await?;
Ok(())
}
pub async fn doc_sparql_construct(
session_id: u64,
sparql: String,

@ -15,8 +15,9 @@ use crate::local_broker::{doc_create, doc_sparql_update};
#[doc(hidden)]
pub mod orm_creation;
pub mod orm_apply_patches;
#[doc(hidden)]
pub mod orm_patches;
pub mod orm_create_patches;
#[doc(hidden)]
pub mod create_or_open_wallet;

@ -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");
}

@ -13,7 +13,7 @@ use crate::tests::{assert_json_eq, create_doc_with_data};
use async_std::stream::StreamExt;
use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0};
use ng_net::orm::{
BasicType, OrmSchemaDataType, OrmSchemaValType, OrmSchemaPredicate, OrmSchemaShape,
BasicType, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType,
OrmShapeType,
};
@ -604,8 +604,6 @@ INSERT DATA {
}
}
// Temporary file - content to be appended to orm_patches.rs
/// Test nested modifications with House -> Person -> Cat hierarchy
async fn test_patch_nested_house_inhabitants(session_id: u64) {
let doc_nuri = create_doc_with_data(
@ -804,7 +802,7 @@ INSERT DATA {
}
log_info!(
"\n=== TEST 1: INSERT - Adding new person with cat, modifying existing properties ===\n"
"\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
@ -945,7 +943,7 @@ INSERT DATA {
break;
}
log_info!("\n=== TEST 2: DELETE - Removing cat, person, and modifying properties ===\n");
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(
Loading…
Cancel
Save