Merge pull request #1192 from Freakz2z/fix/rdf4j-repository-id

fix(triplet_store): honor RDF4J repository id
This commit is contained in:
Mohd Kaif
2026-08-24 12:21:50 +05:30
committed by GitHub
4 changed files with 75 additions and 9 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ for row in result.bindings:
store = TripletStore( store = TripletStore(
backend="rdf4j", backend="rdf4j",
endpoint="http://localhost:8080/rdf4j-server", endpoint="http://localhost:8080/rdf4j-server",
repository_id="semantica", # passed through **config repository_id="semantica", # selects the remote repository
) )
``` ```
+2 -2
View File
@@ -35,7 +35,7 @@ This page is intentionally conservative: it distinguishes between an adapter exi
| FalkorDB | LPG | Yes | Yes | Partial | Partial | Redis-based; provenance depends on node/edge properties, and multi-graph isolation depends on the selected graph name. | | FalkorDB | LPG | Yes | Yes | Partial | Partial | Redis-based; provenance depends on node/edge properties, and multi-graph isolation depends on the selected graph name. |
| Amazon Neptune | LPG | Yes | Yes | Partial | Partial | Use the property-graph endpoint; AWS auth, VPC, and endpoint configuration can affect local tests. Provenance depends on node/edge properties. | | Amazon Neptune | LPG | Yes | Yes | Partial | Partial | Use the property-graph endpoint; AWS auth, VPC, and endpoint configuration can affect local tests. Provenance depends on node/edge properties. |
| Apache AGE | LPG | Yes | Yes | Partial | Partial | Runs through PostgreSQL/AGE; Cypher compatibility and property handling can differ from standalone LPG engines. | | Apache AGE | LPG | Yes | Yes | Partial | Partial | Runs through PostgreSQL/AGE; Cypher compatibility and property handling can differ from standalone LPG engines. |
| RDF4J | RDF | Yes | Partial | Partial | Partial | Context separation relies on named graphs; triple-level provenance may require reification or graph-level metadata. `RDF4JStore(repository_id=...)` currently has no effect — the constructor always connects to the `"default"` repository regardless of the value passed; track a fix separately. | | RDF4J | RDF | Yes | Partial | Partial | Partial | Context separation relies on named graphs; triple-level provenance may require reification or graph-level metadata. |
| Apache Jena | RDF | Yes | Partial | Partial | Partial | Named graphs are needed for context separation; backend configuration and transaction behavior matter. | | Apache Jena | RDF | Yes | Partial | Partial | Partial | Named graphs are needed for context separation; backend configuration and transaction behavior matter. |
| Blazegraph | RDF | Yes | Partial | Partial | Partial | Use quads/named graphs for context; IRI stability and graph naming matter for provenance. | | Blazegraph | RDF | Yes | Partial | Partial | Partial | Use quads/named graphs for context; IRI stability and graph naming matter for provenance. |
| Anzo | RDF | Yes | Partial | Partial | Partial | Anzo deployments are environment-specific; validate `dataset_uri`/graphmart naming, named-graph support, and provenance mapping. | | Anzo | RDF | Yes | Partial | Partial | Partial | Anzo deployments are environment-specific; validate `dataset_uri`/graphmart naming, named-graph support, and provenance mapping. |
@@ -107,7 +107,7 @@ from semantica.triplet_store import RDF4JStore
store = RDF4JStore( store = RDF4JStore(
endpoint='http://localhost:8080/rdf4j-server', endpoint='http://localhost:8080/rdf4j-server',
repository_id='semantica' # currently has no effect; connects to "default" (see Known limitations) repository_id='semantica'
) )
``` ```
+7 -6
View File
@@ -28,7 +28,7 @@ License: MIT
import re import re
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from urllib.parse import urlparse from urllib.parse import quote, urlparse
import requests import requests
from rdflib import Graph, Literal from rdflib import Graph, Literal
@@ -67,7 +67,8 @@ class RDF4JStore:
self.progress_tracker.enabled = True self.progress_tracker.enabled = True
self.endpoint = endpoint.rstrip("/") self.endpoint = endpoint.rstrip("/")
self.repository_id = config.get("repository_id", "default") self.repository_id = repository_id or config.get("repository_id", "default")
self._encoded_repository_id = quote(self.repository_id, safe="")
self.username = config.get("username") self.username = config.get("username")
self.password = config.get("password") self.password = config.get("password")
self.timeout = config.get("timeout", 30) self.timeout = config.get("timeout", 30)
@@ -79,7 +80,7 @@ class RDF4JStore:
"""Connect to RDF4J server.""" """Connect to RDF4J server."""
try: try:
# Test connection # Test connection
test_url = f"{self.endpoint}/repositories/{self.repository_id}" test_url = f"{self.endpoint}/repositories/{self._encoded_repository_id}"
response = requests.get( response = requests.get(
test_url, test_url,
timeout=self.timeout, timeout=self.timeout,
@@ -100,11 +101,11 @@ class RDF4JStore:
def _get_sparql_endpoint(self) -> str: def _get_sparql_endpoint(self) -> str:
"""Get SPARQL query endpoint.""" """Get SPARQL query endpoint."""
return f"{self.endpoint}/repositories/{self.repository_id}" return f"{self.endpoint}/repositories/{self._encoded_repository_id}"
def _get_update_endpoint(self) -> str: def _get_update_endpoint(self) -> str:
"""Get SPARQL Update endpoint.""" """Get SPARQL Update endpoint."""
return f"{self.endpoint}/repositories/{self.repository_id}/statements" return f"{self.endpoint}/repositories/{self._encoded_repository_id}/statements"
def _is_construct_query(self, query: str) -> bool: def _is_construct_query(self, query: str) -> bool:
""" """
@@ -163,7 +164,7 @@ class RDF4JStore:
""" """
# RDF4J transaction support # RDF4J transaction support
transaction_url = ( transaction_url = (
f"{self.endpoint}/repositories/{self.repository_id}/transactions" f"{self.endpoint}/repositories/{self._encoded_repository_id}/transactions"
) )
try: try:
+65
View File
@@ -22,6 +22,71 @@ def _make_connected_store():
CONSTRUCT_QUERY = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" CONSTRUCT_QUERY = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
class TestRDF4JStoreInitialization(unittest.TestCase):
def test_explicit_repository_id_selects_repository(self):
response = MagicMock(status_code=200)
with patch(
"semantica.triplet_store.rdf4j_store.requests.get",
return_value=response,
) as mock_get:
store = RDF4JStore(
endpoint="http://localhost:8080/rdf4j-server/",
repository_id="semantica",
)
self.assertEqual(store.repository_id, "semantica")
mock_get.assert_called_once_with(
"http://localhost:8080/rdf4j-server/repositories/semantica",
timeout=30,
auth=None,
)
def test_repository_id_is_encoded_as_a_single_url_path_segment(self):
response = MagicMock(status_code=200)
with patch(
"semantica.triplet_store.rdf4j_store.requests.get",
return_value=response,
) as mock_get:
store = RDF4JStore(
endpoint="http://localhost:8080/rdf4j-server",
repository_id="team/repo ?#",
)
self.assertEqual(store.repository_id, "team/repo ?#")
mock_get.assert_called_once_with(
"http://localhost:8080/rdf4j-server/repositories/team%2Frepo%20%3F%23",
timeout=30,
auth=None,
)
self.assertEqual(
store._get_sparql_endpoint(),
"http://localhost:8080/rdf4j-server/repositories/team%2Frepo%20%3F%23",
)
self.assertEqual(
store._get_update_endpoint(),
"http://localhost:8080/rdf4j-server/repositories/"
"team%2Frepo%20%3F%23/statements",
)
transaction_response = MagicMock()
transaction_response.headers = {"Location": "/transactions/tx-1"}
with patch(
"semantica.triplet_store.rdf4j_store.requests.post",
return_value=transaction_response,
) as mock_post:
self.assertEqual(store.begin_transaction(), "tx-1")
mock_post.assert_called_once_with(
"http://localhost:8080/rdf4j-server/repositories/"
"team%2Frepo%20%3F%23/transactions",
timeout=30,
auth=None,
)
class TestRDF4JStoreIsConstructQuery(unittest.TestCase): class TestRDF4JStoreIsConstructQuery(unittest.TestCase):
def test_detects_uppercase(self): def test_detects_uppercase(self):
self.assertTrue(_make_connected_store()._is_construct_query( self.assertTrue(_make_connected_store()._is_construct_query(