From b8f14ed5613ba14f5221027d094ea1644cefc924 Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Sat, 25 Oct 2025 18:26:18 +0200 Subject: [PATCH] select query working for current triple setup and tests --- engine/verifier/src/orm/initialize.rs | 20 +- engine/verifier/src/orm/process_changes.rs | 1 + engine/verifier/src/orm/query.rs | 207 +++++++++++++++++---- sdk/rust/src/local_broker.rs | 19 +- sdk/rust/src/tests/orm_creation.rs | 61 ++++-- 5 files changed, 244 insertions(+), 64 deletions(-) diff --git a/engine/verifier/src/orm/initialize.rs b/engine/verifier/src/orm/initialize.rs index 4cb2bfc0..1f1ce0be 100644 --- a/engine/verifier/src/orm/initialize.rs +++ b/engine/verifier/src/orm/initialize.rs @@ -84,8 +84,12 @@ impl Verifier { shape_type: &OrmShapeType, ) -> Result { // Query triples for this shape - let shape_query = shape_type_to_sparql_select(&shape_type.schema, &shape_type.shape, None)?; - let shape_triples = self.query_sparql_select(shape_query, Some(nuri_to_string(nuri)))?; + let shape_triples = self.query_quads_for_shape_type( + Some(nuri_to_string(nuri)), + &shape_type.schema, + &shape_type.shape, + None, + )?; let changes: OrmChanges = self.apply_triple_changes(&shape_triples, &[], nuri, Some(session_id.clone()), true)?; @@ -220,9 +224,15 @@ pub(crate) fn materialize_orm_object( } orm_obj_map.insert(property_name.clone(), Value::Object(nested_objects_map)); } else { - if let Some(BasicType::Str(object_iri)) = pred_change.values_added.get(0) { - if let Some(nested_orm_obj) = get_nested_orm_obj(object_iri) { - orm_obj_map.insert(property_name.clone(), nested_orm_obj); + // Pick the first valid nested object among the added values. + // There may be multiple values (extras), but for single-cardinality + // predicates we materialize just one valid nested object. + for val in &pred_change.values_added { + if let BasicType::Str(object_iri) = val { + if let Some(nested_orm_obj) = get_nested_orm_obj(object_iri) { + orm_obj_map.insert(property_name.clone(), nested_orm_obj); + break; + } } } } diff --git a/engine/verifier/src/orm/process_changes.rs b/engine/verifier/src/orm/process_changes.rs index a04a5f40..d7b4e11a 100644 --- a/engine/verifier/src/orm/process_changes.rs +++ b/engine/verifier/src/orm/process_changes.rs @@ -357,6 +357,7 @@ impl Verifier { &schema, &shape_iri, Some(objects_to_fetch), + None, )?; let new_triples = self.query_sparql_select(shape_query, Some(nuri_to_string(nuri)))?; diff --git a/engine/verifier/src/orm/query.rs b/engine/verifier/src/orm/query.rs index 80562935..b9e46722 100644 --- a/engine/verifier/src/orm/query.rs +++ b/engine/verifier/src/orm/query.rs @@ -23,6 +23,31 @@ use ng_repo::errors::NgError; use ng_repo::log::*; impl Verifier { + pub fn query_quads_for_shape_type( + &self, + nuri: Option, + schema: &OrmSchema, + shape: &ShapeIri, + filter_subjects: Option>, + ) -> Result, NgError> { + // If nuri is present and it is not the whole graph (did:ng:i), use limit_to_graph. + let limit_to_graph = match nuri { + Some(nuri) => { + if nuri == "did:ng:i" { + None + } else { + Some(nuri) + } + } + None => None, + }; + + let select_query = + shape_type_to_sparql_select(schema, shape, filter_subjects, limit_to_graph)?; + + return self.query_sparql_select(select_query, None); + } + pub fn query_sparql_select( &self, query: String, @@ -30,11 +55,12 @@ impl Verifier { ) -> Result, NgError> { let oxistore = self.graph_dataset.as_ref().unwrap(); - let nuri_str = nuri.as_ref().map(|s| s.as_str()); - log_debug!("querying select\n{}\n{}\n", nuri_str.unwrap(), query); + // Log base IRI safely even when None + let nuri_dbg = nuri.as_deref().unwrap_or(""); + log_debug!("querying select\n{}\n{}\n", nuri_dbg, query); - let parsed = - Query::parse(&query, nuri_str).map_err(|e| NgError::OxiGraphError(e.to_string()))?; + let parsed = Query::parse(&query, nuri.as_deref()) + .map_err(|e| NgError::OxiGraphError(e.to_string()))?; let results = oxistore .query(parsed, nuri) .map_err(|e| NgError::OxiGraphError(e.to_string()))?; @@ -353,7 +379,43 @@ pub fn shape_type_to_sparql_select( schema: &OrmSchema, shape: &ShapeIri, filter_subjects: Option>, + limit_to_graph: Option, ) -> Result { + // NOTE FOR MAINTAINERS + // This function generates a SELECT query that mirrors the WHERE semantics of + // `shape_type_to_sparql_construct`, but instead of returning a graph it projects + // triples as rows binding the following variables: + // - ?s: subject + // - ?p: predicate + // - ?o: object + // - ?g: graph name (bound per recursion "layer") + // + // Key ideas: + // - Each shape layer is wrapped in a GRAPH block with its own graph variable (?gN). + // We attribute triples to the layer in which they logically belong by binding ?g + // to that layer’s graph variable when projecting rows. + // - We preserve OPTIONAL, UNION, and the recursive catch-all pattern from the + // CONSTRUCT builder so that validation logic downstream sees the same data surface. + // - We generate two kinds of WHERE content: + // 1) Constraint blocks per layer (GRAPH ?gN { ... }) that ensure the shape matches. + // 2) Projection branches (a UNION of small blocks) that BIND ?s/?p/?o/?g for each + // triple to return. This keeps the constraints readable and separates them from + // the output mapping. + // + // Variable conventions: + // - Term vars: ?v0, ?v1, ... (opaque; used for intermediate subjects/objects) + // - Graph vars: ?g0, ?g1, ... (one per recursion layer, used to bind ?g) + // + // Recursion and cycles: + // - We track visited shapes by IRI to avoid infinite recursion on cyclic schemas. + // - Nested shapes get their own graph var (?gX). We also add a "catch-all" branch + // within that nested layer so that all triples for the nested subject are returned + // for later validation (even if some predicates are optional or missing). + // + // Readability: + // - The generated SPARQL includes comments that "guide through" the query structure + // to make manual inspection and debugging easier. + // Use a counter to generate unique variable names. let mut var_counter = 0; fn get_new_var_name(counter: &mut i32) -> String { @@ -366,6 +428,16 @@ pub fn shape_type_to_sparql_select( *counter += 1; name } + // Small helper to indent multi-line strings by n spaces for cleaner output. + fn indent(s: &str, n: usize) -> String { + let pad = " ".repeat(n); + s.lines() + .map(|l| format!("{}{}", pad, l)) + .collect::>() + .join("\n") + } + + // no-op: graph token computed within process_shape where needed // Collect SELECT branches (each produces bindings for ?s ?p ?o ?g) and shared WHERE constraints. let mut select_branches: Vec = Vec::new(); @@ -393,7 +465,16 @@ pub fn shape_type_to_sparql_select( var_counter: &mut i32, visited_shapes: &mut HashSet, in_recursion: bool, + limit_to_graph: Option<&str>, ) -> Vec { + // Helper to render a graph token: either a variable (?gN) or a fixed graph IRI <...> + let graph_token = |graph_var: &str| -> String { + if let Some(g) = limit_to_graph { + format!("<{}>", g) + } else { + format!("?{}", graph_var) + } + }; // Prevent infinite recursion on cyclic schemas. // TODO: We could handle this as IRI string reference. if visited_shapes.contains(&shape.iri) { @@ -428,13 +509,14 @@ pub fn shape_type_to_sparql_select( // Output branch for the parent link triple itself (belongs to current layer) when not in recursive catch-all if !in_recursion { let branch = format!( - " GRAPH ?{} {{\n{}\n }}\n BIND(?{} AS ?s)\n BIND(<{}> AS ?p)\n BIND(?{} AS ?o)\n BIND(?{} AS ?g)", - current_graph_var_name, + " # Output: parent link triple at layer graph {}\n GRAPH {} {{\n{}\n }}\n # Bind row variables\n BIND(?{} AS ?s)\n BIND(<{}> AS ?p)\n BIND(?{} AS ?o)\n BIND({} AS ?g)", + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), triple, subject_var_name, predicate.iri, obj_var_name, - current_graph_var_name + graph_token(current_graph_var_name).as_str() ); select_branches.push(format!("{{\n{}\n}}", branch)); } @@ -457,6 +539,7 @@ pub fn shape_type_to_sparql_select( var_counter, visited_shapes, true, + limit_to_graph, ); nested_where_blocks.extend(nested_blocks); } @@ -473,9 +556,15 @@ pub fn shape_type_to_sparql_select( if !nested_where_blocks.is_empty() { let nested_joined = nested_where_blocks.join(" .\n"); - where_body = format!("{} .\n{}", union_body, nested_joined); + where_body = format!( + "# Predicate <{}> with nested shapes in shape <{}>\n{} .\n{}", + predicate.iri, shape.iri, union_body, nested_joined + ); } else { - where_body = union_body; + where_body = format!( + "# Predicate <{}> in shape <{}>\n{}", + predicate.iri, shape.iri, union_body + ); } } else { // Value predicate (non-shape) @@ -484,18 +573,22 @@ pub fn shape_type_to_sparql_select( " ?{} <{}> ?{}", subject_var_name, predicate.iri, obj_var_name ); - where_body = triple.clone(); + where_body = format!( + "# Value predicate <{}> in shape <{}>\n{}", + predicate.iri, shape.iri, triple + ); // Output branch for this value triple in current graph layer if !in_recursion { let branch = format!( - " GRAPH ?{} {{\n{}\n }}\n BIND(?{} AS ?s)\n BIND(<{}> AS ?p)\n BIND(?{} AS ?o)\n BIND(?{} AS ?g)", - current_graph_var_name, + " # Output: value triple at layer graph {}\n GRAPH {} {{\n{}\n }}\n # Bind row variables\n BIND(?{} AS ?s)\n BIND(<{}> AS ?p)\n BIND(?{} AS ?o)\n BIND({} AS ?g)", + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), triple, subject_var_name, predicate.iri, obj_var_name, - current_graph_var_name + graph_token(current_graph_var_name).as_str() ); select_branches.push(format!("{{\n{}\n}}", branch)); } @@ -503,7 +596,10 @@ pub fn shape_type_to_sparql_select( // Optional wrapper, if needed if predicate.minCardinality < 1 { - new_where_statements.push(format!(" OPTIONAL {{\n{}\n }}", where_body)); + new_where_statements.push(format!( + " # OPTIONAL predicate <{}>\n OPTIONAL {{\n{}\n }}", + predicate.iri, where_body + )); } else { new_where_statements.push(where_body); } @@ -521,33 +617,39 @@ pub fn shape_type_to_sparql_select( // Output branch for nested triples: include the parent link triple to bind nested subject even when optional if let Some((parent_subj, parent_pred, parent_graph, this_subj)) = link_from_parent { let parent_link = format!( - " GRAPH ?{} {{\n ?{} <{}> ?{}\n }}", - parent_graph, parent_subj, parent_pred, this_subj + " # Bind nested subject via parent link (optional-safe)\n GRAPH {} {{\n ?{} <{}> ?{}\n }}", + graph_token(parent_graph).as_str(), + parent_subj, + parent_pred, + this_subj ); let nested_graph_block = format!( - " GRAPH ?{} {{\n{}\n }}", - current_graph_var_name, catch_all + " # Nested layer catch-all in graph {}\n GRAPH {} {{\n{}\n }}", + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), + catch_all ); let branch = format!( - "{}\n{}\n BIND(?{} AS ?s)\n BIND(?{} AS ?p)\n BIND(?{} AS ?o)\n BIND(?{} AS ?g)", + "{}\n{}\n # Bind row variables\n BIND(?{} AS ?s)\n BIND(?{} AS ?p)\n BIND(?{} AS ?o)\n BIND({} AS ?g)", parent_link, nested_graph_block, subject_var_name, pred_var_name, obj_var_name, - current_graph_var_name + graph_token(current_graph_var_name).as_str() ); select_branches.push(format!("{{\n{}\n}}", branch)); } else { // Fallback: no explicit parent link (shouldn't happen for nested shapes), still output within graph let branch = format!( - " GRAPH ?{} {{\n{}\n }}\n BIND(?{} AS ?s)\n BIND(?{} AS ?p)\n BIND(?{} AS ?o)\n BIND(?{} AS ?g)", - current_graph_var_name, + " # Nested layer catch-all in graph {}\n GRAPH {} {{\n{}\n }}\n # Bind row variables\n BIND(?{} AS ?s)\n BIND(?{} AS ?p)\n BIND(?{} AS ?o)\n BIND({} AS ?g)", + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), catch_all, subject_var_name, pred_var_name, obj_var_name, - current_graph_var_name + graph_token(current_graph_var_name).as_str() ); select_branches.push(format!("{{\n{}\n}}", branch)); } @@ -555,16 +657,20 @@ pub fn shape_type_to_sparql_select( // Combine catch-all with specific predicates of this nested shape inside its graph let joined_where_statements = new_where_statements.join(" .\n"); let inner_union = if joined_where_statements.is_empty() { - format!("{{{}}}", catch_all) + format!("{{\n{}\n }}", catch_all) } else { format!( - "{{{}}} UNION {{\n {}\n }}", - catch_all, joined_where_statements + "{{\n{}\n }} UNION {{\n{}\n }}", + catch_all, + indent(&joined_where_statements, 2) ) }; let nested_block = format!( - " GRAPH ?{} {{\n {}\n }}", - current_graph_var_name, inner_union + " # Nested shape <{}> constraints in graph {}\n GRAPH {} {{\n {}\n }}", + shape.iri, + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), + inner_union ); visited_shapes.remove(&shape.iri); return vec![nested_block]; @@ -573,8 +679,11 @@ pub fn shape_type_to_sparql_select( if !new_where_statements.is_empty() { let body = new_where_statements.join(" .\n"); where_statements.push(format!( - " GRAPH ?{} {{\n{}\n }}", - current_graph_var_name, body + " # Shape <{}> constraints in graph {}\n GRAPH {} {{\n{}\n }}", + shape.iri, + graph_token(current_graph_var_name).as_str(), + graph_token(current_graph_var_name).as_str(), + body )); } } @@ -600,6 +709,7 @@ pub fn shape_type_to_sparql_select( &mut var_counter, &mut visited_shapes, false, + limit_to_graph.as_deref(), ); // Filter subjects, if present (applies to the root subject var) @@ -609,21 +719,42 @@ pub fn shape_type_to_sparql_select( .map(|s| format!("<{}>", s)) .collect::>() .join(", "); - where_statements.push(format!(" FILTER(?v0 IN ({}))", subjects_str)); + where_statements.push(format!( + " # Root subject filter\n FILTER(?v0 IN ({}))", + subjects_str + )); } - // Assemble final query - let mut where_parts: Vec = Vec::new(); + // Assemble final query body with a guided walkthrough as comments + let mut where_body = String::new(); if !where_statements.is_empty() { - where_parts.push(where_statements.join(" .\n")); + where_body.push_str(" # 1) Shape constraints per layer (wrapped in GRAPH ?gN)\n"); + where_body.push_str(&where_statements.join(" .\n")); + where_body.push_str("\n\n"); } if !select_branches.is_empty() { - let union_body = select_branches.join(" UNION "); - where_parts.push(union_body); + where_body.push_str( + " # 2) Output projection: one UNION branch per triple (binds ?s ?p ?o ?g)\n", + ); + where_body.push_str(&select_branches.join(" UNION ")); + where_body.push_str("\n"); } + // Header comments providing context for the generated query + let header = if let Some(ref g) = limit_to_graph { + format!( + "# NextGraph ORM auto-generated SELECT over shape <{}>\n# Returns (?s ?p ?o) with per-layer graph binding (?g)\n# Limited to graph <{}>\n", + shape, g + ) + } else { + format!( + "# NextGraph ORM auto-generated SELECT over shape <{}>\n# Returns (?s ?p ?o) with per-layer graph binding (?g)\n", + shape + ) + }; + Ok(format!( - "SELECT DISTINCT ?s ?p ?o ?g\nWHERE {{\n{}\n}}", - where_parts.join(" .\n") + "{}SELECT DISTINCT ?s ?p ?o ?g\nWHERE {{\n{}\n}}", + header, where_body )) } diff --git a/sdk/rust/src/local_broker.rs b/sdk/rust/src/local_broker.rs index 75f9f23a..d640a799 100644 --- a/sdk/rust/src/local_broker.rs +++ b/sdk/rust/src/local_broker.rs @@ -42,6 +42,7 @@ use ng_net::types::*; use ng_net::utils::{spawn_and_log_error, Receiver, ResultSend, Sender}; use ng_net::{actor::*, actors::admin::*}; +use ng_verifier::orm::types::ShapeIri; use ng_verifier::types::*; use ng_verifier::verifier::Verifier; @@ -2746,7 +2747,8 @@ pub async fn doc_sparql_update( } } -async fn get_broker() -> Result, NgError> { +pub async fn get_broker() -> Result, NgError> +{ let broker = match LOCAL_BROKER.get() { None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), Some(Ok(broker)) => broker.write().await, @@ -2786,6 +2788,21 @@ pub async fn doc_sparql_construct( session.verifier.query_sparql_construct(sparql, nuri) } +/// Runs the shape-type-based quad query using the verifier helper, returning triples. +pub async fn doc_query_quads_for_shape_type( + session_id: u64, + nuri: Option, + schema: &ng_net::orm::OrmSchema, + shape: &ShapeIri, + filter_subjects: Option>, +) -> Result, NgError> { + let broker = get_broker().await?; + let session = broker.get_session(session_id)?; + session + .verifier + .query_quads_for_shape_type(nuri, schema, shape, filter_subjects) +} + pub async fn doc_create( session_id: u64, crdt: String, diff --git a/sdk/rust/src/tests/orm_creation.rs b/sdk/rust/src/tests/orm_creation.rs index 6ad3c6b5..cf529fe4 100644 --- a/sdk/rust/src/tests/orm_creation.rs +++ b/sdk/rust/src/tests/orm_creation.rs @@ -7,7 +7,9 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use crate::local_broker::{doc_create, doc_sparql_construct, doc_sparql_update, orm_start}; +use crate::local_broker::{ + doc_create, doc_query_quads_for_shape_type, doc_sparql_update, get_broker, orm_start, +}; use crate::tests::create_or_open_wallet::create_or_open_wallet; use crate::tests::{assert_json_eq, create_doc_with_data}; use async_std::stream::StreamExt; @@ -18,7 +20,7 @@ use ng_net::orm::{ }; use ng_repo::{log_debug, log_info}; -use ng_verifier::orm::query::shape_type_to_sparql_select; +// use ng_verifier::orm::query::shape_type_to_sparql_select; // replaced by query_quads_for_shape_type use serde_json::json; use std::collections::HashMap; use std::sync::Arc; @@ -77,12 +79,16 @@ INSERT DATA { shape: "http://example.org/TestObject".to_string(), }; - // Generate and execute the CONSTRUCT query - let query = shape_type_to_sparql_select(&shape_type.schema, &shape_type.shape, None).unwrap(); - - let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) - .await - .expect("SPARQL construct failed"); + // Query triples using the new helper + let triples = doc_query_quads_for_shape_type( + session_id, + Some(doc_nuri.clone()), + &shape_type.schema, + &shape_type.shape, + None, + ) + .await + .expect("shape query failed"); // Assert the results let predicates: Vec = triples @@ -181,10 +187,15 @@ INSERT DATA { }; // Generate and run query - let query = shape_type_to_sparql_select(&shape_type.schema, &shape_type.shape, None).unwrap(); - let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) - .await - .unwrap(); + let triples = doc_query_quads_for_shape_type( + session_id, + Some(doc_nuri.clone()), + &shape_type.schema, + &shape_type.shape, + None, + ) + .await + .unwrap(); // Assert: No triples should be returned as the object is incomplete. assert!(triples.is_empty()); @@ -244,10 +255,15 @@ INSERT DATA { }; // Generate and run query - let query = shape_type_to_sparql_select(&shape_type.schema, &shape_type.shape, None).unwrap(); - let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) - .await - .unwrap(); + let triples = doc_query_quads_for_shape_type( + session_id, + Some(doc_nuri.clone()), + &shape_type.schema, + &shape_type.shape, + None, + ) + .await + .unwrap(); // Assert: One triple for prop1 should be returned. assert_eq!(triples.len(), 1); @@ -319,10 +335,15 @@ INSERT DATA { }; // Generate and run query. This must not infinite loop. - let query = shape_type_to_sparql_select(&shape_type.schema, &shape_type.shape, None).unwrap(); - let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) - .await - .unwrap(); + let triples = doc_query_quads_for_shape_type( + session_id, + Some(doc_nuri.clone()), + &shape_type.schema, + &shape_type.shape, + None, + ) + .await + .unwrap(); // Assert: All 6 triples (3 per person) should be returned. assert_eq!(triples.len(), 6);