Skip to content

Add QuadStoreStorageProvider: dotNetRDF IStorageProvider compatibility for Leviathan SPARQL#4

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/support-dotnetrdf-storage-interfaces
Draft

Add QuadStoreStorageProvider: dotNetRDF IStorageProvider compatibility for Leviathan SPARQL#4
Copilot wants to merge 3 commits intomainfrom
copilot/support-dotnetrdf-storage-interfaces

Conversation

Copy link

Copilot AI commented Mar 5, 2026

QuadStore lacked the dotNetRDF storage interfaces required to plug into the Leviathan SPARQL engine and broader dotNetRDF tooling. This PR adds a QuadStoreStorageProvider adapter that implements IStorageProvider, IQueryableStorage, and IUpdateableStorage.

New: QuadStoreStorageProvider

  • IStorageProvider: Full graph CRUD — LoadGraph (4 overloads: Uri/string × IGraph/IRdfHandler), SaveGraph, UpdateGraph (3 overloads), DeleteGraph, ListGraphs/ListGraphNames
  • IQueryableStorage: SPARQL SELECT/ASK/CONSTRUCT/DESCRIBE via Leviathan (LeviathanQueryProcessor over an InMemoryDataset snapshot)
  • IUpdateableStorage: Declared but throws RdfStorageException — QuadStore is append-only
  • Capabilities: DeleteSupported=false, UpdateSupported=true (additions only), ListGraphsSupported=true, IOBehaviour.IsQuadStore

Notable implementation details

  • Dual-format URI queries: data may be stored with or without angle brackets (http://… vs <http://…>); QueryByGraph tries both and deduplicates
  • dotNetRDF 3.x literal normalisation: plain literals carry xsd:string DataType; language-tagged literals carry rdf:langString. NodeToString checks Language before DataType, and uses AbsoluteUri string comparison (not Uri.Equals) to avoid .NET's fragment-ignoring equality bug
  • Recursive lock fix: QuadStore uses ReaderWriterLockSlim(NoRecursion); CreateQueryProcessor materialises ListGraphNames().ToList() before calling LoadGraph to prevent re-entrant lock acquisition
using var qs = new QuadStore("/path/to/data");
IStorageProvider provider = new QuadStoreStorageProvider(qs);

// Load a named graph
var g = new Graph();
provider.LoadGraph(g, new Uri("http://example.org/mygraph"));

// SPARQL query via Leviathan
var results = (SparqlResultSet)((IQueryableStorage)provider)
    .Query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }");

