Merge remote-tracking branch 'origin/feat/orm-diffs' into refactor

feat/orm-diffs
Niko PLP 23 hours ago
commit c3d480a8c5
  1. 10
      engine/net/src/app_protocol.rs
  2. 14
      engine/net/src/orm.rs
  3. 44
      engine/verifier/src/orm/handle_backend_update.rs
  4. 110
      engine/verifier/src/orm/handle_frontend_update.rs
  5. 4
      engine/verifier/src/orm/materialize.rs
  6. 4
      engine/verifier/src/orm/mod.rs
  7. 2
      engine/verifier/src/orm/process_changes.rs
  8. 2
      engine/verifier/src/orm/query.rs
  9. 2
      engine/verifier/src/orm/utils.rs
  10. 3
      pnpm-lock.yaml
  11. 3
      sdk/js/examples/multi-framework-signals/src/app/pages/index.astro
  12. 2
      sdk/js/lib-wasm/src/lib.rs
  13. 43
      sdk/js/shex-orm/src/schema-converter/util/annotateReadablePredicates.ts
  14. 1
      sdk/js/signals/package.json
  15. 15
      sdk/js/signals/src/connector/createSignalObjectForShape.ts
  16. 4
      sdk/rust/src/local_broker.rs
  17. 4
      sdk/rust/src/tests/mod.rs
  18. 341
      sdk/rust/src/tests/orm_apply_patches.rs
  19. 10
      sdk/rust/src/tests/orm_create_patches.rs
  20. 34
      sdk/rust/src/tests/orm_creation.rs

