diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java
new file mode 100644
index 000000000..26c4a4654
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java
@@ -0,0 +1,46 @@
+package fr.inria.corese.core.next.query.impl.sparql.bridge;
+
+import fr.inria.corese.core.next.data.api.Value;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.kgram.api.core.Node;
+import fr.inria.corese.core.sparql.api.IDatatype;
+
+/**
+ * Converts KGRAM {@link Node} constants to API {@link Value} instances.
+ *
+ *
This is the single point in the bridge layer that is allowed to inspect
+ * {@link IDatatype} on behalf of callers outside {@code next.query.kgram}.
+ */
+public final class KgramNodeConverter {
+
+ private KgramNodeConverter() {}
+
+ /**
+ * Converts a KGRAM constant {@link Node} to the corresponding API {@link Value}.
+ *
+ * @param node the KGRAM node to convert (must not be null)
+ * @param factory the value factory used to create API term instances
+ * @return the API value, or {@code null} when the node kind is not supported
+ */
+ public static Value nodeToValue(Node node, CoreseValueFactory factory) {
+ IDatatype dt = node.getDatatypeValue();
+ if (dt.isURI()) {
+ return factory.createIRI(dt.getLabel());
+ }
+ if (dt.isBlank()) {
+ return factory.createBNode(dt.getLabel());
+ }
+ if (dt.isLiteral()) {
+ String lang = dt.getLang();
+ if (lang != null && !lang.isEmpty()) {
+ return factory.createLiteral(dt.getLabel(), lang);
+ }
+ String datatypeUri = dt.getDatatypeURI();
+ if (datatypeUri != null) {
+ return factory.createLiteral(dt.getLabel(), factory.createIRI(datatypeUri));
+ }
+ return factory.createLiteral(dt.getLabel());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
index 0574490d4..70b19c30a 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
@@ -23,6 +23,7 @@
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder;
+import fr.inria.corese.core.next.query.impl.sparql.bridge.KgramNodeConverter;
import fr.inria.corese.core.next.query.kgram.api.core.Edge;
import fr.inria.corese.core.next.query.kgram.api.core.Node;
import fr.inria.corese.core.next.query.kgram.core.Eval;
@@ -36,10 +37,6 @@
import fr.inria.corese.core.next.query.kgram.tool.NodeImpl;
import fr.inria.corese.core.next.query.kgram.tool.StorageManagerProducer;
import fr.inria.corese.core.next.storagemanager.api.StorageManager;
-import fr.inria.corese.core.sparql.api.IDatatype;
-import fr.inria.corese.core.sparql.datatype.DatatypeMap;
-import fr.inria.corese.core.sparql.triple.parser.Constant;
-import fr.inria.corese.core.sparql.triple.parser.Variable;
import java.util.ArrayList;
import java.util.List;
@@ -348,28 +345,13 @@ private Node resolveTemplateNode(Node templateNode, Mapping mapping) {
/**
* Converts a KGRAM constant {@link Node} to the corresponding API {@link Value}.
*
- * @return the API value, or {@code null} when the datatype kind is not supported
+ * Delegates to {@link KgramNodeConverter} so that this class does not depend on
+ * {@code IDatatype} directly.
+ *
+ * @return the API value, or {@code null} when the node kind is not supported
*/
private Value kgramNodeToApiValue(Node node, CoreseValueFactory factory) {
- IDatatype dt = node.getDatatypeValue();
- if (dt.isURI()) {
- return factory.createIRI(dt.getLabel());
- }
- if (dt.isBlank()) {
- return factory.createBNode(dt.getLabel());
- }
- if (dt.isLiteral()) {
- String lang = dt.getLang();
- if (lang != null && !lang.isEmpty()) {
- return factory.createLiteral(dt.getLabel(), lang);
- }
- String datatypeUri = dt.getDatatypeURI();
- if (datatypeUri != null) {
- return factory.createLiteral(dt.getLabel(), factory.createIRI(datatypeUri));
- }
- return factory.createLiteral(dt.getLabel());
- }
- return null;
+ return KgramNodeConverter.nodeToValue(node, factory);
}
// -------------------------------------------------------------------------
@@ -399,7 +381,7 @@ private void applyDataset(Query kgramQuery, Dataset dataset) {
private List urisToKgramNodes(List uris) {
List nodes = new ArrayList<>(uris.size());
for (String uri : uris) {
- nodes.add(new NodeImpl(Constant.create(DatatypeMap.newResource(uri))));
+ nodes.add(NodeImpl.forIRI(uri));
}
return nodes;
}
@@ -429,7 +411,7 @@ private Mapping buildInitialMapping(BindingSet bindings) {
for (Binding b : bindings) {
Node targetNode = valueToKgramNode(b.value());
if (targetNode != null) {
- queryNodes.add(new NodeImpl(new Variable(b.name())));
+ queryNodes.add(NodeImpl.forVariable(b.name()));
targetNodes.add(targetNode);
}
}
@@ -442,24 +424,20 @@ private Mapping buildInitialMapping(BindingSet bindings) {
* @return a constant node, or {@code null} when the value type is not supported
*/
private Node valueToKgramNode(Value value) {
- IDatatype dt;
if (value instanceof IRI iri) {
- dt = DatatypeMap.newResource(iri.stringValue());
+ return NodeImpl.forIRI(iri.stringValue());
} else if (value instanceof BNode bNode) {
- dt = DatatypeMap.createBlank(bNode.getID());
+ return NodeImpl.forBlank(bNode.getID());
} else if (value instanceof Literal literal) {
String lang = literal.getLanguage().orElse(null);
if (lang != null && !lang.isEmpty()) {
- dt = DatatypeMap.createLiteral(literal.getLabel(), null, lang);
- } else {
- String datatypeUri = literal.getDatatype() != null
- ? literal.getDatatype().stringValue()
- : null;
- dt = DatatypeMap.createLiteral(literal.getLabel(), datatypeUri, null);
+ return NodeImpl.forLiteral(literal.getLabel(), null, lang);
}
- } else {
- return null;
+ String datatypeUri = literal.getDatatype() != null
+ ? literal.getDatatype().stringValue()
+ : null;
+ return NodeImpl.forLiteral(literal.getLabel(), datatypeUri, null);
}
- return new NodeImpl(Constant.create(dt));
+ return null;
}
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/NodeImpl.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/NodeImpl.java
index 65120ff41..37954ef0d 100644
--- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/NodeImpl.java
+++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/NodeImpl.java
@@ -5,8 +5,10 @@
import fr.inria.corese.core.next.query.kgram.api.core.Node;
import fr.inria.corese.core.next.query.kgram.api.core.TripleStore;
import fr.inria.corese.core.sparql.api.IDatatype;
+import fr.inria.corese.core.sparql.datatype.DatatypeMap;
import fr.inria.corese.core.sparql.triple.parser.Atom;
import fr.inria.corese.core.sparql.triple.parser.Constant;
+import fr.inria.corese.core.sparql.triple.parser.Variable;
public class NodeImpl implements Node {
@@ -19,6 +21,32 @@ public NodeImpl(Atom at) {
atom = at;
}
+ /** Creates a constant node for an IRI. */
+ public static NodeImpl forIRI(String iri) {
+ return new NodeImpl(Constant.create(DatatypeMap.newResource(iri)));
+ }
+
+ /** Creates a constant node for a blank node. */
+ public static NodeImpl forBlank(String id) {
+ return new NodeImpl(Constant.create(DatatypeMap.createBlank(id)));
+ }
+
+ /**
+ * Creates a constant node for a literal.
+ *
+ * @param label lexical value
+ * @param datatypeUri datatype IRI, or {@code null}
+ * @param lang language tag, or {@code null}
+ */
+ public static NodeImpl forLiteral(String label, String datatypeUri, String lang) {
+ return new NodeImpl(Constant.create(DatatypeMap.createLiteral(label, datatypeUri, lang)));
+ }
+
+ /** Creates a variable node with the given name. */
+ public static NodeImpl forVariable(String name) {
+ return new NodeImpl(new Variable(name));
+ }
+
@Override
public IDatatype getValue() {
return atom.getDatatypeValue();
diff --git a/src/main/java/fr/inria/corese/core/next/util/StringUtils.java b/src/main/java/fr/inria/corese/core/next/util/StringUtils.java
index a07f5565c..afb73f8bd 100644
--- a/src/main/java/fr/inria/corese/core/next/util/StringUtils.java
+++ b/src/main/java/fr/inria/corese/core/next/util/StringUtils.java
@@ -1,7 +1,5 @@
package fr.inria.corese.core.next.util;
-import fr.inria.corese.core.sparql.triple.parser.Processor;
-
public class StringUtils {
public static String trimChevronIRIs(String uri) {
@@ -101,7 +99,7 @@ public static String escapeForDisplay(String iri) {
/**
* Strips angle brackets if present, then returns the local part after {@code #}, {@code /}, or {@code :},
- * lowercased for {@link Processor} lookup.
+ * lowercased.
*/
public static String localNameFromIriToken(String raw) {
String t = raw.trim();
diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java
new file mode 100644
index 000000000..914e3decd
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java
@@ -0,0 +1,87 @@
+package fr.inria.corese.core.next.query.impl.sparql.bridge;
+
+import fr.inria.corese.core.next.data.api.BNode;
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Literal;
+import fr.inria.corese.core.next.data.api.Value;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.kgram.api.core.Node;
+import fr.inria.corese.core.next.query.kgram.tool.NodeImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class KgramNodeConverterTest {
+
+ private CoreseValueFactory factory;
+
+ @BeforeEach
+ void setUp() {
+ factory = new CoreseValueFactory();
+ }
+
+ @Test
+ @DisplayName("IRI node converts to API IRI with the same string value")
+ void iriNodeConvertsToApiIRI() {
+ Node node = NodeImpl.forIRI("http://example.org/alice");
+
+ Value value = KgramNodeConverter.nodeToValue(node, factory);
+
+ assertInstanceOf(IRI.class, value);
+ assertEquals("http://example.org/alice", value.stringValue());
+ }
+
+ @Test
+ @DisplayName("Blank node converts to API BNode with the same ID")
+ void blankNodeConvertsToApiBNode() {
+ Node node = NodeImpl.forBlank("b1");
+
+ Value value = KgramNodeConverter.nodeToValue(node, factory);
+
+ assertInstanceOf(BNode.class, value);
+ assertEquals("b1", ((BNode) value).getID());
+ }
+
+ @Test
+ @DisplayName("Language-tagged literal converts to API Literal preserving label and lang")
+ void langLiteralConvertsToApiLiteral() {
+ Node node = NodeImpl.forLiteral("hello", null, "en");
+
+ Value value = KgramNodeConverter.nodeToValue(node, factory);
+
+ assertInstanceOf(Literal.class, value);
+ Literal lit = (Literal) value;
+ assertEquals("hello", lit.getLabel());
+ assertEquals("en", lit.getLanguage().orElse(null));
+ }
+
+ @Test
+ @DisplayName("Typed literal converts to API Literal preserving label and datatype IRI")
+ void typedLiteralConvertsToApiLiteral() {
+ String xsdInteger = "http://www.w3.org/2001/XMLSchema#integer";
+ Node node = NodeImpl.forLiteral("42", xsdInteger, null);
+
+ Value value = KgramNodeConverter.nodeToValue(node, factory);
+
+ assertInstanceOf(Literal.class, value);
+ Literal lit = (Literal) value;
+ assertEquals("42", lit.getLabel());
+ assertNotNull(lit.getDatatype());
+ assertEquals(xsdInteger, lit.getDatatype().stringValue());
+ assertTrue(lit.getLanguage().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Plain literal (no lang, no explicit datatype) converts to API Literal with label")
+ void plainLiteralConvertsToApiLiteral() {
+ Node node = NodeImpl.forLiteral("bare", null, null);
+
+ Value value = KgramNodeConverter.nodeToValue(node, factory);
+
+ assertInstanceOf(Literal.class, value);
+ assertEquals("bare", ((Literal) value).getLabel());
+ }
+
+}
diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
index 6fc6df0b6..42d9e22ce 100644
--- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
+++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
@@ -2,6 +2,7 @@
import fr.inria.corese.core.next.data.api.IRI;
import fr.inria.corese.core.next.data.api.Resource;
+import fr.inria.corese.core.next.data.api.Statement;
import fr.inria.corese.core.next.data.api.Value;
import fr.inria.corese.core.next.data.api.ValueFactory;
import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
@@ -9,6 +10,7 @@
import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException;
import fr.inria.corese.core.next.query.api.result.Binding;
import fr.inria.corese.core.next.query.api.result.BindingSet;
+import fr.inria.corese.core.next.query.api.result.GraphQueryResult;
import fr.inria.corese.core.next.query.api.result.TupleQueryResult;
import fr.inria.corese.core.next.query.impl.dataset.CoreseDataset;
import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager;
@@ -194,6 +196,65 @@ void selectWithDatasetFromReturnsNothingForEmptyNamedGraph() {
assertFalse(result.hasNext(), "No results expected for an empty named graph");
}
+ // -------------------------------------------------------------------------
+ // CONSTRUCT / graph evaluation
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("CONSTRUCT query materialises triples from WHERE bindings")
+ void constructQueryMaterialisesTriples() {
+ GraphQueryResult result = executor.evaluateGraph("""
+ CONSTRUCT { ?s ?o }
+ WHERE { ?s ?o }
+ """);
+
+ assertTrue(result.hasNext());
+ Statement stmt = result.next();
+ assertEquals(ALICE, stmt.getSubject().stringValue());
+ assertEquals(KNOWS, stmt.getPredicate().stringValue());
+ assertEquals(BOB, stmt.getObject().stringValue());
+ assertFalse(result.hasNext());
+ }
+
+ @Test
+ @DisplayName("CONSTRUCT with no matching WHERE returns empty graph result")
+ void constructWithNoMatchReturnsEmptyResult() {
+ GraphQueryResult result = executor.evaluateGraph("""
+ CONSTRUCT { ?s ?o }
+ WHERE { ?s ?o }
+ """);
+
+ assertFalse(result.hasNext());
+ }
+
+ @Test
+ @DisplayName("Graph evaluation rejects non-CONSTRUCT/DESCRIBE queries")
+ void graphEvaluationRejectsSelectQuery() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> executor.evaluateGraph("SELECT * WHERE { ?s ?p ?o }"));
+ }
+
+ // -------------------------------------------------------------------------
+ // Initial bindings — literal and blank-node values
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("SELECT with literal initial binding filters results by literal value")
+ void selectWithLiteralInitialBindingFiltersResults() {
+ insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob"));
+ insert(iri(ALICE), iri(NAME), valueFactory.createLiteral("Alice"));
+
+ BindingSet bindings = singleBinding("name", valueFactory.createLiteral("Bob"));
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT ?s WHERE { ?s <" + NAME + "> ?name }",
+ bindings, null, 0L);
+
+ assertTrue(result.hasNext());
+ assertEquals(BOB, result.next().getValue("s").stringValue());
+ assertFalse(result.hasNext(), "Only the triple with literal 'Bob' should match");
+ }
+
// -------------------------------------------------------------------------
// Timeout
// -------------------------------------------------------------------------
diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/NodeImplFactoryTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/NodeImplFactoryTest.java
new file mode 100644
index 000000000..44f692840
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/NodeImplFactoryTest.java
@@ -0,0 +1,106 @@
+package fr.inria.corese.core.next.query.kgram.tool;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisplayName("NodeImpl factory methods: forIRI, forBlank, forLiteral, forVariable")
+class NodeImplFactoryTest {
+
+ @Test
+ @DisplayName("forIRI creates a constant non-variable non-blank node with the given label")
+ void forIRICreatesConstantIRINode() {
+ NodeImpl node = NodeImpl.forIRI("http://example.org/alice");
+
+ assertTrue(node.isConstant());
+ assertFalse(node.isVariable());
+ assertFalse(node.isBlank());
+ assertEquals("http://example.org/alice", node.getLabel());
+ }
+
+ @Test
+ @DisplayName("forIRI: two nodes with the same URI compare as the same RDF term")
+ void twoIRINodesWithSameURIAreSameTerm() {
+ NodeImpl a = NodeImpl.forIRI("http://example.org/alice");
+ NodeImpl b = NodeImpl.forIRI("http://example.org/alice");
+
+ assertNotSame(a, b);
+ assertTrue(a.same(b));
+ assertEquals(0, a.compare(b));
+ }
+
+ @Test
+ @DisplayName("forIRI: two nodes with different URIs are not the same RDF term")
+ void twoIRINodesWithDifferentURIAreNotSame() {
+ NodeImpl a = NodeImpl.forIRI("http://example.org/alice");
+ NodeImpl b = NodeImpl.forIRI("http://example.org/bob");
+
+ assertFalse(a.same(b));
+ }
+
+ @Test
+ @DisplayName("forBlank creates a constant blank node with the given ID as label")
+ void forBlankCreatesBlankNode() {
+ NodeImpl node = NodeImpl.forBlank("b42");
+
+ assertTrue(node.isConstant());
+ assertTrue(node.isBlank());
+ assertFalse(node.isVariable());
+ assertEquals("b42", node.getLabel());
+ }
+
+ @Test
+ @DisplayName("forLiteral creates a constant non-blank non-variable node with the given label")
+ void forLiteralCreatesLiteralNode() {
+ NodeImpl node = NodeImpl.forLiteral("hello", "http://www.w3.org/2001/XMLSchema#string", null);
+
+ assertTrue(node.isConstant());
+ assertFalse(node.isVariable());
+ assertFalse(node.isBlank());
+ assertEquals("hello", node.getLabel());
+ }
+
+ @Test
+ @DisplayName("forLiteral with lang tag stores the label correctly")
+ void forLiteralWithLangStoresLabel() {
+ NodeImpl node = NodeImpl.forLiteral("bonjour", null, "fr");
+
+ assertEquals("bonjour", node.getLabel());
+ assertTrue(node.isConstant());
+ assertFalse(node.isBlank());
+ }
+
+ @Test
+ @DisplayName("forVariable creates a variable non-constant node with the given name")
+ void forVariableCreatesVariableNode() {
+ NodeImpl node = NodeImpl.forVariable("x");
+
+ assertTrue(node.isVariable());
+ assertFalse(node.isConstant());
+ assertFalse(node.isBlank());
+ assertEquals("x", node.getLabel());
+ }
+
+ @Test
+ @DisplayName("forVariable: two variables with the same name compare as same")
+ void twoVariablesWithSameNameAreSame() {
+ NodeImpl a = NodeImpl.forVariable("x");
+ NodeImpl b = NodeImpl.forVariable("x");
+
+ assertNotSame(a, b);
+ assertTrue(a.same(b));
+ }
+
+ @Test
+ @DisplayName("forVariable: two variables with different names are not same")
+ void twoVariablesWithDifferentNamesAreNotSame() {
+ NodeImpl a = NodeImpl.forVariable("x");
+ NodeImpl b = NodeImpl.forVariable("y");
+
+ assertFalse(a.same(b));
+ }
+}