feat: Named Graphs Within Perspectives#812
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review: Deep SPARQL Integration for Named GraphsThe named graphs implementation works — wind tunnel confirmed no regressions and showed clear wins in bulk delete (2.2×) and graph-scoped query selectivity (14.8× for 1-graph vs all-graphs). The architectural intent is sound. But the current approach bolts named graphs on as an external parameter rather than integrating them properly into the SPARQL layer. If we're doing this, let's do it right. Core Issue: Queries Are Graph-UnawareCurrently: // Application code
perspective.querySparql(
"SELECT ?msg WHERE { <channel1> <has_child> ?msg }",
["ad4m://graph/channel1"] // ← scoping is external
);The SPARQL query itself is oblivious to named graphs. Scoping happens via post-parse dataset mutation: parsed_query.dataset_mut().set_default_graph(named_nodes);SPARQL has had proper named graph syntax since 1.1 (2013): -- FROM clause: sets the dataset declaratively
SELECT ?msg FROM <ad4m://graph/channel1>
WHERE { <channel1> <has_child> ?msg }
-- GRAPH keyword: explicit named graph patterns
SELECT ?msg WHERE {
GRAPH <ad4m://graph/channel1> { <channel1> <has_child> ?msg }
}
-- Cross-graph patterns
SELECT ?msg ?channel WHERE {
GRAPH ?g { ?channel <has_child> ?msg }
FILTER(?g IN (<ad4m://graph/ch1>, <ad4m://graph/ch2>))
}Oxigraph supports all of this natively. The infrastructure is there. What's Lost With External Scoping
Suggested Improvements1. Model Query Builder Should Emit
|
Follow-Up Review: Post-Improvement Wind Tunnel Results + Further SuggestionsRebuilt and re-ran the full wind tunnel after Wind Tunnel Results (Post-Improvement)S1–S8: No Regression ✅
S9a: Scoped Query — No measurable difference at 12.5k linksBoth branches return sub-5ms for channel-scoped queries. Expected — oxigraph's B-tree index handles S9b: Bulk Delete —
|
| Named Graphs | Sparql 1.2 | Ratio | |
|---|---|---|---|
| Previous run (before cross-graph cleanup) | 152ms | 329ms | 2.2× faster |
| This run (with cross-graph cleanup) | 1448ms | 335ms | 4.3× SLOWER |
The cross-graph reference cleanup in PerspectiveInstance::remove_graph() is doing:
SELECT DISTINCT ?s WHERE { ?s ?p ?o . FILTER(isIRI(?s)) }— finds all subjects in the graph- For each subject, calls
query_links(target: subject)+remove_link()per result
This N+1 query pattern eliminates the O(1) advantage of graph drop. With 500 messages, each having ~5 links, that's 500 subjects × (1 query + K removes). The cure is worse than the disease.
S9c: Subscription Overhead — Named graphs slightly worse
| Named Graphs | Sparql 1.2 | |
|---|---|---|
| Overhead | +391% | +339% |
This is expected: raw SPARQL subscriptions (perspectiveSubscribeQuery) set graph_scope: None, so all 5 subs re-evaluate on every add regardless. The named-graphs branch additionally pays for set_default_graph_as_union() per re-evaluation. The model_subscribe with graphIris path (which sets graph_scope) would show the win, but isn't exercised here.
S9d: Cross-Graph Query — Same advantage as before ✅
1-graph scope: 0.8ms → all-graphs scope: 10.2ms (13.6× ratio). Graph selectivity works correctly.
Issue #1: remove_graph Cross-Graph Cleanup Is O(n²)
The intent is correct (cleaning up dangling references from other graphs that target subjects in the deleted graph), but the implementation is quadratic.
Current code:
// For each subject in the deleted graph...
for subject in subjects_in_graph {
// Query ALL links targeting this subject (from any graph)
let links = query_links(None, None, Some(subject), None, None, None)?;
for link in links {
let _ = self.sparql_store.remove_link(&link);
}
}Problems:
- N individual
query_linkscalls (one per subject IRI in the graph) - Each
query_linksscans all graphs becausehas_named_graphs()is true - Each
remove_linkdoes its own quad lookups and deletions
Suggested fix — single SPARQL DELETE + batch approach:
pub fn remove_graph(&self, graph_iri: &str) -> Result<(), deno_core::anyhow::Error> {
// 1. Collect all subject IRIs in one query (already done)
let subjects = self.sparql_store.subjects_in_graph(graph_iri)?;
// 2. Remove the graph atomically (fast — single oxigraph op)
self.sparql_store.remove_named_graph_and_quads(graph_iri)?;
// 3. Batch-remove incoming links from OTHER graphs
// Single SPARQL query with VALUES clause instead of N queries:
if !subjects.is_empty() {
let values = subjects.iter()
.map(|s| format!("<{}>", s))
.collect::<Vec<_>>()
.join(" ");
// Find all links in remaining graphs that target our deleted subjects
let query = format!(
"SELECT ?s ?p ?o WHERE {{ ?s ?p ?o . FILTER(?o IN ({})) }}",
values
);
// Execute against remaining graphs only, then batch-remove
let incoming = self.sparql_store.query_with_graphs(&query, None)?;
// ... batch remove from parsed results
}
Ok(())
}Or better yet — make cross-graph cleanup optional and document the contract:
/// Remove a named graph.
/// - `cascade: true` → also remove links from other graphs targeting subjects in this graph
/// - `cascade: false` → only drop the graph (caller handles cleanup, or dangling refs are acceptable)
pub fn remove_graph(&self, graph_iri: &str, cascade: bool) -> Result<(), AnyError>Most deletion patterns in Flux already know what incoming references exist (parent → child links). The generic cascade is doing speculative work that the caller could do more efficiently.
Issue #2: Subscription graph_scope Not Derived From Query FROM Clauses
The review suggested: "derive subscription graph_scope from the query's dataset." This was implemented for model_subscribe_and_query (passing graph_iris through), but raw SPARQL subscriptions still hardcode graph_scope: None.
If a user subscribes with:
SELECT ?msg FROM <ad4m://graph/channel1> WHERE { <channel1> <has_child> ?msg }The subscription doesn't extract the FROM clause and set graph_scope: ["ad4m://graph/channel1"]. It re-evaluates on every link change in any graph.
Suggestion: When registering a raw SPARQL subscription, parse the query and extract FROM graph IRIs:
let subscribed_query = SubscribedQuery {
query: query.clone(),
// ...
graph_scope: extract_from_graph_iris(&query), // NEW: parse FROM clauses
model_query_params: None,
};Where:
fn extract_from_graph_iris(query: &str) -> Option<Vec<String>> {
let parsed = oxigraph::sparql::Query::parse(query, None).ok()?;
let dataset = parsed.dataset();
if dataset.is_default_dataset() { return None; }
let graphs: Vec<String> = dataset.default_graph_graphs()
.iter()
.filter_map(|g| match g {
GraphName::NamedNode(n) => Some(n.as_str().to_string()),
_ => None,
})
.collect();
if graphs.is_empty() { None } else { Some(graphs) }
}This makes graph-scoped raw SPARQL subscriptions automatically benefit from graph filtering without requiring the model subscription API.
Issue #3: GRAPH Keyword Detection Is String-Based
let query_has_graph_keyword = query_string
.to_uppercase()
.contains(" GRAPH ");This will false-positive on:
- String literals:
"contains GRAPH pattern" - Comments:
# GRAPH is used here - IRIs:
<http://example.org/GRAPH/thing>
And miss:
\nGRAPH <...>(no leading space, only newline){GRAPH ...}(no space before GRAPH, just brace)
Suggestion: Since you're already parsing the query with Query::parse(), you could inspect the algebra instead. But a simpler fix:
// Parse-based detection: if the query has FROM NAMED declarations,
// or if the parsed algebra contains GraphPattern::Graph nodes.
// For now, a regex that handles word boundaries is more robust:
let query_has_graph_keyword = regex::Regex::new(r"(?i)\bGRAPH\s*<")
.unwrap()
.is_match(query_string);Or better: walk the parsed query's algebra tree for GraphPattern::Graph variants. Oxigraph's Query type exposes this.
Issue #4: Seed Performance Regression (28% Slower)
| Named Graphs | Sparql 1.2 | |
|---|---|---|
| Seed 12.5k links | 3.9s | 3.2s |
Each add_link on the named-graphs branch does extra work:
link.graph.as_ref().map(...)— allocatesNamedNodeself.store.insert_named_graph(node.as_ref())— called on every single link add, even though the graph is already registered
Suggestion: Track registered graphs in a HashSet<String> on the SparqlStore and skip the oxigraph insert_named_graph call if already present:
if let Some(ref node) = graph_node {
if self.registered_graphs.borrow_mut().insert(node.as_str().to_string()) {
let _ = self.store.insert_named_graph(node.as_ref());
}
}This turns the redundant registration from O(1)-with-syscall to O(1)-with-hashset-check.
Issue #5: DefaultGraphChanged Handling in Subscription Filter
ChangedGraphs::DefaultGraphChanged => {
// Default graph changed — only relevant if subscription has no graph scope
// (but we're in the Some(scope) branch, so skip)
continue;
}This skips graph-scoped subscriptions when DefaultGraphChanged fires. But consider: if a model uses @Model({ graph: true }) and another model uses the default graph, adding a default-graph link should NOT skip graph-scoped subscriptions if their trigger predicates overlap. The graph scope filter is meant to be an optimization hint, not a semantic guarantee.
Currently the predicate filter already runs first and would continue if predicates don't overlap. But if they DO overlap (e.g., both models use <ad4m://has_child>), the graph filter prematurely skips a subscription that the predicate filter would have let through.
Suggestion: DefaultGraphChanged should NOT skip graph-scoped subscriptions. Change to:
ChangedGraphs::DefaultGraphChanged => {
// Default graph changed — graph-scoped subs MIGHT still be relevant
// if their trigger predicates are shared. Let the query re-evaluate.
// (The cost is one extra SPARQL eval, the benefit is correctness.)
}Or: only skip when DefaultGraphChanged AND the subscription's predicates don't overlap with the changed predicates (requires correlating both filters).
Summary
The FROM clause integration and dataset-respecting query logic are solid. The main issues:
- Critical:
remove_graphcascade is O(n²) — needs batch approach orcascadeflag (this reversed the bulk-delete advantage) - Medium: Raw SPARQL subscriptions don't extract
FROMfor graph_scope — leaves performance on the table - Low: String-based
GRAPHdetection — fragile, should use parsed algebra - Low: Redundant
insert_named_graphper link — 28% seed overhead - Correctness:
DefaultGraphChangedprematurely skips graph-scoped subs — edge case but could cause missed updates
The cross-graph query semantics (issue from first review) are properly addressed — queries with FROM/GRAPH are now respected as self-describing. The architectural direction is right; the implementation just needs performance tuning on the delete path and robustness on the subscription path.
— ⬡
Third Review: Post-Performance-Fix Wind Tunnel Results
Wind Tunnel Results Summary
Key Improvements From Previous Run
The bulk delete regression is fully resolved. The batch VALUES approach brought Code Review of
|
27c2833 to
8bf5064
Compare
Wind Tunnel Run 4 — WebSocket Transport (Final)All scenarios now running over WebSocket RPC (GraphQL removed in both branches after PR #805 merge). Fixed: Named-graphs WS handlers were missing Core Scenarios (s1–s8) — No Regression
Both within noise. Named-graphs adds zero overhead to standard operations. Named-Graph Scenarios (s9a–s9d) — Where It Shines
Key Findings
Transport: WS vs GraphQLSeparate run confirmed WebSocket provides 6-15% improvement over GraphQL across all write/query operations (same branch, different transport). Both branches now exclusively use WS. Build: |
HexaField
left a comment
There was a problem hiding this comment.
Named Graphs Review — WS RPC Handler Gap
The store, model, subscription, and sync layers are solid. The SparqlStore implementation in particular has some genuinely good patterns — the registered_graphs cache, 4-tier query_with_graphs() resolution, two-query get_all_links() preserving graph identity, and remove_links_targeting_subjects() for batch cross-graph cleanup are all improvements over what was originally specced.
Critical missing piece: WebSocket RPC handler layer
The perspective instance methods all accept graph parameters, but the WS RPC handlers in perspectives_ws.rs and request types in types.rs don't extract or forward them. This means none of the graph functionality is accessible from the TypeScript SDK at runtime.
rust-executor/src/api/types.rs — add graph: Option<String> to:
AddLinkRequestAddLinksBulkRequestExecuteCommandsRequestCreateSubjectRequest
rust-executor/src/api/perspectives_ws.rs — handlers that need graph forwarding:
| Handler | Current call (missing graph) | Needs |
|---|---|---|
add_link |
perspective.add_link(link, status, body.batch_id, &agent_context) |
Add body.graph arg |
add_links_bulk |
Uses link_mutations path |
Propagate body.graph through bulk path |
execute_commands |
perspective.execute_commands(commands, body.expression, parameters, body.batch_id, &agent_context) |
Add body.graph arg |
query_sparql |
perspective.sparql_query(query) |
Call sparql_query_with_graphs(query, graphs) with new graphs param |
create_subject |
perspective.create_subject(..., body.batch_id, &agent_context) |
Add body.graph arg |
model_query_handler |
perspective.model_query(&class_name, &query_json, shape_json.as_deref()) |
Add graph_iris param |
model_subscribe_handler |
perspective.model_subscribe_and_query(class_name, query_json, shape_json, user_email) |
Add graph_iris param |
New handlers to register:
perspective.namedGraphs→ callsperspective.named_graphs()perspective.removeNamedGraph→ callsperspective.remove_graph(&graph_iri)
Minor: @Field decorator on graph in Links.ts
The PR adds graph?: string to LinkExpression without the @Field({ nullable: true }) decorator that status has. Worth checking whether @Field is still needed for the serialization pipeline or if it's a GraphQL artifact that can be ignored.
Updated spec
The spec at .specs/SPEC_NAMED_GRAPHS.md has been updated to incorporate the PR's patterns (which are better than the original spec in several areas) and clearly documents the WS RPC handler changes needed in section 12.
HexaField
left a comment
There was a problem hiding this comment.
Named Graphs PR Review
The store, model, and subscription layers are solid. The registered_graphs cache pattern, the 4-tier dataset resolution in query_with_graphs(), the ChangedGraphs enum for subscription filtering, the extract_from_graph_iris() auto-detection, and the batch remove_links_targeting_subjects() approach are all improvements over the original spec — we have updated the spec and plan to incorporate these patterns.
Critical Gap: WebSocket RPC Handler Layer
The perspective instance methods accept graph parameters throughout, but the WS RPC handlers in perspectives_ws.rs and request types in api/types.rs do not extract or forward them. This means the entire named graph API surface is unreachable at runtime despite the underlying implementation being complete.
Files needing changes:
rust-executor/src/api/types.rs — Add pub graph: Option<String> to:
AddLinkRequestAddLinksBulkRequestExecuteCommandsRequestCreateSubjectRequest- Add new
NamedGraphsRequestandRemoveNamedGraphRequesttypes
rust-executor/src/api/perspectives_ws.rs — Update handlers to forward graph:
add_link()→ passbody.graphtoperspective.add_link()add_links_bulk()→ propagatebody.graphthroughlink_mutationspathexecute_commands()→ passbody.graphtoperspective.execute_commands()query_sparql()→ extractgraphsparam, callperspective.sparql_query_with_graphs()instead ofperspective.sparql_query()create_subject()→ passbody.graphtoperspective.create_subject()model_query_handler()→ extractgraphIris, pass toperspective.model_query()model_subscribe_handler()→ extractgraphIris, pass toperspective.model_subscribe_and_query()- Register new handlers:
map.register("perspective.namedGraphs", ...)andmap.register("perspective.removeNamedGraph", ...)
Minor: @Field decorator on graph field
The PR adds @Field({ nullable: true }) on graph in Links.ts, but @Field is not imported or defined anywhere in core/src/ — it was a GraphQL-era decorator that has been removed. The status field on the same class has no decorator. The graph field should be added as a plain property like status:
graph?: string; // not @Field({ nullable: true })Updated spec and plan
The spec §12 has the complete handler and request type changes needed, with code examples matching the actual codebase patterns (params.require_str(), get_perspective_with_access(), etc.). The plan has been updated to "In Progress" with an implementation status table.
623fee0 to
93d67dc
Compare
Implement RDF named graphs to partition triples within a single perspective's Oxigraph store. This enables subject classes to declare graph-rooted models where each instance's triples live in an isolated named graph (IRI: ad4m://graph/<baseExpression>). - Add `graph?: string` field to LinkExpression/LinkExpressionInput - Update linkEqual() to include graph in equality check - Add `graph` param to PerspectiveClient mutations (addLink, addLinks, removeLink, createSubject) and queries (queryLinks, queryProlog) - Add `graphs` filter to PerspectiveProxy query methods - Add PerspectiveProxy.namedGraphs() and removeNamedGraph() methods - Add ModelConfig.graph option and _graphRooted metadata flag - Implement graph-aware Ad4mModel: graphIri getter, graphIriFor(), graph-scoped create/save/delete, parent→child graph resolution - Update SHACLShape to emit ad4m:hasGraph triple in Turtle output - Update shacl-gen to set shape.hasGraph from _graphRooted decorator - Add `graph: Option<String>` to LinkExpression, DecoratedLinkExpression, and all From trait implementations in types.rs - Add `graph: Option<String>` to LinkExpressionInput GraphQL type - Implement graph-aware SparqlStore: insert into named graph, remove from named graph, scoped query with GRAPH patterns, get_named_graphs(), remove_named_graph() lifecycle methods - Add graph param to PerspectiveInstance::add_link/add_links/execute_commands - Add graph param to create_subject full chain - Add perspective_named_graphs query resolver - Add perspective_remove_named_graph mutation resolver - Add graph_iris param to execute_model_query for graph-scoped queries - Add has_graph field to ModelShape for SHACL→query integration - Implement subscription graph filtering: ChangedGraphs enum tracks which graphs were modified, SubscribedQuery stores graph_scope, check_subscribed_queries skips irrelevant graph changes - Add 11 unit tests for named graph operations in sparql_store.rs: insert/query, scoped exclusion, bulk delete, cross-graph duplicates, graph field preservation, lifecycle, graph-aware remove, default graph behavior, make_graph_iri, nonexistent graph queries - Fix all test compilation (graph: None on struct literals, has_graph on ModelShape, execute_model_query signature updates)
1. Model query builder emits FROM clauses in generated SPARQL
- build_from_clauses helper generates FROM <iri> strings
- build_instance_sparql and build_count_sparql accept graph_iris
- Projection and reverse relation sub-queries include FROM clauses
2. query_with_graphs respects self-describing queries (FROM/GRAPH)
- 4-tier dataset resolution: query dataset → GRAPH keyword →
external graph_iris → union fallback
- Queries with native FROM clauses or GRAPH patterns pass through
without external dataset override
3. Subscription graph_scope derived from model query's graph_iris
- Single source of truth: same value generates FROM clauses
- Docstring clarifies derivation
4. Cross-graph GRAPH patterns supported
- query_with_graphs detects GRAPH keyword in query text
- Self-describing queries execute without dataset override
5. removeGraph cascades cross-graph reference cleanup
- Queries subjects in graph before removal
- Removes incoming links from other graphs targeting those subjects
- TypeScript delete() simplified (executor handles atomically)
1. remove_graph: O(n²) → batch with VALUES clause
- Single SPARQL query finds all incoming links across graphs
- Replaces N individual query_links + remove_link calls
- Restores bulk delete performance advantage
2. Raw SPARQL subscription graph scoping
- extract_from_graph_iris() parses FROM clauses at registration
- Graph-scoped raw subscriptions skip on unrelated graph changes
- No API change needed — automatic from query content
3. GRAPH keyword detection: string-based → regex
- Uses word boundary + following </? to avoid false positives
- Handles newline-prefixed GRAPH, brace-prefixed, etc.
- Won't match GRAPH inside string literals or IRIs
4. insert_named_graph cache (Arc<Mutex<HashSet>>)
- Skip redundant oxigraph insert_named_graph on every add_link
- Cache evicted on remove_named_graph_and_quads
- Eliminates 28% seed overhead from repeated registration
5. DefaultGraphChanged no longer skips graph-scoped subs
- Shared predicates between default-graph and graph-scoped models
could cause missed updates
- Now lets query re-evaluate (correctness over micro-optimization)
- Fix get_all_links to preserve graph field (GRAPH ?g pattern for named graphs, separate default graph query for non-graph links) - Run cargo fmt across crate
After the SSE-to-WebSocket migration (PR #805), the named graph parameters were only wired through the now-deleted GraphQL resolvers. This commit adds: - graph param to addLink and addLinks WS handlers - graphs param to querySparql WS handler - graphIris param to modelQuery and modelSubscribe WS handlers - New perspective.namedGraphs handler (list named graphs) - New perspective.removeNamedGraph handler (drop graph + cleanup) - graph field on DecoratedLinkExpression in agent_ws
…ct handlers, remove dead @field decorators
93d67dc to
b70c4e9
Compare
Summary
Implements RDF named graphs to partition triples within a single perspective's Oxigraph store. Subject classes can declare graph-rooted models where each instance's triples live in an isolated named graph (
ad4m://graph/<baseExpression>).This enables efficient scoped queries, O(1) bulk deletion, and subscription filtering by graph — critical for performance in perspectives with many subject instances (e.g., Flux channels with thousands of messages).
Motivation
Without named graphs, all triples in a perspective share a single default graph. This makes it expensive to:
Named graphs solve all three by partitioning triples at the storage level while integrating deeply with SPARQL's native graph semantics.
Performance Results
Wind tunnel benchmarks (Apple Silicon, 48GB RAM) comparing this branch against
feat/sparql-1.2-cleanupwith 5 channels × 500 messages (12,510 links):No regression on existing workloads (cold start, link throughput, query scaling, subject class queries all identical between branches).
At production scale (Flux perspectives with 100k+ links across dozens of channels), benefits are multiplicative:
model_subscribewithgraphIrismeans unrelated channel changes trigger zero SPARQL re-evaluationArchitecture: Deep SPARQL Integration
Named graphs are not bolted on as an external parameter — they integrate with SPARQL's native dataset semantics:
Self-Describing Queries via FROM Clauses
The model query builder (
model_query.rs) emitsFROM <graph_iri>clauses when the model is graph-rooted. Queries are self-describing:Dataset Resolution Priority
query_with_graphs()respects a clear priority order:FROM/FROM NAMED/GRAPHclauses → respect as-is (query is self-describing)graph_irisparameter provided → set those as the default datasetThis means native SPARQL cross-graph patterns work:
Subscription Graph Scope (Automatic)
model_subscribe_and_query):graph_scopeset from thegraphIrisparameter — only changes in watched graphs trigger re-evaluationperspectiveSubscribeQuery):FROMclauses are parsed at registration time and automatically populategraph_scopeChangedGraphsenum (NoneRecorded/DefaultGraphChanged/Specific(HashSet)) enables O(1) skip decisions per subscriptionBulk Delete with Cross-Graph Cleanup
removeGraph(iri)performs:store.remove_named_graph()(drops all quads in one oxigraph operation)This ensures no dangling cross-graph references while maintaining O(1) graph drop performance.
Performance Optimizations
HashSet<String>behindMutex): skips redundantinsert_named_graphcalls on every link addGRAPHkeyword detection (word-boundary + following<or?): avoids false positives from string literals or IRIsChanges
Core SDK (TypeScript)
core/src/links/Links.tsgraph?: stringon LinkExpression/LinkExpressionInput, updatedlinkEqual()core/src/perspectives/PerspectiveClient.tsgraphparam on mutations,graphsfilter on queries,namedGraphs(),removeNamedGraph()core/src/perspectives/PerspectiveProxy.tscore/src/model/decorators.tsModelConfig.graphoption,_graphRootedmetadatacore/src/model/types.tsModelMetadata.graphfieldcore/src/model/Ad4mModel.tsgraphIrigetter,graphIriFor(), graph-scoped CRUD, parent→child graph resolution, graph-aware deletecore/src/shacl/SHACLShape.tshasGraphfield,toTurtle()emitsad4m:hasGraphcore/src/model/shacl-gen.tsshape.hasGraphfrom_graphRootedRust Executor
rust-executor/src/types.rsgraph: Option<String>on LinkExpression, DecoratedLinkExpression, all From implsrust-executor/src/graphql/graphql_types.rsgraphon LinkExpressionInputrust-executor/src/perspectives/sparql_store.rsFROMclause detection,registered_graphscache, batch cross-graph cleanup, named graph CRUDrust-executor/src/perspectives/perspective_instance.rsChangedGraphstracking, subscription graph filtering,extract_from_graph_irisfor raw subsrust-executor/src/perspectives/model_query.rsbuild_from_clauses()helper,graph_iristhreaded through all query builders (instance, count, projection, reverse-include)rust-executor/src/graphql/query_resolvers.rsperspectiveNamedGraphsquery,graphsfilter onperspectiveQuerySparql,graph_irisonperspectiveModelQueryrust-executor/src/graphql/mutation_resolvers.rsperspectiveRemoveNamedGraphmutation,graphon add/addLinks/executeCommands/createSubjectInstance,graph_irisonperspectiveModelSubscribeTests
11 new unit tests in
sparql_store.rs:Graph IRI Convention
Where
baseExpressionis the subject instance's base URI (the source of itsrdf:typelink).API
TypeScript
GraphQL
Backward Compatibility
graphfield work exactly as before (default graph)set_default_graph_as_union()— all data visiblelinkEqual()now includesgraphin equality checks (two links with same s/p/t in different graphs are distinct)Dependencies
@Model({ graph: true })to Channel