Limitations (documented on class)

  • No graph deletion (append-only store)
  • No triple removal via UpdateGraph
  • No SPARQL Update (IUpdateableStorage.Update throws)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • www.w3.org
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.deps.json /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/testhost.dll --port 40859 --endpoint 127.0.0.1:040859 --role client --parentprocessid 4144 --telemetryoptedin false (dns block)
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.deps.json /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/testhost.dll --port 35233 --endpoint 127.0.0.1:035233 --role client --parentprocessid 7612 --telemetryoptedin false 0.3/lib/net45/Ne-c 0.3/lib/netstand. /tmp/MSBuildTempqoFzc1/tmp0494768c8bfa4731bb2dd2cee36798be.exec.cmd 0.3/lib/netstandard2.0/Newtonsof--noheadings (dns block)
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/TripleStore.Tests.deps.json /home/REDACTED/work/QuadStore/QuadStore/test/TripleStore.Tests/bin/Release/net10.0/testhost.dll --port 40783 --endpoint 127.0.0.1:040783 --role client --parentprocessid 9230 --telemetryoptedin false (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Support dotNetRDF storage interfaces for Leviathan SPARQL engine compatibility</issue_title>
<issue_description>## Summary
To enable QuadStore to serve as a backend for the dotNetRDF Leviathan SPARQL query engine, it must implement key interfaces provided by dotNetRDF for storage providers. This interoperability is necessary so that queries using Leviathan (and the rest of dotNetRDF tooling) can treat QuadStore as a first-class RDF dataset.

Background

The dotNetRDF project defines a set of interfaces that allow pluggable triple/quad stores to be used as backends for querying, updating, and graph management. The central interfaces relevant for store integration, specifically for SPARQL query execution via Leviathan, are found in the VDS.RDF.Storage namespace. Full compatibility with Leviathan typically involves implementing these interfaces so QuadStore presents itself as an IStorageProvider or its variants.

Acceptance Criteria

  • Implement the following dotNetRDF interfaces in QuadStore:
    • IStorageProvider: The main contract for all storages. Enables CRUD access to triples/graphs.
    • Optionally: IQueryableStorage, IUpdateableStorage (if supporting remote or in-process SPARQL querying and updates).
    • Ensure correct handling of graph URIs and named graph semantics (quad support).
  • Pass all relevant Leviathan SPARQL test cases (using dotNetRDF's test suite where possible).
  • Document how users can use QuadStore as a dotNetRDF backend (sample code, configuration, limitations).

References

Implementation Hints

  • Implement IStorageProvider on a new adapter class, e.g. QuadStoreStorageProvider.
  • Map interface methods (LoadGraph, SaveGraph, UpdateGraph, DeleteGraph, ListGraphs, etc.) to existing QuadStore capabilities.
  • For quad support: ensure that named graphs are surfaced as required by dotNetRDF, not just default graph.
  • If in-memory QuadStore does not support all features, document unimplemented interface members clearly.

This feature should make it possible to use code such as:

IStorageProvider provider = new QuadStoreStorageProvider(/* ... */);
// Used transparently as a backend by dotNetRDF APIs
var g = new Graph();
provider.LoadGraph(g, "http://example.org/mygraph");
...

Which will then be available for use by the Leviathan query engine and related code.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits March 5, 2026 04:07
…torage, IUpdateableStorage

Co-authored-by: aabs <157775+aabs@users.noreply.github.com>
…oreStorageProvider

Co-authored-by: aabs <157775+aabs@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement dotNetRDF storage interfaces for Leviathan compatibility Add QuadStoreStorageProvider: dotNetRDF IStorageProvider compatibility for Leviathan SPARQL Mar 5, 2026
Copilot finished work on behalf of aabs March 5, 2026 04:13
@aabs aabs requested a review from Copilot March 5, 2026 05:53
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a QuadStoreStorageProvider adapter class to TripleStore.Core that wraps the existing QuadStore and implements the dotNetRDF IStorageProvider, IQueryableStorage, and IUpdateableStorage interfaces, enabling the store to be used as a first-class backend for the Leviathan SPARQL engine and other dotNetRDF tooling.

Changes:

  • New QuadStoreStorageProvider adapter implementing three dotNetRDF storage interfaces with graph CRUD, SPARQL query support via Leviathan, and documented limitations (append-only, no graph deletion, no SPARQL Update).
  • New QuadStoreStorageProviderTests test class with 38 tests covering capabilities, graph CRUD, SPARQL querying, node serialization round-trips, and integration scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/TripleStore.Core/QuadStoreStorageProvider.cs New adapter class implementing dotNetRDF storage interfaces with helper methods for node serialization and dual-format URI querying.
test/TripleStore.Tests/QuadStoreStorageProviderTests.cs New test file covering the full public surface of QuadStoreStorageProvider including round-trip and integration tests.

Comment on lines +461 to +464
.Replace("\\\"", "\"")
.Replace("\\n", "\n")
.Replace("\\r", "\r")
.Replace("\\\\", "\\");
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UnescapeLiteral method uses sequential String.Replace calls in the wrong order, causing data corruption for literals that contain a backslash followed by the letter n or r.

Consider a literal value that is the two-character string \n (backslash + letter 'n', NOT a newline). EscapeLiteral correctly stores it as \\n (three chars: \, \, n). However, UnescapeLiteral then processes it as follows:

  1. Step "Replace \""": no match.
  2. Step "Replace \n → newline": finds the 2-char sequence \n at positions 1–2 of \\n and replaces it, yielding \<newline> (two chars: backslash + actual newline). This is wrong.
  3. Step "Replace \\\": no match on the remaining string.

Final result: \<newline> instead of the correct \n (backslash + n).

The root cause is that the \\\ substitution must happen before the \n → newline and \r → carriage-return substitutions. Otherwise an escaped backslash at the end of \\n is consumed as the first char of the \n escape sequence.

The fix is to reorder: process \\ first, then \", \n, \r. Note that even with reordering, chained String.Replace calls are not fully correct for mutual escape sequences; the truly correct solution is a character-by-character scanner that processes each \X token as a single unit.

There is no test covering this case (a literal that contains a backslash followed by n or r as literal characters).

Suggested change
.Replace("\\\"", "\"")
.Replace("\\n", "\n")
.Replace("\\r", "\r")
.Replace("\\\\", "\\");
.Replace("\\\\", "\\")
.Replace("\\\"", "\"")
.Replace("\\n", "\n")
.Replace("\\r", "\r");

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +202
/// <inheritdoc/>
public IEnumerable<string> ListGraphNames()
{
return _store.Query()
.Select(q => q.graph)
.Distinct();
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the store contains data with angle-bracketed graph URIs (e.g. <http://example.org/g>), ListGraphNames() (line 197–202) returns those angle-bracketed strings as-is. In CreateQueryProcessor() (line 347), these raw strings are then passed to LoadGraph(IGraph g, string graphUri) (line 105).

In LoadGraph(IGraph, string), after populating the graph, the code tries Uri.TryCreate(graphUri, UriKind.Absolute, out var uri) to set g.BaseUri. An angle-bracketed string like <http://example.org/g> is not a valid absolute URI, so TryCreate returns false and g.BaseUri is never set. The graph added to the in-memory triple store will have a null BaseUri, so when the graph is added to the TripleStore via tripleStore.Add(g), it will be treated as the default graph instead of the named graph http://example.org/g.

Additionally, ListGraphs() (line 187) filters with Uri.TryCreate(g, UriKind.Absolute, out _), which also silently drops angle-bracketed URIs. This creates an inconsistency where ListGraphNames() and ListGraphs() can return different numbers of graphs when some are stored with angle brackets.

Both ListGraphNames() and ListGraphs() should normalise the raw stored graph string by stripping angle brackets before returning.

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +360
private LeviathanQueryProcessor CreateQueryProcessor()
{
var tripleStore = new VDS.RDF.TripleStore();
// Materialise the graph names eagerly to release the QuadStore read lock
// before calling LoadGraph (which also acquires the same lock).
var graphNames = ListGraphNames().ToList();
foreach (var graphName in graphNames)
{
var g = new Graph();
LoadGraph(g, graphName);
tripleStore.Add(g, mergeIfExists: true);
}
return new LeviathanQueryProcessor(new InMemoryDataset(tripleStore, unionDefaultGraph: true));
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateQueryProcessor() is called on every invocation of Query() and rebuilds a full in-memory snapshot of the entire store by loading every graph for every query. For a store with many graphs or large graphs, this means a complete data copy per query, making query performance O(n) in store size regardless of query complexity.

Consider caching the LeviathanQueryProcessor (or the underlying InMemoryDataset) and invalidating it on writes, or at minimum documenting this as a known performance limitation.

Copilot uses AI. Check for mistakes.
Comment on lines +587 to +597
[Fact]
public void RoundTrip_LiteralWithSpecialChars()
{
var factory = new NodeFactory();
var node = factory.CreateLiteralNode("line1\nline2");
var str = QuadStoreStorageProvider.NodeToString(node);
str.Should().Be("\"line1\\nline2\"");
var back = QuadStoreStorageProvider.StringToNode(str, factory);
back.Should().BeAssignableTo<ILiteralNode>();
((ILiteralNode)back).Value.Should().Be("line1\nline2");
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RoundTrip_LiteralWithSpecialChars test only covers a literal with an actual newline character (\n). It does not cover a literal containing a backslash followed by the letter n (the two-char sequence \n), which triggers the UnescapeLiteral ordering bug described in the corresponding production code comment. A test case for this would be: factory.CreateLiteralNode("value\\ntext") (where \\n is backslash + n, not a newline). Similarly, a literal containing \r (backslash + r) and \" (backslash + quote) are not tested.

Copilot generated this review using guidance from repository custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support dotNetRDF storage interfaces for Leviathan SPARQL engine compatibility

3 participants