select query working for current triple setup and tests

feat/orm-diffs
Laurin Weger 5 hours ago
parent 0753859d0d
commit b8f14ed561
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 20
      engine/verifier/src/orm/initialize.rs
  2. 1
      engine/verifier/src/orm/process_changes.rs
  3. 207
      engine/verifier/src/orm/query.rs
  4. 19
      sdk/rust/src/local_broker.rs
  5. 61
      sdk/rust/src/tests/orm_creation.rs

@ -84,8 +84,12 @@ impl Verifier {
shape_type: &OrmShapeType,
) -> Result<Value, NgError> {
// 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;
}
}
}
}

@ -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)))?;

@ -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<String>,
schema: &OrmSchema,
shape: &ShapeIri,
filter_subjects: Option<Vec<String>>,
) -> Result<Vec<Triple>, 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<Vec<Triple>, 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<Vec<String>>,
limit_to_graph: Option<String>,
) -> Result<String, NgError> {
// 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::<Vec<_>>()
.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<String> = Vec::new();
@ -393,7 +465,16 @@ pub fn shape_type_to_sparql_select(
var_counter: &mut i32,
visited_shapes: &mut HashSet<String>,
in_recursion: bool,
limit_to_graph: Option<&str>,
) -> Vec<String> {
// 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::<Vec<_>>()
.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<String> = 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
))
}

@ -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<async_std::sync::RwLockWriteGuard<'static, LocalBroker>, NgError> {
pub async fn get_broker() -> Result<async_std::sync::RwLockWriteGuard<'static, LocalBroker>, 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<String>,
schema: &ng_net::orm::OrmSchema,
shape: &ShapeIri,
filter_subjects: Option<Vec<String>>,
) -> Result<Vec<Triple>, 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,

@ -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<String> = 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);

Loading…
Cancel
Save