@ -20,7 +20,7 @@ use ng_repo::utils::{decode_digest, decode_key, decode_sym_key};
use ng_repo::utils::{decode_overlayid, display_timestamp_local};
use serde_json::Value;
use crate::orm::{OrmDiff, OrmShapeType, OrmUpdateBlankNodeIds};
use crate::orm::{OrmPatches, OrmShapeType, OrmUpdateBlankNodeIds};
use crate::types::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -815,7 +815,7 @@ impl AppRequest {
)
}
pub fn new_orm_update(scope: NuriV0, shape_type_name: String, diff: OrmDiff) -> Self {
pub fn new_orm_update(scope: NuriV0, shape_type_name: String, diff: OrmPatches) -> Self {
AppRequest::new(
AppRequestCommandV0::OrmUpdate,
scope,
@ -1056,8 +1056,8 @@ pub enum AppRequestPayloadV0 {
QrCodeProfile(u32),
QrCodeProfileImport(String),
OrmStart(OrmShapeType),
OrmUpdate((OrmDiff, String)), // ShapeID
OrmStop(String), //ShapeID
OrmUpdate((OrmPatches, String)), // ShapeID
OrmStop(String), //ShapeID
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -1308,7 +1308,7 @@ pub enum AppResponseV0 {
Header(AppHeader),
Commits(Vec<String>),
OrmInitial(Value),
OrmUpdate(OrmDiff),
OrmUpdate(OrmPatches),
OrmUpdateBlankNodeIds(OrmUpdateBlankNodeIds),
OrmError(String),
}

@ -23,30 +23,30 @@ pub struct OrmShapeType {
pub shape: String,
}
/* == Diff Types == */
/* == Patch Types == */
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum OrmDiffOpType {
pub enum OrmPatchOp {
add,
remove,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum OrmDiffType {
pub enum OrmPatchType {
set,
object,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmDiffOp {
pub op: OrmDiffOpType,
pub valType: Option<OrmDiffType>,
pub struct OrmPatch {
pub op: OrmPatchOp,
pub valType: Option<OrmPatchType>,
pub path: String,
pub value: Option<Value>, // TODO: Improve type
}
pub type OrmDiff = Vec<OrmDiffOp>;
pub type OrmPatches = Vec<OrmPatch>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmUpdateBlankNodeId {

@ -11,7 +11,7 @@ use std::collections::HashMap;
use std::u64;
use futures::SinkExt;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_net::{app_protocol::*, orm::*};
use ng_repo::log::*;
@ -132,7 +132,7 @@ impl Verifier {
);
// The JSON patches to send to JS land.
let mut patches: Vec<OrmDiffOp> = vec![];
let mut patches: Vec<OrmPatch> = vec![];
// Keep track of objects to create: (path, Option<IRI>)
// The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers)
@ -199,7 +199,7 @@ impl Verifier {
&sub.tracked_subjects,
&sub.shape_type.shape,
&mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None),
(OrmPatchOp::remove, Some(OrmPatchType::object), None, None),
&mut patches,
&mut objects_to_create,
);
@ -255,19 +255,19 @@ impl Verifier {
let json_pointer = format!("/{}", escaped_path.join("/"));
// Always create the object itself
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
patches.push(OrmPatch {
op: OrmPatchOp::add,
valType: Some(OrmPatchType::object),
path: json_pointer.clone(),
value: None,
});
// If this object has an IRI (it's a real subject), add the id field
if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
patches.push(OrmPatch {
op: OrmPatchOp::add,
valType: None,
path: format!("{}/id", json_pointer),
path: format!("{}/@id", json_pointer),
value: Some(json!(iri)),
});
}
@ -291,7 +291,7 @@ fn queue_patches_for_newly_valid_subject(
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
root_shape: &String,
path: &[String],
patches: &mut Vec<OrmDiffOp>,
patches: &mut Vec<OrmPatch>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
// Check if we're at a root subject or need to traverse to parents
@ -435,12 +435,12 @@ fn build_path_to_root_and_create_patches(
root_shape: &String,
path: &mut Vec<String>,
diff_op: (
OrmDiffOpType,
Option<OrmDiffType>,
OrmPatchOp,
Option<OrmPatchType>,
Option<Value>, // The value added / removed
Option<String>, // The IRI, if change is an added / removed object.
),
patches: &mut Vec<OrmDiffOp>,
patches: &mut Vec<OrmPatch>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
log_debug!(
@ -465,7 +465,7 @@ fn build_path_to_root_and_create_patches(
);
// Create the patch for the actual value change
patches.push(OrmDiffOp {
patches.push(OrmPatch {
op: diff_op.0.clone(),
valType: diff_op.1.clone(),
path: json_pointer.clone(),
@ -515,8 +515,8 @@ fn build_path_to_root_and_create_patches(
fn create_diff_ops_from_predicate_change(
pred_change: &OrmTrackedPredicateChanges,
) -> Vec<(
OrmDiffOpType,
Option<OrmDiffType>,
OrmPatchOp,
Option<OrmPatchType>,
Option<Value>, // The value added / removed
Option<String>, // The IRI, if change is an added / removed object.
)> {
@ -537,7 +537,7 @@ fn create_diff_ops_from_predicate_change(
// A value was added. Another one might have been removed
// but the add patch overwrite previous values.
return [(
OrmDiffOpType::add,
OrmPatchOp::add,
None,
Some(json!(pred_change.values_added[0])),
None,
@ -545,21 +545,21 @@ fn create_diff_ops_from_predicate_change(
.to_vec();
} else {
// Since there is only one possible value, removing the path is enough.
return [(OrmDiffOpType::remove, None, None, None)].to_vec();
return [(OrmPatchOp::remove, None, None, None)].to_vec();
}
} else if is_multi && !is_object {
if pred_change.values_added.len() > 0 {
ops.push((
OrmDiffOpType::add,
Some(OrmDiffType::set),
OrmPatchOp::add,
Some(OrmPatchType::set),
Some(json!(pred_change.values_added)),
None,
));
}
if pred_change.values_removed.len() > 0 {
ops.push((
OrmDiffOpType::remove,
Some(OrmDiffType::set),
OrmPatchOp::remove,
Some(OrmPatchType::set),
Some(json!(pred_change.values_removed)),
None,
));

@ -7,20 +7,20 @@
// notice may not be copied, modified, or distributed except
// according to those terms.
use ng_net::orm::{OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaPredicate, OrmSchemaShape};
use ng_net::orm::{OrmPatch, OrmPatchOp, OrmPatchType, OrmSchemaPredicate, OrmSchemaShape};
use ng_oxigraph::oxrdf::Quad;
use ng_repo::errors::VerifierError;
use std::sync::{Arc, RwLock};
use std::u64;
use futures::SinkExt;
use ng_net::app_protocol::*;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_repo::log::*;
use crate::orm::types::*;
use crate::orm::utils::{decode_json_pointer, json_to_sparql_val};
use crate::types::GraphQuadsPatch;
use crate::verifier::*;
impl Verifier {
@ -34,25 +34,22 @@ impl Verifier {
shape_iri: ShapeIri,
session_id: u64,
_skolemnized_blank_nodes: Vec<Quad>,
_revert_inserts: Vec<Quad>,
_revert_removes: Vec<Quad>,
revert_inserts: Vec<Quad>,
revert_removes: Vec<Quad>,
) -> Result<(), VerifierError> {
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
// use orm_subscription if needed
// note(niko): I think skolemnized blank nodes can still be many, in case of multi-level nested sub-objects.
let orm_bnids = vec![];
let _ = sender
.send(AppResponse::V0(AppResponseV0::OrmUpdateBlankNodeIds(
orm_bnids,
)))
.await;
// Revert changes, if there.
if revert_inserts.len() > 0 || revert_removes.len() > 0 {
let revert_changes = GraphQuadsPatch {
inserts: revert_removes,
removes: revert_inserts,
};
// TODO (later) revert the inserts and removes
// let orm_diff = vec![];
// let _ = sender.send(AppResponse::V0(AppResponseV0::OrmUpdate(orm_diff))).await;
// TODO: Call with correct params.
// self.orm_backend_update(session_id, scope, "", revert_changes)
}
Ok(())
}
@ -63,7 +60,7 @@ impl Verifier {
session_id: u64,
scope: &NuriV0,
shape_iri: ShapeIri,
diff: OrmDiff,
diff: OrmPatches,
) -> Result<(), String> {
log_info!(
"frontend_update_orm session={} shape={} diff={:?}",
@ -118,21 +115,21 @@ impl Verifier {
fn create_sparql_update_query_for_diff(
orm_subscription: &OrmSubscription,
diff: OrmDiff,
diff: OrmPatches,
) -> 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)
.filter(|patch| patch.op == OrmPatchOp::remove)
.collect();
let add_object_patches: Vec<_> = diff
.iter()
.filter(|patch| {
patch.op == OrmDiffOpType::add
patch.op == OrmPatchOp::add
&& match &patch.valType {
Some(vt) => *vt == OrmDiffType::object,
Some(vt) => *vt == OrmPatchType::object,
_ => false,
}
})
@ -140,9 +137,9 @@ fn create_sparql_update_query_for_diff(
let add_literal_patches: Vec<_> = diff
.iter()
.filter(|patch| {
patch.op == OrmDiffOpType::add
patch.op == OrmPatchOp::add
&& match &patch.valType {
Some(vt) => *vt != OrmDiffType::object,
Some(vt) => *vt != OrmPatchType::object,
_ => true,
}
})
@ -289,7 +286,7 @@ fn find_pred_schema_by_name(
/// - 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,
patch: &OrmPatch,
var_counter: &mut i32,
orm_subscription: &OrmSubscription,
) -> (
@ -334,6 +331,7 @@ 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);
// Case: We arrived at a leaf value.
if path.len() == 0 {
return (
where_statements,
@ -342,6 +340,8 @@ fn create_where_statements_for_patch(
);
}
// Else, we have a nested object.
where_statements.push(format!(
"{} <{}> ?o{}",
subject_ref, pred_schema.iri, var_counter,
@ -369,7 +369,7 @@ fn create_where_statements_for_patch(
}
current_subj_schema =
get_first_valid_child_schema(&object_iri, &pred_schema, &orm_subscription);
get_first_child_schema(Some(&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}>");
@ -379,15 +379,18 @@ fn create_where_statements_for_patch(
// 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);
current_subj_schema = get_first_child_schema(None, &pred_schema, &orm_subscription);
}
}
// Can't happen.
panic!();
}
fn get_first_valid_child_schema(
subject_iri: &String,
/// Get the schema for a given subject and predicate schema.
/// It will return the first schema of which the tracked subject is valid.
/// If there is no subject found, return the first subject schema of the predicate schema.
fn get_first_child_schema(
subject_iri: Option<&String>,
pred_schema: &OrmSchemaPredicate,
orm_subscription: &OrmSubscription,
) -> Arc<OrmSchemaShape> {
@ -396,18 +399,31 @@ fn get_first_valid_child_schema(
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)
let tracked_subject_opt = subject_iri
.and_then(|iri| orm_subscription.tracked_subjects.get(iri))
.and_then(|ts_shapes| ts_shapes.get(schema_shape));
if let Some(tracked_subject) = tracked_subject_opt {
// The subject is already being tracked (it's not new).
if tracked_subject.read().unwrap().valid == OrmTrackedSubjectValidity::Valid {
return orm_subscription
.shape_type
.schema
.get(schema_shape)
.unwrap()
.clone();
}
} else {
// New subject, we need to guess the schema, take the first one.
// TODO: We could do that by looking at a distinct property, e.g. @type which must be non-overlapping.
return pred_schema
.dataTypes
.iter()
.find_map(|dt| {
dt.shape
.as_ref()
.and_then(|shape_str| orm_subscription.shape_type.schema.get(shape_str))
})
.unwrap()
.clone();
}
@ -415,15 +431,3 @@ fn get_first_valid_child_schema(
// 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();
}

@ -9,7 +9,7 @@
use futures::SinkExt;
use ng_net::orm::*;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_net::utils::Receiver;
use serde_json::json;
use serde_json::Value;
@ -141,7 +141,7 @@ pub(crate) fn materialize_orm_object(
shape: &OrmSchemaShape,
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
) -> Value {
let mut orm_obj = json!({"id": change.subject_iri});
let mut orm_obj = json!({"@id": change.subject_iri});
let orm_obj_map = orm_obj.as_object_mut().unwrap();
for pred_schema in &shape.predicates {
let property_name = &pred_schema.readablePredicate;

@ -17,13 +17,13 @@ pub mod shape_validation;
pub mod types;
pub mod utils;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use crate::orm::types::*;
use crate::verifier::*;
impl Verifier {
pub(crate) fn clean_orm_subscriptions(&mut self) {
pub(crate) fn _clean_orm_subscriptions(&mut self) {
self.orm_subscriptions.retain(|_, subscriptions| {
subscriptions.retain(|sub| !sub.sender.is_closed());
!subscriptions.is_empty()

@ -15,7 +15,7 @@ use std::collections::HashSet;
use std::sync::Arc;
use std::u64;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_net::{app_protocol::*, orm::*};
use ng_oxigraph::oxrdf::Triple;
use ng_repo::errors::NgError;

@ -11,7 +11,7 @@ use ng_repo::errors::VerifierError;
use std::collections::HashSet;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use crate::orm::types::*;
use crate::orm::utils::{escape_literal, is_iri};

@ -16,7 +16,7 @@ use std::collections::HashSet;
use lazy_static::lazy_static;
use regex::Regex;
pub use ng_net::orm::{OrmDiff, OrmShapeType};
pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_net::{app_protocol::*, orm::*};
use ng_oxigraph::oxrdf::Triple;

@ -877,6 +877,9 @@ importers:
'@ng-org/alien-deepsignals':
specifier: workspace:*
version: link:../alien-deepsignals
'@ng-org/lib-wasm':
specifier: workspace:*
version: link:../lib-wasm/pkg
'@ng-org/shex-orm':
specifier: workspace:*
version: link:../shex-orm

@ -6,9 +6,6 @@ import VueRoot from "../components/VueRoot.vue";
import ReactRoot from "../components/ReactRoot";
import SvelteRoot from "../components/SvelteRoot.svelte";
// Hack to get mock backend started
//import { mockTestObject } from "../../ng-mock/wasm-land/shapeHandler";
const title = "Multi-framework app";
---

@ -58,7 +58,7 @@ use ng_wallet::types::*;
use ng_wallet::*;
use nextgraph::local_broker::*;
use nextgraph::verifier::orm::{OrmDiff, OrmShapeType};
use nextgraph::verifier::orm::{OrmPatches, OrmShapeType};
use nextgraph::verifier::CancelFn;
use crate::model::*;

@ -33,25 +33,40 @@ export default function annotateReadablePredicates(schema: Schema): void {
if (tcs.length > 0) {
// Group by local token (last segment of IRI) and set a base readablePredicate for all
const groups = new Map<string, TCwReadable[]>();
for (const tc of tcs) {
const tokens = splitIriTokens(tc.predicate);
const local = tokens.length
? tokens[tokens.length - 1]
: tc.predicate;
const readableNameToPredicatesMap = new Map<
string,
TCwReadable[]
>();
for (const tripleConstraint of tcs) {
// Use the name based on the IRI ending.
let readableName: string;
// Special case rdfs:type => @type
if (
tripleConstraint.predicate ===
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
) {
readableName = "@type";
} else {
const tokens = splitIriTokens(tripleConstraint.predicate);
readableName = tokens.length
? tokens[tokens.length - 1]
: tripleConstraint.predicate;
}
// default base name for non-colliders
tc.readablePredicate = local;
const arr = groups.get(local) ?? [];
arr.push(tc);
groups.set(local, arr);
tripleConstraint.readablePredicate = readableName;
const groupMembers =
readableNameToPredicatesMap.get(readableName) ?? [];
groupMembers.push(tripleConstraint);
readableNameToPredicatesMap.set(readableName, groupMembers);
}
// Resolve each group (rename all in collisions)
for (const [, arr] of groups) {
if (arr.length <= 1) continue;
for (const [, groupMembers] of readableNameToPredicatesMap) {
if (groupMembers.length <= 1) continue;
const used = new Set<string>();
const local =
splitIriTokens(arr[0].predicate).slice(-1)[0] ?? "";
for (const tc of arr) {
splitIriTokens(groupMembers[0].predicate).slice(-1)[0] ??
"";
for (const tc of groupMembers) {
const tokens = splitIriTokens(tc.predicate);
let localIdx = tokens.lastIndexOf(local);
if (localIdx === -1)

@ -62,6 +62,7 @@
"vue": "3.5.19"
},
"devDependencies": {
"@ng-org/lib-wasm": "workspace:*",
"@playwright/test": "^1.55.0",
"@types/node": "24.3.0",
"@types/react": "19.1.10",

@ -1,17 +1,10 @@
import type { Diff, Scope } from "../types.js";
import { applyDiff } from "./applyDiff.js";
import ng from "@ng-org/lib-wasm";
import {
deepSignal,
watch,
batch,
} from "@ng-org/alien-deepsignals";
import type {
DeepPatch,
DeepSignalObject,
} from "@ng-org/alien-deepsignals";
import type * as NG from "@ng-org/lib-wasm";
import { deepSignal, watch, batch } from "@ng-org/alien-deepsignals";
import type { DeepPatch, DeepSignalObject } from "@ng-org/alien-deepsignals";
import type { ShapeType, BaseType } from "@ng-org/shex-orm";
interface PoolEntry<T extends BaseType> {

@ -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::{OrmDiff, OrmShapeType};
use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_oxigraph::oxrdf::Triple;
use once_cell::sync::Lazy;
use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
@ -2767,7 +2767,7 @@ pub async fn orm_start(
pub async fn orm_update(
scope: NuriV0,
shape_type_name: String,
diff: OrmDiff,
diff: OrmPatches,
session_id: u64,
) -> Result<(), NgError> {
let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff);

@ -86,14 +86,14 @@ fn sort_arrays(value: &mut Value) {
}
}
/// Helper to recursively remove nested "id" fields from nested objects,
/// Helper to recursively remove nested "@id" fields from nested objects,
/// but only if they are not at the root level.
fn remove_id_fields(value: &mut Value) {
fn remove_id_fields_inner(value: &mut Value, is_root: bool) {
match value {
Value::Object(map) => {
if !is_root {
map.remove("id");
map.remove("@id");
}
for v in map.values_mut() {
remove_id_fields_inner(v, false);

@ -13,7 +13,7 @@ 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,
BasicType, OrmPatch, OrmPatchOp, OrmPatchType, OrmSchemaDataType, OrmSchemaPredicate,
OrmSchemaShape, OrmSchemaValType, OrmShapeType,
};
@ -44,11 +44,17 @@ async fn test_orm_apply_patches() {
// Test 5: Remove from multi-value literal array
test_patch_remove_from_array(session_id).await;
// // Test 6: Nested object - modify nested literal
// 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 8: Root object creation
test_patch_create_root_object(session_id).await;
// Test 9: Nested object creation
test_patch_create_nested_object(session_id).await;
}
/// Test adding a single literal value via ORM patch
@ -122,8 +128,8 @@ INSERT DATA {
}
// Apply ORM patch: Add name
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::add,
let diff = vec![OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person1/name".to_string(),
valType: None,
value: Some(json!("Alice")),
@ -221,8 +227,8 @@ INSERT DATA {
}
// Apply ORM patch: Remove name
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::remove,
let diff = vec![OrmPatch {
op: OrmPatchOp::remove,
path: "urn:test:person2/name".to_string(),
valType: None,
value: Some(json!("Bob")),
@ -327,8 +333,8 @@ INSERT DATA {
// valType: None,
// value: Some(json!("Charlie")),
// },
OrmDiffOp {
op: OrmDiffOpType::add,
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person3/name".to_string(),
valType: None,
value: Some(json!("Charles")),
@ -434,9 +440,9 @@ INSERT DATA {
}
// Apply ORM patch: Add hobby
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::set),
let diff = vec![OrmPatch {
op: OrmPatchOp::add,
valType: Some(OrmPatchType::set),
path: "urn:test:person4/hobby".to_string(),
value: Some(json!("Swimming")),
}];
@ -535,8 +541,8 @@ INSERT DATA {
}
// Apply ORM patch: Remove hobby
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::remove,
let diff = vec![OrmPatch {
op: OrmPatchOp::remove,
path: "urn:test:person5/hobby".to_string(),
valType: None,
value: Some(json!("Swimming")),
@ -704,8 +710,8 @@ INSERT DATA {
}
// Apply ORM patch: Change city in nested address
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::add,
let diff = vec![OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person6/address/city".to_string(),
valType: None,
value: Some(json!("Shelbyville")),
@ -923,8 +929,8 @@ INSERT DATA {
}
// Apply ORM patch: Change street in company's headquarter address (3 levels deep)
let diff = vec![OrmDiffOp {
op: OrmDiffOpType::add,
let diff = vec![OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person7/company/urn:test:company1/headquarter/street".to_string(),
valType: None,
value: Some(json!("Rich Street")),
@ -957,3 +963,304 @@ INSERT DATA {
log_info!("✓ Test passed: Multi-level nested modification");
}
/// Test multi-level nested object modifications via ORM patch
async fn test_patch_create_root_object(session_id: u64) {
log_info!("\n\n=== TEST: Creation of root object ===\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" .
}
"#
.to_string(),
)
.await;
let mut schema = HashMap::new();
schema.insert(
"http://example.org/PersonShape".to_string(),
Arc::new(OrmSchemaShape {
iri: "http://example.org/PersonShape".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/PersonShape".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: Create a new object
let diff = vec![
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person8".to_string(),
valType: Some(OrmPatchType::object),
value: None,
},
OrmPatch {
// This does nothing as it does not represent a triple.
// A subject is created when inserting data.
op: OrmPatchOp::add,
path: "urn:test:person8/@id".to_string(),
valType: Some(OrmPatchType::object),
value: None,
},
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person8/type".to_string(),
valType: None,
value: Some(json!("http://example.org/Person")),
},
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person8/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.to_string() == "urn:test:person8"
&& t.predicate.as_str() == "http://example.org/name"
&& t.object.to_string().contains("Alice")
});
let has_type = triples.iter().any(|t| {
t.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
&& t.object.to_string().contains("http://example.org/Person")
});
assert!(!has_name, "New person has name");
assert!(has_type, "New person has type");
log_info!("✓ Test passed: Creation of root object");
}
/// Test adding a second address object.
async fn test_patch_create_nested_object(session_id: u64) {
log_info!("\n\n=== TEST: Nested object creation ===\n");
let doc_nuri = create_doc_with_data(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:person9> 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: 2,
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,
}],
}),
],
}),
);
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: Add a second address.
let diff = vec![
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/type".to_string(),
valType: None,
value: Some(json!("http://example.org/Address")),
},
OrmPatch {
op: OrmPatchOp::add,
path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/street"
.to_string(),
valType: None,
value: Some(json!("Heaven Avenue")),
},
];
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_new_address_type = triples.iter().any(|t| {
t.subject
.to_string()
.contains("http://example.org/exampleAddress")
&& t.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
&& t.object.to_string().contains("http://example.org/Address")
});
let has_new_address_street = triples.iter().any(|t| {
t.subject
.to_string()
.contains("http://example.org/exampleAddress")
&& t.predicate.as_str() == "http://example.org/street"
&& t.object.to_string().contains("Heaven Avenue")
});
assert!(has_new_address_type, "New address type should be added");
assert!(has_new_address_street, "New street should be added");
log_info!("✓ Test passed: Nested object creation");
}

@ -209,7 +209,7 @@ INSERT DATA {
{
"op": "add",
"value": "urn:test:numArrayObj4",
"path": "/urn:test:numArrayObj4/id",
"path": "/urn:test:numArrayObj4/@id",
"valType": Value::Null,
},
{
@ -575,7 +575,7 @@ INSERT DATA {
"op": "add",
// "valType": None,
"value": "urn:test:multiNested4",
"path": "/urn:test:oj1/multiNest/urn:test:multiNested4/id",
"path": "/urn:test:oj1/multiNest/urn:test:multiNested4/@id",
},
{
"op": "add",
@ -882,7 +882,7 @@ INSERT DATA {
{
"op": "add",
"value": "urn:test:cat2",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/id",
"path": "/urn:test:house1/inhabitants/urn:test:person2/cat/@id",
},
{
"op": "add",
@ -903,7 +903,7 @@ INSERT DATA {
{
"op": "add",
"value": "urn:test:person3",
"path": "/urn:test:house1/inhabitants/urn:test:person3/id",
"path": "/urn:test:house1/inhabitants/urn:test:person3/@id",
},
{
"op": "add",
@ -923,7 +923,7 @@ INSERT DATA {
{
"op": "add",
"value": "urn:test:cat3",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat/id",
"path": "/urn:test:house1/inhabitants/urn:test:person3/cat/@id",
},
{
"op": "add",

@ -448,7 +448,7 @@ INSERT DATA {
let mut expected = json!([{
"type":"http://example.org/TestObject",
"id":"urn:test:obj1",
"@id":"urn:test:obj1",
"anotherObject":{
"urn:test:id1":{
"prop1":"one",
@ -465,7 +465,7 @@ INSERT DATA {
"numOrStr":"either",
"numValue":42.0,
"objectValue":{
"id":"urn:test:id3",
"@id":"urn:test:id3",
"nestedArray":[5.0,6.0],
"nestedNum":7.0,
"nestedString":"nested"
@ -473,7 +473,7 @@ INSERT DATA {
"stringValue": "hello world",
},
{
"id":"urn:test:obj2",
"@id":"urn:test:obj2",
"type":"http://example.org/TestObject",
"anotherObject": {
"urn:test:id4":{
@ -491,7 +491,7 @@ INSERT DATA {
"numOrStr":4.0,
"numValue":422.0,
"objectValue":{
"id":"urn:test:id6",
"@id":"urn:test:id6",
"nestedArray": [7.0,8.0,9.0],
"nestedNum":72.0,
"nestedString":"nested2"
@ -602,17 +602,17 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:numArrayObj1",
"@id": "urn:test:numArrayObj1",
"type": "http://example.org/TestObject",
"numArray": [1.0, 2.0, 3.0]
},
{
"id": "urn:test:numArrayObj2",
"@id": "urn:test:numArrayObj2",
"type": "http://example.org/TestObject",
"numArray": []
},
{
"id": "urn:test:numArrayObj3",
"@id": "urn:test:numArrayObj3",
"type": "http://example.org/TestObject",
"numArray": [1.0, 2.0]
}
@ -689,7 +689,7 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:oj1",
"@id": "urn:test:oj1",
"opt": true
}
]);
@ -786,12 +786,12 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:oj1",
"@id": "urn:test:oj1",
"lit1": ["lit 1"],
"lit2": "lit 2"
},
{
"id": "urn:test:obj2",
"@id": "urn:test:obj2",
"lit1": ["lit 1", "lit 1 extra"],
"lit2": "lit 2"
}
@ -878,7 +878,7 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:oj1",
"@id": "urn:test:oj1",
"strOrNum": ["a string", "another string", 2.0]
}
]);
@ -1079,7 +1079,7 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:oj1",
"@id": "urn:test:oj1",
"str": "obj1 str",
"nestedWithExtra": {
"nestedStr": "obj1 nested with extra valid",
@ -1199,7 +1199,7 @@ INSERT DATA {
// claire2 is invalid (missing name), so alice2's knows chain is incomplete
let mut expected = json!([
{
"id": "urn:test:alice",
"@id": "urn:test:alice",
"name": "Alice",
"knows": {
"urn:test:bob": {
@ -1218,7 +1218,7 @@ INSERT DATA {
}
},
{
"id": "urn:test:bob",
"@id": "urn:test:bob",
"name": "Bob",
"knows": {
"urn:test:claire": {
@ -1228,7 +1228,7 @@ INSERT DATA {
}
},
{
"id": "urn:test:claire",
"@id": "urn:test:claire",
"name": "Claire",
"knows": {}
}
@ -1411,7 +1411,7 @@ INSERT DATA {
// alice2 is incomplete because claire2 has wrong type
let mut expected = json!([
{
"id": "urn:test:alice",
"@id": "urn:test:alice",
"type": "http://example.org/Alice",
"knows": {
"urn:test:bob": {
@ -1538,7 +1538,7 @@ INSERT DATA {
let mut expected = json!([
{
"id": "urn:test:alice",
"@id": "urn:test:alice",
"type": "http://example.org/Person",
"cats": {
"urn:test:kitten1": {

Loading…
Cancel
Save