From 5a544a87a4eeab66889fb3914920eb17b074fae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Mon, 6 Jul 2026 16:16:11 +0200 Subject: [PATCH 1/9] feat(next): connect KGRAM producer to StorageManager --- .../sparql/bridge/CoreseAstQueryBuilder.java | 11 +- .../query/kgram/tool/StorageManagerEdge.java | 87 +++ .../kgram/tool/StorageManagerProducer.java | 620 ++++++++++++++++++ .../tool/StorageManagerProducerTest.java | 388 +++++++++++ 4 files changed, 1105 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index ce4e43c57..526f43f31 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -237,7 +237,9 @@ private Query createQuery( GroupGraphPatternAst whereClause, DatasetClauseAst datasetClause, SolutionModifierAst solutionModifier) { - Query query = Query.create(whereCompiler.compile(whereClause)); + Exp body = whereCompiler.compile(whereClause); + markEvaluable(body); + Query query = Query.create(body); // Collect visible nodes once so later clauses (projection, ORDER BY, DESCRIBE) // can resolve variables against the compiled runtime body. query.collect(); @@ -246,6 +248,13 @@ private Query createQuery( return query; } + private static void markEvaluable(Exp exp) { + exp.setFail(true); + for (Exp child : exp) { + markEvaluable(child); + } + } + private void applyDataset(Query query, DatasetClauseAst datasetClause) { query.setFrom(toNodeList(datasetClause.graphs())); query.setNamed(toNodeList(datasetClause.namedGraphs())); diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java new file mode 100644 index 000000000..6b1ff2c7d --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java @@ -0,0 +1,87 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +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.impl.temp.CoreseValueConverter; +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.sparql.triple.parser.Constant; + +import java.util.Objects; + +/** + * KGRAM edge view over a statement returned by the Corese-next storage layer. + * + *

KGRAM models a triple edge with the subject at {@code getNode(0)}, the object at + * {@code getNode(1)}, and the predicate as the edge node. Keeping that contract here + * lets the storage-backed producer feed regular SPO patterns into the next evaluator.

+ */ +public final class StorageManagerEdge implements Edge { + + private static final CoreseValueConverter VALUE_CONVERTER = new CoreseValueConverter(); + + private final Statement statement; + private final Node subject; + private final Node predicate; + private final Node object; + private final Node graph; + + public StorageManagerEdge(Statement statement) { + this.statement = Objects.requireNonNull(statement, "statement"); + this.subject = node(statement.getSubject()); + this.predicate = node(statement.getPredicate()); + this.object = node(statement.getObject()); + this.graph = statement.getContext() == null ? null : node(statement.getContext()); + } + + public Statement getSourceStatement() { + return statement; + } + + @Override + public Node getNode(int i) { + return switch (i) { + case 0 -> subject; + case 1 -> object; + default -> throw new IndexOutOfBoundsException( + "Statement edge has nodes 0 (subject) and 1 (object), got: " + i); + }; + } + + @Override + public Node getProperty() { + return predicate; + } + + @Override + public String getEdgeLabel() { + return predicate.getLabel(); + } + + @Override + public Node getGraph() { + return graph; + } + + @Override + public boolean contains(Node node) { + if (node == null) { + return false; + } + return sameNode(subject, node) || sameNode(predicate, node) || sameNode(object, node); + } + + @Override + public Edge getEdge() { + return this; + } + + private static boolean sameNode(Node left, Node right) { + return left == right || left.same(right); + } + + private static Node node(Value value) { + fr.inria.corese.core.kgram.api.core.Node node = VALUE_CONVERTER.toCoreseNode(value); + return new NodeImpl(Constant.create(node.getDatatypeValue())); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java new file mode 100644 index 000000000..274f24ece --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -0,0 +1,620 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +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.Resource; +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.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.query.kgram.api.core.Edge; +import fr.inria.corese.core.next.query.kgram.api.core.Graph; +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.api.core.Regex; +import fr.inria.corese.core.next.query.kgram.api.query.Environment; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.core.Mapping; +import fr.inria.corese.core.next.query.kgram.core.Mappings; +import fr.inria.corese.core.next.query.kgram.core.Query; +import fr.inria.corese.core.next.storagemanager.api.StorageManager; +import fr.inria.corese.core.next.storagemanager.api.support.model.StatementPattern; +import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.sparql.triple.parser.Constant; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * KGRAM producer backed by the Corese-next {@link StorageManager}. + */ +public final class StorageManagerProducer extends ProducerDefault { + + private final StorageManager storage; + private final ValueFactory valueFactory; + + public StorageManagerProducer(StorageManager storage) { + this(storage, new CoreseAdaptedValueFactory()); + } + + StorageManagerProducer(StorageManager storage, ValueFactory valueFactory) { + this.storage = Objects.requireNonNull(storage, "storage"); + this.valueFactory = Objects.requireNonNull(valueFactory, "valueFactory"); + } + + @Override + public Iterable getEdges(Node graphNode, List from, Edge queryEdge, Environment environment) { + Objects.requireNonNull(queryEdge, "queryEdge"); + + StorageQueryPattern queryPattern = queryPattern(graphNode, from, queryEdge, environment); + if (queryPattern.noMatch()) { + return List.of(); + } + + try (Stream statements = + storage.getQueryOperations().query(queryPattern.statementPattern())) { + return statements + .map(StorageManagerEdge::new) + .map(Edge.class::cast) + .toList(); + } + } + + @Override + public Iterable getGraphNodes(Node graphNode, List from, Environment environment) { + List nodes = new ArrayList<>(); + for (Resource context : storage.getMetadataOperations().getContexts()) { + Node node = node(context); + if (matchesFrom(node, from, environment)) { + nodes.add(node); + } + } + return nodes; + } + + @Override + public Iterable getEdges( + Node graphNode, + List from, + Edge queryEdge, + Environment environment, + Regex regex, + Node source, + Node start, + int index) { + return List.of(); + } + + @Override + public void start(Query query) { + // No per-query storage state is required. + } + + @Override + public void finish(Query query) { + // No per-query storage state is required. + } + + @Override + public Node getNode(Object value) { + if (value instanceof Node node) { + return node; + } + if (value instanceof Value rdfValue) { + return node(rdfValue); + } + if (value instanceof IDatatype datatype) { + return node(datatype); + } + return null; + } + + @Override + public IDatatype getValue(Object value) { + return getDatatypeValue(value); + } + + @Override + public IDatatype getDatatypeValue(Object value) { + if (value instanceof Node node) { + return node.getDatatypeValue(); + } + if (value instanceof Value rdfValue) { + return node(rdfValue).getDatatypeValue(); + } + if (value instanceof IDatatype datatype) { + return datatype; + } + return null; + } + + @Override + public boolean isBindable(Node node) { + return node != null && node.isVariable(); + } + + /** + * Materializes mappings for basic graph patterns made of triple {@code EDGE} expressions. + * + *

Higher-level algebra remains handled by {@link fr.inria.corese.core.next.query.kgram.core.Eval}; + * paths, services and values are explicit follow-ups.

+ */ + @Override + public Mappings getMappings(Node graphNode, List from, Exp exp, Environment environment) { + if (!exp.isBGP()) { + throw new UnsupportedOperationException("StorageManagerProducer only supports BGP mappings"); + } + + List bindings = new ArrayList<>(); + bindings.add(new BindingSet()); + for (Exp element : exp) { + if (!element.isEdge()) { + throw new UnsupportedOperationException( + "StorageManagerProducer only supports EDGE expressions inside BGP mappings"); + } + bindings = join(graphNode, from, element.getEdge(), environment, bindings); + if (bindings.isEmpty()) { + break; + } + } + + Mappings mappings = Mappings.create(environment.getQuery()); + for (BindingSet binding : bindings) { + mappings.add(binding.toMapping()); + } + return mappings; + } + + @Override + public Graph getGraph() { + return null; + } + + private StorageQueryPattern queryPattern(Node graphNode, List from, Edge queryEdge, Environment environment) { + Node subjectNode = resolve(queryEdge.getNode(0), environment); + Node predicateNode = resolve(predicateQueryNode(queryEdge), environment); + Node objectNode = resolve(queryEdge.getNode(1), environment); + + Resource subject = null; + IRI predicate = null; + Value object = null; + + if (subjectNode != null) { + Value value = rdfValue(subjectNode); + if (!(value instanceof Resource resource)) { + return StorageQueryPattern.emptyResult(); + } + subject = resource; + } + if (predicateNode != null) { + Value value = rdfValue(predicateNode); + if (!(value instanceof IRI iri)) { + return StorageQueryPattern.emptyResult(); + } + predicate = iri; + } + if (objectNode != null) { + object = rdfValue(objectNode); + } + + ContextSelection contextSelection = contextSelection(graphNode, from, environment); + if (contextSelection.noMatch()) { + return StorageQueryPattern.emptyResult(); + } + return StorageQueryPattern.of(StatementPattern.of( + subject, + predicate, + object, + contextSelection.contextsArray())); + } + + private Node predicateQueryNode(Edge queryEdge) { + return queryEdge.getEdgeVariable() == null ? queryEdge.getEdgeNode() : queryEdge.getEdgeVariable(); + } + + private ContextSelection contextSelection(Node graphNode, List from, Environment environment) { + if (graphNode != null) { + Node resolvedGraphNode = resolve(graphNode, environment); + if (resolvedGraphNode == null) { + return ContextSelection.allContexts(); + } + Value value = rdfValue(resolvedGraphNode); + if (!(value instanceof Resource resource)) { + return ContextSelection.emptyResult(); + } + return ContextSelection.of(List.of(resource)); + } + + if (from == null || from.isEmpty()) { + return ContextSelection.allContexts(); + } + + List contexts = new ArrayList<>(); + for (Node node : from) { + Node resolvedNode = resolve(node, environment); + if (resolvedNode == null) { + continue; + } + Value value = rdfValue(resolvedNode); + if (!(value instanceof Resource resource)) { + return ContextSelection.emptyResult(); + } + contexts.add(resource); + } + return ContextSelection.of(contexts); + } + + private boolean matchesFrom(Node graphNode, List from, Environment environment) { + if (from == null || from.isEmpty()) { + return true; + } + for (Node fromNode : from) { + Node resolvedNode = resolve(fromNode, environment); + if (resolvedNode != null && resolvedNode.match(graphNode)) { + return true; + } + } + return false; + } + + private Node resolve(Node queryNode, Environment environment) { + if (queryNode == null) { + return null; + } + if (queryNode.isConstant()) { + return queryNode; + } + return environment == null ? null : environment.getNode(queryNode); + } + + private List join( + Node graphNode, + List from, + Edge queryEdge, + Environment environment, + List inputBindings) { + List results = new ArrayList<>(); + for (BindingSet binding : inputBindings) { + Environment joinedEnvironment = new BindingEnvironment(environment, binding); + for (Edge candidate : getEdges(graphNode, from, queryEdge, joinedEnvironment)) { + BindingSet next = binding.copy(); + if (next.bind(queryEdge.getNode(0), candidate.getNode(0)) + && next.bind(queryEdge.getNode(1), candidate.getNode(1)) + && next.bind(queryEdge.getEdgeVariable(), candidate.getEdgeNode())) { + results.add(next); + } + } + } + return results; + } + + private Node node(Value value) { + return node(datatypeValue(value)); + } + + private static Node node(IDatatype datatype) { + return new NodeImpl(Constant.create(datatype)); + } + + private Value rdfValue(Node node) { + return rdfValue(node.getDatatypeValue()); + } + + private Value rdfValue(IDatatype datatype) { + if (datatype.isURI()) { + return valueFactory.createIRI(datatype.getLabel()); + } + if (datatype.isBlank()) { + return valueFactory.createBNode(datatype.getLabel()); + } + if (datatype.isLiteral()) { + String language = datatype.getLang(); + if (language != null && !language.isEmpty()) { + return valueFactory.createLiteral(datatype.getLabel(), language); + } + String datatypeUri = datatype.getDatatypeURI(); + if (datatypeUri != null && !datatypeUri.isEmpty()) { + return valueFactory.createLiteral(datatype.getLabel(), valueFactory.createIRI(datatypeUri)); + } + return valueFactory.createLiteral(datatype.getLabel()); + } + throw new IllegalArgumentException("Unsupported KGRAM datatype value: " + datatype); + } + + private IDatatype datatypeValue(Value value) { + if (value instanceof IRI iri) { + return fr.inria.corese.core.sparql.datatype.DatatypeMap.newResource(iri.stringValue()); + } + if (value instanceof BNode bNode) { + return fr.inria.corese.core.sparql.datatype.DatatypeMap.createBlank(bNode.getID()); + } + if (value instanceof Literal literal) { + return literal.getLanguage() + .map(language -> fr.inria.corese.core.sparql.datatype.DatatypeMap + .createLiteral(literal.getLabel(), null, language)) + .orElseGet(() -> fr.inria.corese.core.sparql.datatype.DatatypeMap + .createLiteral(literal.getLabel(), literal.getDatatype().stringValue(), null)); + } + throw new IllegalArgumentException("Unsupported RDF value: " + value); + } + + private record StorageQueryPattern(StatementPattern statementPattern, boolean noMatch) { + + private static StorageQueryPattern of(StatementPattern statementPattern) { + return new StorageQueryPattern(statementPattern, false); + } + + private static StorageQueryPattern emptyResult() { + return new StorageQueryPattern(StatementPattern.matchAll(), true); + } + } + + private record ContextSelection(List contexts, boolean noMatch) { + + private static ContextSelection of(List contexts) { + return new ContextSelection(List.copyOf(contexts), false); + } + + private static ContextSelection allContexts() { + return of(List.of()); + } + + private static ContextSelection emptyResult() { + return new ContextSelection(List.of(), true); + } + + private Resource[] contextsArray() { + return contexts.toArray(Resource[]::new); + } + } + + private static final class BindingSet { + + private final List queryNodes = new ArrayList<>(); + private final List targetNodes = new ArrayList<>(); + + private BindingSet copy() { + BindingSet copy = new BindingSet(); + copy.queryNodes.addAll(queryNodes); + copy.targetNodes.addAll(targetNodes); + return copy; + } + + private boolean bind(Node queryNode, Node targetNode) { + if (queryNode == null || queryNode.isConstant()) { + return true; + } + Node current = get(queryNode); + if (current == null) { + queryNodes.add(queryNode); + targetNodes.add(targetNode); + return true; + } + return current.match(targetNode); + } + + private Node get(Node queryNode) { + if (queryNode == null) { + return null; + } + for (int i = 0; i < queryNodes.size(); i++) { + if (queryNodes.get(i) == queryNode || queryNodes.get(i).same(queryNode)) { + return targetNodes.get(i); + } + } + return null; + } + + private Mapping toMapping() { + return Mapping.create(queryNodes, targetNodes); + } + } + + private static final class BindingEnvironment implements Environment { + + private final Environment delegate; + private final BindingSet bindings; + + private BindingEnvironment(Environment delegate, BindingSet bindings) { + this.delegate = delegate; + this.bindings = bindings; + } + + @Override + public Node getNode(Node queryNode) { + Node node = bindings.get(queryNode); + if (node != null) { + return node; + } + return delegate == null ? null : delegate.getNode(queryNode); + } + + @Override + public Query getQuery() { + return delegate == null ? null : delegate.getQuery(); + } + + @Override + public fr.inria.corese.core.next.query.kgram.api.core.BindingContext getBind() { + return delegate == null ? null : delegate.getBind(); + } + + @Override + public void setBind(fr.inria.corese.core.next.query.kgram.api.core.BindingContext bindingContext) { + if (delegate != null) { + delegate.setBind(bindingContext); + } + } + + @Override + public boolean hasBind() { + return delegate != null && delegate.hasBind(); + } + + @Override + public Node getGraphNode() { + return delegate == null ? null : delegate.getGraphNode(); + } + + @Override + public Node getNode(fr.inria.corese.core.next.query.kgram.api.core.Expr varExpr) { + return delegate == null ? null : delegate.getNode(varExpr); + } + + @Override + public Node getNode(String label) { + return delegate == null ? null : delegate.getNode(label); + } + + @Override + public Node getQueryNode(int n) { + return delegate == null ? null : delegate.getQueryNode(n); + } + + @Override + public Node getQueryNode(String label) { + return delegate == null ? null : delegate.getQueryNode(label); + } + + @Override + public boolean isBound(Node queryNode) { + return getNode(queryNode) != null; + } + + @Override + public int pathLength(Node queryNode) { + return delegate == null ? 0 : delegate.pathLength(queryNode); + } + + @Override + public fr.inria.corese.core.next.query.kgram.path.Path getPath(Node queryNode) { + return delegate == null ? null : delegate.getPath(queryNode); + } + + @Override + public int count() { + return delegate == null ? 0 : delegate.count(); + } + + @Override + public fr.inria.corese.core.next.query.kgram.event.EventManager getEventManager() { + return delegate == null ? null : delegate.getEventManager(); + } + + @Override + public Object getObject() { + return delegate == null ? null : delegate.getObject(); + } + + @Override + public void setObject(Object object) { + if (delegate != null) { + delegate.setObject(object); + } + } + + @Override + public Exp getExp() { + return delegate == null ? null : delegate.getExp(); + } + + @Override + public void setExp(Exp exp) { + if (delegate != null) { + delegate.setExp(exp); + } + } + + @Override + public java.util.Map getMap() { + return delegate == null ? java.util.Map.of() : delegate.getMap(); + } + + @Override + public Edge[] getEdges() { + return delegate == null ? new Edge[0] : delegate.getEdges(); + } + + @Override + public Node[] getNodes() { + return delegate == null ? new Node[0] : delegate.getNodes(); + } + + @Override + public Node[] getQueryNodes() { + return delegate == null ? new Node[0] : delegate.getQueryNodes(); + } + + @Override + public Mappings getMappings() { + return delegate == null ? null : delegate.getMappings(); + } + + @Override + public Mapping getMapping() { + return delegate == null ? null : delegate.getMapping(); + } + + @Override + public Iterable getAggregate() { + return delegate == null ? List.of() : delegate.getAggregate(); + } + + @Override + public void aggregate(Mapping mapping, int n) { + if (delegate != null) { + delegate.aggregate(mapping, n); + } + } + + @Override + public Node get(fr.inria.corese.core.next.query.kgram.api.core.Expr varExpr) { + return delegate == null ? null : delegate.get(varExpr); + } + + @Override + public fr.inria.corese.core.sparql.triple.parser.ASTExtension getExtension() { + return delegate == null ? null : delegate.getExtension(); + } + + @Override + public ApproximateSearchEnv getAppxSearchEnv() { + return delegate == null ? null : delegate.getAppxSearchEnv(); + } + + @Override + public fr.inria.corese.core.next.query.kgram.core.Eval getEval() { + return delegate == null ? null : delegate.getEval(); + } + + @Override + public void setEval(fr.inria.corese.core.next.query.kgram.core.Eval eval) { + if (delegate != null) { + delegate.setEval(eval); + } + } + + @Override + public fr.inria.corese.core.next.query.kgram.api.query.ProcessVisitor getVisitor() { + return delegate == null ? null : delegate.getVisitor(); + } + + @Override + public IDatatype getReport() { + return delegate == null ? null : delegate.getReport(); + } + + @Override + public void setReport(IDatatype datatype) { + if (delegate != null) { + delegate.setReport(datatype); + } + } + + @Override + public int size() { + return delegate == null ? 0 : delegate.size(); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java new file mode 100644 index 000000000..d45dcc149 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java @@ -0,0 +1,388 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +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.Value; +import fr.inria.corese.core.next.data.api.ValueFactory; +import fr.inria.corese.core.next.data.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.query.impl.parser.SparqlParser; +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.kgram.api.core.Edge; +import fr.inria.corese.core.next.query.kgram.api.core.ExpType.Type; +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.api.query.Environment; +import fr.inria.corese.core.next.query.kgram.api.query.Evaluator; +import fr.inria.corese.core.next.query.kgram.api.query.Matcher; +import fr.inria.corese.core.next.query.kgram.core.Eval; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.core.Mappings; +import fr.inria.corese.core.next.query.kgram.core.Query; +import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager; +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class StorageManagerProducerTest { + + private static final String ALICE = "http://example.org/alice"; + private static final String BOB = "http://example.org/bob"; + private static final String CAROL = "http://example.org/carol"; + private static final String KNOWS = "http://example.org/knows"; + private static final String NAME = "http://example.org/name"; + private static final String GRAPH = "http://example.org/graph"; + + private ValueFactory valueFactory; + private MemoryStorageManager storage; + private StorageManagerProducer producer; + + @BeforeEach + void setUp() { + valueFactory = new CoreseAdaptedValueFactory(); + storage = MemoryStorageManager.builder().build(); + producer = new StorageManagerProducer(storage); + + insert(iri(ALICE), iri(KNOWS), iri(BOB)); + insert(iri(ALICE), iri(KNOWS), iri(CAROL)); + insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob")); + insert(iri(CAROL), iri(NAME), valueFactory.createLiteral("Carol"), iri(GRAPH)); + } + + @Test + @DisplayName("Unbound ?s ?p ?o enumerates every storage statement as KGRAM edges") + void unboundSpoEnumeratesEveryStatement() { + Node s = variable("s"); + Node p = variable("p"); + Node o = variable("o"); + + List edges = edges(new QueryEdge(s, KgramNodes.rootProperty(), p, o)); + + assertEquals(4, edges.size()); + assertTrue(edges.stream().anyMatch(edge -> + ALICE.equals(edge.getNode(0).getLabel()) + && KNOWS.equals(edge.getEdgeNode().getLabel()) + && BOB.equals(edge.getNode(1).getLabel()))); + } + + @Test + @DisplayName("Constant predicate is pushed into the StatementPattern") + void constantPredicateFiltersStorageStatements() { + Node s = variable("s"); + Node o = variable("o"); + + List edges = edges(new QueryEdge(s, resource(KNOWS), null, o)); + + assertEquals(2, edges.size()); + assertTrue(edges.stream().allMatch(edge -> KNOWS.equals(edge.getEdgeNode().getLabel()))); + assertIterableEquals(List.of(BOB, CAROL), edges.stream().map(edge -> edge.getNode(1).getLabel()).toList()); + } + + @Test + @DisplayName("Bound predicate variable is pushed into the StatementPattern") + void boundPredicateVariableFiltersStorageStatements() { + Node s = variable("s"); + Node p = variable("p"); + Node o = variable("o"); + Environment environment = mock(Environment.class); + when(environment.getNode(p)).thenReturn(resource(NAME)); + + List edges = edges(new QueryEdge(s, KgramNodes.rootProperty(), p, o), null, List.of(), environment); + + assertEquals(2, edges.size()); + assertTrue(edges.stream().allMatch(edge -> NAME.equals(edge.getEdgeNode().getLabel()))); + } + + @Test + @DisplayName("Graph node restricts the StatementPattern context") + void graphNodeRestrictsContext() { + Node s = variable("s"); + Node o = variable("o"); + + List edges = edges(new QueryEdge(s, resource(NAME), null, o), resource(GRAPH), List.of(), null); + + assertEquals(1, edges.size()); + Edge edge = edges.getFirst(); + assertEquals(CAROL, edge.getNode(0).getLabel()); + assertEquals("Carol", edge.getNode(1).getLabel()); + assertEquals(GRAPH, edge.getGraph().getLabel()); + } + + @Test + @DisplayName("FROM nodes restrict the StatementPattern contexts") + void fromNodesRestrictContexts() { + Node s = variable("s"); + Node o = variable("o"); + + List edges = edges(new QueryEdge(s, resource(NAME), null, o), null, List.of(resource(GRAPH)), null); + + assertEquals(1, edges.size()); + assertEquals(CAROL, edges.getFirst().getNode(0).getLabel()); + } + + @Test + @DisplayName("Invalid predicate binding returns no candidate edges") + void invalidPredicateBindingReturnsNoEdges() { + Node s = variable("s"); + Node p = variable("p"); + Node o = variable("o"); + Environment environment = mock(Environment.class); + when(environment.getNode(p)).thenReturn(literal("not-a-predicate")); + + List edges = edges(new QueryEdge(s, KgramNodes.rootProperty(), p, o), null, List.of(), environment); + + assertTrue(edges.isEmpty()); + } + + @Test + @DisplayName("Eval executes SELECT * WHERE { ?s ?p ?o } over StorageManagerProducer") + void evalExecutesSelectStarSpoOverStorageManagerProducer() throws Exception { + Node s = variable("s"); + Node p = variable("p"); + Node o = variable("o"); + Query query = selectStarSpoQuery(s, p, o); + Eval eval = Eval.create(producer, new NoOpEvaluator(), new BasicMatcher()); + + Mappings mappings = eval.query(query); + + assertEquals(4, mappings.size()); + assertContainsMapping(mappings, ALICE, KNOWS, BOB); + } + + @Test + @DisplayName("Parser and bridge execute SELECT * WHERE { ?s ?p ?o } over StorageManagerProducer") + void parserBridgeExecutesSelectStarSpoOverStorageManagerProducer() throws Exception { + QueryAst ast = new SparqlParser().parse("SELECT * WHERE { ?s ?p ?o }"); + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast); + Query query = new CoreseAstQueryBuilder().toNextQuery(select); + Eval eval = Eval.create(producer, new NoOpEvaluator(), new BasicMatcher()); + + Mappings mappings = eval.query(query); + + assertEquals(4, mappings.size()); + assertContainsMapping(mappings, ALICE, KNOWS, BOB); + } + + @Test + @DisplayName("Producer materializes BGP mappings for SELECT * WHERE { ?s ?p ?o }") + void producerMaterializesBgpMappings() { + Node s = variable("s"); + Node p = variable("p"); + Node o = variable("o"); + Query query = selectStarSpoQuery(s, p, o); + Environment environment = mock(Environment.class); + when(environment.getQuery()).thenReturn(query); + + assertEquals(1, query.getBody().size()); + Exp bgp = query.getBody().first(); + assertEquals(1, bgp.size()); + assertEquals(4, edges(bgp.first().getEdge()).size()); + Mappings mappings = producer.getMappings(null, List.of(), bgp, environment); + + assertEquals(4, mappings.size()); + assertContainsMapping(mappings, ALICE, KNOWS, BOB); + } + + private static void assertContainsMapping(Mappings mappings, String subject, String predicate, String object) { + assertTrue(mappings.getMappingList().stream().anyMatch(mapping -> + subject.equals(mapping.getValue("s").getLabel()) + && predicate.equals(mapping.getValue("p").getLabel()) + && object.equals(mapping.getValue("o").getLabel()))); + } + + private void insert(Resource subject, IRI predicate, Value object, Resource... contexts) { + if (contexts == null || contexts.length == 0) { + storage.getMutationOperations().insertStatement(valueFactory.createStatement(subject, predicate, object)); + return; + } + for (Resource context : contexts) { + storage.getMutationOperations().insertStatement( + valueFactory.createStatement(subject, predicate, object, context)); + } + } + + private IRI iri(String iri) { + return valueFactory.createIRI(iri); + } + + private List edges(Edge queryEdge) { + return edges(queryEdge, null, List.of(), null); + } + + private List edges(Edge queryEdge, Node graphNode, List from, Environment environment) { + List result = new ArrayList<>(); + producer.getEdges(graphNode, from, queryEdge, environment).forEach(result::add); + return result; + } + + private static Node variable(String name) { + return new NodeImpl(Variable.create(name)); + } + + private static Node resource(String iri) { + return new NodeImpl(Constant.create(DatatypeMap.newResource(iri))); + } + + private static Node literal(String label) { + return new NodeImpl(Constant.create(DatatypeMap.newLiteral(label))); + } + + private static Query selectStarSpoQuery(Node s, Node p, Node o) { + Exp body = Exp.create(Type.AND); + Exp bgp = Exp.create(Type.BGP); + bgp.add(Exp.create(Type.EDGE, new QueryEdge(s, KgramNodes.rootProperty(), p, o))); + body.add(bgp); + markEvaluable(body); + Query query = Query.create(body); + query.collect(); + List select = query.selectNodesFromPattern(); + query.setSelect(select); + query.setSelectFun(select.stream().map(node -> Exp.create(Type.NODE, node)).toList()); + return query; + } + + private static void markEvaluable(Exp exp) { + exp.setFail(true); + for (Exp child : exp) { + markEvaluable(child); + } + } + + private static final class BasicMatcher implements Matcher { + + private int mode = Matcher.UNDEF; + + @Override + public boolean match(Edge query, Edge target, Environment environment) { + return match(query.getNode(0), target.getNode(0), environment) + && match(query.getNode(1), target.getNode(1), environment) + && match(predicateNode(query), target.getEdgeNode(), environment); + } + + @Override + public boolean match(Node query, Node target, Environment environment) { + if (query == null || target == null) { + return query == target; + } + if (query.isVariable()) { + Node bound = environment == null ? null : environment.getNode(query); + return bound == null || bound.match(target); + } + return query.match(target); + } + + @Override + public boolean same(Node queryNode, Node left, Node right, Environment environment) { + return left != null && left.same(right); + } + + @Override + public int getMode() { + return mode; + } + + @Override + public void setMode(int mode) { + this.mode = mode; + } + + private static Node predicateNode(Edge edge) { + return edge.getEdgeVariable() == null ? edge.getEdgeNode() : edge.getEdgeVariable(); + } + } + + private static final class NoOpEvaluator implements Evaluator { + + private Mode mode = Mode.KGRAM_MODE; + + @Override + public Mode getMode() { + return mode; + } + + @Override + public void setMode(Mode mode) { + this.mode = mode; + } + + @Override + public void setProducer(fr.inria.corese.core.next.query.kgram.api.query.Producer producer) { + // BGP-only test support does not evaluate filters. + } + + @Override + public void setKGRAM(Eval eval) { + // BGP-only test support does not evaluate filters. + } + + @Override + public void start(Environment environment) { + // BGP-only test support does not evaluate filters. + } + + @Override + public void finish(Environment environment) { + // BGP-only test support does not evaluate filters. + } + + @Override + public void init(Environment environment) { + // BGP-only test support does not evaluate filters. + } + } + + private record QueryEdge(Node subject, Node edgeNode, Node edgeVariable, Node object) implements Edge { + + @Override + public Node getNode(int i) { + return switch (i) { + case 0 -> subject; + case 1 -> object; + default -> throw new IndexOutOfBoundsException(); + }; + } + + @Override + public Node getEdgeNode() { + return edgeNode; + } + + @Override + public Node getEdgeVariable() { + return edgeVariable; + } + + @Override + public Node getProperty() { + return edgeVariable == null ? edgeNode : edgeVariable; + } + + @Override + public String getEdgeLabel() { + return edgeNode.getLabel(); + } + + @Override + public Node getGraph() { + return null; + } + + @Override + public Edge getEdge() { + return this; + } + } +} From bd3f894d67cff4d1fa40a89a2cc6e02d0bf32280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 7 Jul 2026 11:03:05 +0200 Subject: [PATCH 2/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../core/next/query/kgram/tool/StorageManagerProducer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java index 274f24ece..1237e3ee6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -83,7 +83,7 @@ public Iterable getEdges( Node source, Node start, int index) { - return List.of(); + throw new UnsupportedOperationException("StorageManagerProducer does not support path/regex edge enumeration yet"); } @Override From 04af05f20a0e6c1d829d03023b56ef6ecde3943b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 9 Jul 2026 11:14:45 +0200 Subject: [PATCH 3/9] fix(next): restore KGRAM fail semantics --- .../impl/sparql/bridge/CoreseAstQueryBuilder.java | 11 +---------- .../inria/corese/core/next/query/kgram/core/Eval.java | 6 ++++-- .../query/kgram/tool/StorageManagerProducerTest.java | 8 -------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index 526f43f31..ce4e43c57 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -237,9 +237,7 @@ private Query createQuery( GroupGraphPatternAst whereClause, DatasetClauseAst datasetClause, SolutionModifierAst solutionModifier) { - Exp body = whereCompiler.compile(whereClause); - markEvaluable(body); - Query query = Query.create(body); + Query query = Query.create(whereCompiler.compile(whereClause)); // Collect visible nodes once so later clauses (projection, ORDER BY, DESCRIBE) // can resolve variables against the compiled runtime body. query.collect(); @@ -248,13 +246,6 @@ private Query createQuery( return query; } - private static void markEvaluable(Exp exp) { - exp.setFail(true); - for (Exp child : exp) { - markEvaluable(child); - } - } - private void applyDataset(Query query, DatasetClauseAst datasetClause) { query.setFrom(toNodeList(datasetClause.graphs())); query.setNamed(toNodeList(datasetClause.namedGraphs())); diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Eval.java b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Eval.java index 2066eafc4..f0e6e398c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Eval.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Eval.java @@ -845,7 +845,9 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp send(Event.START, exp, graphNode, stack); } - if (exp.isFail()) { + // A false filter was detected at compile time, or this expression was + // identified as always failing: no use to evaluate it. + if (!exp.isFail()) { if (exp.isBGPAble()) { // @deprecated // evaluate and record result for next time @@ -2092,4 +2094,4 @@ public void setListener(ResultListener listener) { this.listener = listener; } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java index d45dcc149..92c6ac735 100644 --- a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java @@ -245,7 +245,6 @@ private static Query selectStarSpoQuery(Node s, Node p, Node o) { Exp bgp = Exp.create(Type.BGP); bgp.add(Exp.create(Type.EDGE, new QueryEdge(s, KgramNodes.rootProperty(), p, o))); body.add(bgp); - markEvaluable(body); Query query = Query.create(body); query.collect(); List select = query.selectNodesFromPattern(); @@ -254,13 +253,6 @@ private static Query selectStarSpoQuery(Node s, Node p, Node o) { return query; } - private static void markEvaluable(Exp exp) { - exp.setFail(true); - for (Exp child : exp) { - markEvaluable(child); - } - } - private static final class BasicMatcher implements Matcher { private int mode = Matcher.UNDEF; From 7e7ba6e810eed53c3d78d0365fe434357d500d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 9 Jul 2026 14:54:52 +0200 Subject: [PATCH 4/9] fix(next): honor named dataset for graph patterns --- .../kgram/tool/StorageManagerProducer.java | 16 +++++++++- .../tool/StorageManagerProducerTest.java | 31 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java index 1237e3ee6..2f2ce15d1 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -210,9 +210,20 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q } private Node predicateQueryNode(Edge queryEdge) { + // KGRAM stores variable predicates in edgeVariable; edgeNode may only be + // the technical root-property placeholder for ?s ?p ?o patterns. return queryEdge.getEdgeVariable() == null ? queryEdge.getEdgeNode() : queryEdge.getEdgeVariable(); } + /** + * Selects the storage contexts for the active graph pattern. + * + *

SPARQL evaluates {@code GRAPH { ... }} only when {@code } is a named graph + * in the active dataset; otherwise the graph pattern has no solution. + * + * @see SPARQL 1.1 Query + * Language - Evaluation of Graph + */ private ContextSelection contextSelection(Node graphNode, List from, Environment environment) { if (graphNode != null) { Node resolvedGraphNode = resolve(graphNode, environment); @@ -223,6 +234,9 @@ private ContextSelection contextSelection(Node graphNode, List from, Envir if (!(value instanceof Resource resource)) { return ContextSelection.emptyResult(); } + if (!matchesFrom(resolvedGraphNode, from, environment)) { + return ContextSelection.emptyResult(); + } return ContextSelection.of(List.of(resource)); } @@ -350,7 +364,7 @@ private static StorageQueryPattern emptyResult() { } } - private record ContextSelection(List contexts, boolean noMatch) { + private record ContextSelection(List contexts, boolean noMatch) { private static ContextSelection of(List contexts) { return new ContextSelection(List.copyOf(contexts), false); diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java index 92c6ac735..cd9d85216 100644 --- a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java @@ -121,6 +121,37 @@ void graphNodeRestrictsContext() { assertEquals(GRAPH, edge.getGraph().getLabel()); } + @Test + @DisplayName("Graph node is accepted when it belongs to the named dataset") + void graphNodeAllowedByFromRestrictsContext() { + Node s = variable("s"); + Node o = variable("o"); + + List edges = edges( + new QueryEdge(s, resource(NAME), null, o), + resource(GRAPH), + List.of(resource(GRAPH)), + null); + + assertEquals(1, edges.size()); + assertEquals(CAROL, edges.getFirst().getNode(0).getLabel()); + } + + @Test + @DisplayName("Graph node outside the named dataset returns no candidate edges") + void graphNodeOutsideFromReturnsNoEdges() { + Node s = variable("s"); + Node o = variable("o"); + + List edges = edges( + new QueryEdge(s, resource(NAME), null, o), + resource(GRAPH), + List.of(resource("http://example.org/other-graph")), + null); + + assertTrue(edges.isEmpty()); + } + @Test @DisplayName("FROM nodes restrict the StatementPattern contexts") void fromNodesRestrictContexts() { From 21f9104b109adedf65560722f4f4ef37c489f16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 9 Jul 2026 17:34:09 +0200 Subject: [PATCH 5/9] docs(next): add javadoc explanations for StorageManagerProducer, join and bindings --- .../kgram/tool/StorageManagerProducer.java | 130 +++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java index 2f2ce15d1..24ef57226 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -34,10 +34,21 @@ public final class StorageManagerProducer extends ProducerDefault { private final StorageManager storage; private final ValueFactory valueFactory; + /** + * Creates a KGRAM producer backed by the given storage manager. + * + * @param storage storage manager queried by this producer + */ public StorageManagerProducer(StorageManager storage) { this(storage, new CoreseAdaptedValueFactory()); } + /** + * Creates a producer with an explicit value factory. + * + * @param storage storage manager queried by this producer + * @param valueFactory factory used to convert KGRAM values to storage values + */ StorageManagerProducer(StorageManager storage, ValueFactory valueFactory) { this.storage = Objects.requireNonNull(storage, "storage"); this.valueFactory = Objects.requireNonNull(valueFactory, "valueFactory"); @@ -171,6 +182,19 @@ public Graph getGraph() { return null; } + /** + * Converts a KGRAM query edge into the storage-layer statement pattern. + * + *

Unbound KGRAM variables become {@code null} components, which the storage API + * interprets as wildcards. Impossible RDF combinations, such as a literal subject + * or predicate, are represented as an empty-result pattern. + * + * @param graphNode current GRAPH node, or {@code null} for the default graph context + * @param from active FROM/FROM NAMED restriction computed by KGRAM + * @param queryEdge KGRAM triple pattern to translate + * @param environment current bindings used to resolve already-bound variables + * @return a storage query pattern, or an empty-result marker when no RDF statement can match + */ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge queryEdge, Environment environment) { Node subjectNode = resolve(queryEdge.getNode(0), environment); Node predicateNode = resolve(predicateQueryNode(queryEdge), environment); @@ -180,6 +204,8 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q IRI predicate = null; Value object = null; + // Subject and predicate have stricter RDF roles than object: subject must + // be a resource, predicate must be an IRI, while object accepts any RDF value. if (subjectNode != null) { Value value = rdfValue(subjectNode); if (!(value instanceof Resource resource)) { @@ -198,6 +224,7 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q object = rdfValue(objectNode); } + // Graph and dataset clauses become the statement contexts passed to storage. ContextSelection contextSelection = contextSelection(graphNode, from, environment); if (contextSelection.noMatch()) { return StorageQueryPattern.emptyResult(); @@ -209,9 +236,16 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q contextSelection.contextsArray())); } + /** + * Returns the effective predicate node carried by a KGRAM edge. + * + *

KGRAM stores variable predicates in {@link Edge#getEdgeVariable()}; {@link Edge#getEdgeNode()} + * may only be the technical root-property placeholder for {@code ?s ?p ?o} patterns. + * + * @param queryEdge KGRAM edge whose predicate must be read + * @return the predicate variable when present, otherwise the constant predicate node + */ private Node predicateQueryNode(Edge queryEdge) { - // KGRAM stores variable predicates in edgeVariable; edgeNode may only be - // the technical root-property placeholder for ?s ?p ?o patterns. return queryEdge.getEdgeVariable() == null ? queryEdge.getEdgeNode() : queryEdge.getEdgeVariable(); } @@ -226,6 +260,8 @@ private Node predicateQueryNode(Edge queryEdge) { */ private ContextSelection contextSelection(Node graphNode, List from, Environment environment) { if (graphNode != null) { + // GRAPH : evaluate in the requested named graph, provided it belongs + // to the active named dataset restriction. Node resolvedGraphNode = resolve(graphNode, environment); if (resolvedGraphNode == null) { return ContextSelection.allContexts(); @@ -241,9 +277,11 @@ private ContextSelection contextSelection(Node graphNode, List from, Envir } if (from == null || from.isEmpty()) { + // No GRAPH and no dataset restriction: leave storage contexts unrestricted. return ContextSelection.allContexts(); } + // No explicit GRAPH: use the dataset restriction as storage contexts. List contexts = new ArrayList<>(); for (Node node : from) { Node resolvedNode = resolve(node, environment); @@ -259,6 +297,14 @@ private ContextSelection contextSelection(Node graphNode, List from, Envir return ContextSelection.of(contexts); } + /** + * Checks whether a resolved graph node is allowed by the active dataset restriction. + * + * @param graphNode resolved graph node to test + * @param from active FROM/FROM NAMED restriction computed by KGRAM + * @param environment current bindings used to resolve graph variables in {@code from} + * @return {@code true} when no restriction exists or when {@code graphNode} belongs to it + */ private boolean matchesFrom(Node graphNode, List from, Environment environment) { if (from == null || from.isEmpty()) { return true; @@ -272,6 +318,16 @@ private boolean matchesFrom(Node graphNode, List from, Environment environ return false; } + /** + * Resolves a query node against the current KGRAM environment. + * + *

Constants resolve to themselves, bound variables resolve to their current target node, + * and unbound variables resolve to {@code null}. + * + * @param queryNode KGRAM query node to resolve + * @param environment current bindings, or {@code null} + * @return resolved node, or {@code null} when the node is unbound + */ private Node resolve(Node queryNode, Environment environment) { if (queryNode == null) { return null; @@ -282,6 +338,20 @@ private Node resolve(Node queryNode, Environment environment) { return environment == null ? null : environment.getNode(queryNode); } + /** + * Extends partial bindings with the matches of one triple pattern. + * + *

Each input binding is exposed through a temporary {@link BindingEnvironment}, so already + * bound variables are pushed into {@link #queryPattern(Node, List, Edge, Environment)} before + * querying storage. + * + * @param graphNode active graph node + * @param from active dataset restriction + * @param queryEdge triple pattern to join + * @param environment base KGRAM environment + * @param inputBindings bindings produced by previous BGP edges + * @return bindings extended with the matches of {@code queryEdge} + */ private List join( Node graphNode, List from, @@ -303,18 +373,43 @@ private List join( return results; } + /** + * Converts a storage RDF value to a KGRAM node. + * + * @param value storage value to wrap + * @return KGRAM node carrying the equivalent Corese datatype + */ private Node node(Value value) { return node(datatypeValue(value)); } + /** + * Wraps a Corese datatype as a KGRAM node. + * + * @param datatype Corese datatype to wrap + * @return KGRAM node backed by a parser constant + */ private static Node node(IDatatype datatype) { return new NodeImpl(Constant.create(datatype)); } + /** + * Converts a KGRAM node to a storage RDF value. + * + * @param node KGRAM node to convert + * @return equivalent storage value + */ private Value rdfValue(Node node) { return rdfValue(node.getDatatypeValue()); } + /** + * Converts a Corese datatype to the storage RDF value model. + * + * @param datatype Corese datatype to convert + * @return equivalent storage value + * @throws IllegalArgumentException when the datatype cannot be represented as RDF + */ private Value rdfValue(IDatatype datatype) { if (datatype.isURI()) { return valueFactory.createIRI(datatype.getLabel()); @@ -336,6 +431,13 @@ private Value rdfValue(IDatatype datatype) { throw new IllegalArgumentException("Unsupported KGRAM datatype value: " + datatype); } + /** + * Converts a storage RDF value to the Corese datatype model used by KGRAM nodes. + * + * @param value storage value to convert + * @return equivalent Corese datatype + * @throws IllegalArgumentException when the value is not an RDF IRI, blank node or literal + */ private IDatatype datatypeValue(Value value) { if (value instanceof IRI iri) { return fr.inria.corese.core.sparql.datatype.DatatypeMap.newResource(iri.stringValue()); @@ -395,6 +497,13 @@ private BindingSet copy() { return copy; } + /** + * Adds a query-variable binding, or validates it against an existing binding. + * + * @param queryNode query-side node, usually a variable + * @param targetNode storage match node + * @return {@code true} when the binding is compatible with previous bindings + */ private boolean bind(Node queryNode, Node targetNode) { if (queryNode == null || queryNode.isConstant()) { return true; @@ -408,6 +517,12 @@ private boolean bind(Node queryNode, Node targetNode) { return current.match(targetNode); } + /** + * Looks up the target node already bound to a query node. + * + * @param queryNode query node to find + * @return bound target node, or {@code null} when the query node is unbound + */ private Node get(Node queryNode) { if (queryNode == null) { return null; @@ -420,11 +535,22 @@ private Node get(Node queryNode) { return null; } + /** + * Converts this local binding set into a KGRAM mapping. + * + * @return mapping containing all query-to-target bindings + */ private Mapping toMapping() { return Mapping.create(queryNodes, targetNodes); } } + /** + * Environment overlay used while joining BGP edges. + * + *

Local bindings produced by earlier triple patterns take precedence over the delegate + * environment, which lets later patterns see already-bound variables. + */ private static final class BindingEnvironment implements Environment { private final Environment delegate; From 1ce0ecc8b12f5bab9b891a8e014af19e557fb3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 10 Jul 2026 12:17:39 +0200 Subject: [PATCH 6/9] docs(next): clarify storage producer selection records --- .../query/kgram/tool/StorageManagerProducer.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java index 24ef57226..4faac5df6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -455,6 +455,12 @@ private IDatatype datatypeValue(Value value) { throw new IllegalArgumentException("Unsupported RDF value: " + value); } + /** + * Statement pattern plus an explicit empty-result marker. + * + *

This avoids using {@code null} to represent impossible RDF patterns while still + * keeping a non-null placeholder pattern for the record state. + */ private record StorageQueryPattern(StatementPattern statementPattern, boolean noMatch) { private static StorageQueryPattern of(StatementPattern statementPattern) { @@ -466,6 +472,12 @@ private static StorageQueryPattern emptyResult() { } } + /** + * Storage contexts selected for a query pattern. + * + *

An empty context list means "all contexts" for the storage API; {@code noMatch} + * distinguishes this from a graph/dataset restriction that cannot produce results. + */ private record ContextSelection(List contexts, boolean noMatch) { private static ContextSelection of(List contexts) { From f3a5b353e1bb107a4e5a2eab71c0871eb5d6ac90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 10 Jul 2026 14:35:39 +0200 Subject: [PATCH 7/9] refactor(next): centralize storage KGRAM value conversion --- .../query/kgram/tool/StorageManagerEdge.java | 18 +-- .../kgram/tool/StorageManagerKgramValues.java | 112 ++++++++++++++++++ .../kgram/tool/StorageManagerProducer.java | 103 ++-------------- .../kgram/tool/StorageManagerEdgeTest.java | 47 ++++++++ 4 files changed, 172 insertions(+), 108 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerKgramValues.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdgeTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java index 6b1ff2c7d..e637168e6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java @@ -1,11 +1,8 @@ package fr.inria.corese.core.next.query.kgram.tool; 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.impl.temp.CoreseValueConverter; 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.sparql.triple.parser.Constant; import java.util.Objects; @@ -18,8 +15,6 @@ */ public final class StorageManagerEdge implements Edge { - private static final CoreseValueConverter VALUE_CONVERTER = new CoreseValueConverter(); - private final Statement statement; private final Node subject; private final Node predicate; @@ -28,10 +23,10 @@ public final class StorageManagerEdge implements Edge { public StorageManagerEdge(Statement statement) { this.statement = Objects.requireNonNull(statement, "statement"); - this.subject = node(statement.getSubject()); - this.predicate = node(statement.getPredicate()); - this.object = node(statement.getObject()); - this.graph = statement.getContext() == null ? null : node(statement.getContext()); + this.subject = StorageManagerKgramValues.node(statement.getSubject()); + this.predicate = StorageManagerKgramValues.node(statement.getPredicate()); + this.object = StorageManagerKgramValues.node(statement.getObject()); + this.graph = statement.getContext() == null ? null : StorageManagerKgramValues.node(statement.getContext()); } public Statement getSourceStatement() { @@ -79,9 +74,4 @@ public Edge getEdge() { private static boolean sameNode(Node left, Node right) { return left == right || left.same(right); } - - private static Node node(Value value) { - fr.inria.corese.core.kgram.api.core.Node node = VALUE_CONVERTER.toCoreseNode(value); - return new NodeImpl(Constant.create(node.getDatatypeValue())); - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerKgramValues.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerKgramValues.java new file mode 100644 index 000000000..8fe909c44 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerKgramValues.java @@ -0,0 +1,112 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +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.api.ValueFactory; +import fr.inria.corese.core.next.query.kgram.api.core.Node; +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; + +/** + * Conversion helpers for the StorageManager/KGRAM boundary. + * + *

The storage layer exposes RDF values through the Corese-next data model, while KGRAM + * nodes still carry Corese {@link IDatatype} values. This helper keeps that transitional + * conversion in one place and avoids routing storage results through the legacy KGRAM node API. + */ +final class StorageManagerKgramValues { + + private StorageManagerKgramValues() { + } + + /** + * Converts a storage RDF value to a KGRAM node. + * + * @param value storage value to wrap + * @return KGRAM node carrying the equivalent Corese datatype + */ + static Node node(Value value) { + return node(datatypeValue(value)); + } + + /** + * Wraps a Corese datatype as a KGRAM node. + * + * @param datatype Corese datatype to wrap + * @return KGRAM node backed by a parser constant + */ + static Node node(IDatatype datatype) { + return new NodeImpl(Constant.create(datatype)); + } + + /** + * Converts a KGRAM node to a storage RDF value. + * + * @param node KGRAM node to convert + * @param valueFactory factory used to create storage values + * @return equivalent storage value + */ + static Value rdfValue(Node node, ValueFactory valueFactory) { + return rdfValue(node.getDatatypeValue(), valueFactory); + } + + /** + * Converts a Corese datatype to the storage RDF value model. + * + *

Language-tagged literals are converted through the language branch, and therefore + * become RDF {@code langString} literals in the storage model. + * + * @param datatype Corese datatype to convert + * @param valueFactory factory used to create storage values + * @return equivalent storage value + * @throws IllegalArgumentException when the datatype cannot be represented as RDF + */ + static Value rdfValue(IDatatype datatype, ValueFactory valueFactory) { + if (datatype.isURI()) { + return valueFactory.createIRI(datatype.getLabel()); + } + if (datatype.isBlank()) { + return valueFactory.createBNode(datatype.getLabel()); + } + if (datatype.isLiteral()) { + String language = datatype.getLang(); + if (language != null && !language.isEmpty()) { + return valueFactory.createLiteral(datatype.getLabel(), language); + } + String datatypeUri = datatype.getDatatypeURI(); + if (datatypeUri != null && !datatypeUri.isEmpty()) { + return valueFactory.createLiteral(datatype.getLabel(), valueFactory.createIRI(datatypeUri)); + } + return valueFactory.createLiteral(datatype.getLabel()); + } + throw new IllegalArgumentException("Unsupported KGRAM datatype value: " + datatype); + } + + /** + * Converts a storage RDF value to the Corese datatype model used by KGRAM nodes. + * + * @param value storage value to convert + * @return equivalent Corese datatype + * @throws IllegalArgumentException when the value is not an RDF IRI, blank node or literal + */ + static IDatatype datatypeValue(Value value) { + if (value instanceof IRI iri) { + return DatatypeMap.newResource(iri.stringValue()); + } + if (value instanceof BNode bNode) { + return DatatypeMap.createBlank(bNode.getID()); + } + if (value instanceof Literal literal) { + return literal.getLanguage() + .map(language -> DatatypeMap.createLiteral(literal.getLabel(), null, language)) + .orElseGet(() -> DatatypeMap.createLiteral( + literal.getLabel(), + literal.getDatatype().stringValue(), + null)); + } + throw new IllegalArgumentException("Unsupported RDF value: " + value); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java index 4faac5df6..090964c4e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java @@ -1,8 +1,6 @@ package fr.inria.corese.core.next.query.kgram.tool; -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.Resource; import fr.inria.corese.core.next.data.api.Value; import fr.inria.corese.core.next.data.api.ValueFactory; @@ -19,7 +17,6 @@ import fr.inria.corese.core.next.storagemanager.api.StorageManager; import fr.inria.corese.core.next.storagemanager.api.support.model.StatementPattern; import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.parser.Constant; import java.util.ArrayList; import java.util.List; @@ -76,7 +73,7 @@ public Iterable getEdges(Node graphNode, List from, Edge queryEdge, public Iterable getGraphNodes(Node graphNode, List from, Environment environment) { List nodes = new ArrayList<>(); for (Resource context : storage.getMetadataOperations().getContexts()) { - Node node = node(context); + Node node = StorageManagerKgramValues.node(context); if (matchesFrom(node, from, environment)) { nodes.add(node); } @@ -113,10 +110,10 @@ public Node getNode(Object value) { return node; } if (value instanceof Value rdfValue) { - return node(rdfValue); + return StorageManagerKgramValues.node(rdfValue); } if (value instanceof IDatatype datatype) { - return node(datatype); + return StorageManagerKgramValues.node(datatype); } return null; } @@ -132,7 +129,7 @@ public IDatatype getDatatypeValue(Object value) { return node.getDatatypeValue(); } if (value instanceof Value rdfValue) { - return node(rdfValue).getDatatypeValue(); + return StorageManagerKgramValues.datatypeValue(rdfValue); } if (value instanceof IDatatype datatype) { return datatype; @@ -207,21 +204,21 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q // Subject and predicate have stricter RDF roles than object: subject must // be a resource, predicate must be an IRI, while object accepts any RDF value. if (subjectNode != null) { - Value value = rdfValue(subjectNode); + Value value = StorageManagerKgramValues.rdfValue(subjectNode, valueFactory); if (!(value instanceof Resource resource)) { return StorageQueryPattern.emptyResult(); } subject = resource; } if (predicateNode != null) { - Value value = rdfValue(predicateNode); + Value value = StorageManagerKgramValues.rdfValue(predicateNode, valueFactory); if (!(value instanceof IRI iri)) { return StorageQueryPattern.emptyResult(); } predicate = iri; } if (objectNode != null) { - object = rdfValue(objectNode); + object = StorageManagerKgramValues.rdfValue(objectNode, valueFactory); } // Graph and dataset clauses become the statement contexts passed to storage. @@ -266,7 +263,7 @@ private ContextSelection contextSelection(Node graphNode, List from, Envir if (resolvedGraphNode == null) { return ContextSelection.allContexts(); } - Value value = rdfValue(resolvedGraphNode); + Value value = StorageManagerKgramValues.rdfValue(resolvedGraphNode, valueFactory); if (!(value instanceof Resource resource)) { return ContextSelection.emptyResult(); } @@ -288,7 +285,7 @@ private ContextSelection contextSelection(Node graphNode, List from, Envir if (resolvedNode == null) { continue; } - Value value = rdfValue(resolvedNode); + Value value = StorageManagerKgramValues.rdfValue(resolvedNode, valueFactory); if (!(value instanceof Resource resource)) { return ContextSelection.emptyResult(); } @@ -373,88 +370,6 @@ private List join( return results; } - /** - * Converts a storage RDF value to a KGRAM node. - * - * @param value storage value to wrap - * @return KGRAM node carrying the equivalent Corese datatype - */ - private Node node(Value value) { - return node(datatypeValue(value)); - } - - /** - * Wraps a Corese datatype as a KGRAM node. - * - * @param datatype Corese datatype to wrap - * @return KGRAM node backed by a parser constant - */ - private static Node node(IDatatype datatype) { - return new NodeImpl(Constant.create(datatype)); - } - - /** - * Converts a KGRAM node to a storage RDF value. - * - * @param node KGRAM node to convert - * @return equivalent storage value - */ - private Value rdfValue(Node node) { - return rdfValue(node.getDatatypeValue()); - } - - /** - * Converts a Corese datatype to the storage RDF value model. - * - * @param datatype Corese datatype to convert - * @return equivalent storage value - * @throws IllegalArgumentException when the datatype cannot be represented as RDF - */ - private Value rdfValue(IDatatype datatype) { - if (datatype.isURI()) { - return valueFactory.createIRI(datatype.getLabel()); - } - if (datatype.isBlank()) { - return valueFactory.createBNode(datatype.getLabel()); - } - if (datatype.isLiteral()) { - String language = datatype.getLang(); - if (language != null && !language.isEmpty()) { - return valueFactory.createLiteral(datatype.getLabel(), language); - } - String datatypeUri = datatype.getDatatypeURI(); - if (datatypeUri != null && !datatypeUri.isEmpty()) { - return valueFactory.createLiteral(datatype.getLabel(), valueFactory.createIRI(datatypeUri)); - } - return valueFactory.createLiteral(datatype.getLabel()); - } - throw new IllegalArgumentException("Unsupported KGRAM datatype value: " + datatype); - } - - /** - * Converts a storage RDF value to the Corese datatype model used by KGRAM nodes. - * - * @param value storage value to convert - * @return equivalent Corese datatype - * @throws IllegalArgumentException when the value is not an RDF IRI, blank node or literal - */ - private IDatatype datatypeValue(Value value) { - if (value instanceof IRI iri) { - return fr.inria.corese.core.sparql.datatype.DatatypeMap.newResource(iri.stringValue()); - } - if (value instanceof BNode bNode) { - return fr.inria.corese.core.sparql.datatype.DatatypeMap.createBlank(bNode.getID()); - } - if (value instanceof Literal literal) { - return literal.getLanguage() - .map(language -> fr.inria.corese.core.sparql.datatype.DatatypeMap - .createLiteral(literal.getLabel(), null, language)) - .orElseGet(() -> fr.inria.corese.core.sparql.datatype.DatatypeMap - .createLiteral(literal.getLabel(), literal.getDatatype().stringValue(), null)); - } - throw new IllegalArgumentException("Unsupported RDF value: " + value); - } - /** * Statement pattern plus an explicit empty-result marker. * diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdgeTest.java new file mode 100644 index 000000000..e28bcd12f --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdgeTest.java @@ -0,0 +1,47 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +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.Statement; +import fr.inria.corese.core.next.data.api.ValueFactory; +import fr.inria.corese.core.next.data.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StorageManagerEdgeTest { + + private final ValueFactory valueFactory = new CoreseAdaptedValueFactory(); + + @Test + void exposesStorageStatementAsKgramEdge() { + IRI subject = valueFactory.createIRI("http://example.org/alice"); + IRI predicate = valueFactory.createIRI("http://example.org/name"); + Literal object = valueFactory.createLiteral("Alice", "en"); + IRI graph = valueFactory.createIRI("http://example.org/graph"); + Statement statement = valueFactory.createStatement(subject, predicate, object, graph); + + StorageManagerEdge edge = new StorageManagerEdge(statement); + + assertSame(statement, edge.getSourceStatement()); + assertEquals(subject.stringValue(), edge.getNode(0).getLabel()); + assertEquals(predicate.stringValue(), edge.getProperty().getLabel()); + assertEquals(predicate.stringValue(), edge.getEdgeLabel()); + assertEquals("Alice", edge.getNode(1).getLabel()); + assertEquals("en", edge.getNode(1).getDatatypeValue().getLang()); + assertEquals(graph.stringValue(), edge.getGraph().getLabel()); + assertTrue(edge.contains(edge.getNode(0))); + } + + @Test + void convertsTypedLiteralThroughSharedKgramValueHelper() { + IRI integerDatatype = valueFactory.createIRI("http://www.w3.org/2001/XMLSchema#integer"); + Node node = StorageManagerKgramValues.node(valueFactory.createLiteral("42", integerDatatype)); + + assertEquals("42", node.getLabel()); + assertEquals(integerDatatype.stringValue(), node.getDatatypeValue().getDatatypeURI()); + } +} From 0a40329de7b25665f2d59e000454bbdaccf1b631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 10 Jul 2026 14:46:39 +0200 Subject: [PATCH 8/9] test(next): cover storage producer edge cases --- .../tool/StorageManagerProducerTest.java | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java index cd9d85216..c4e510a89 100644 --- a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java @@ -178,6 +178,19 @@ void invalidPredicateBindingReturnsNoEdges() { assertTrue(edges.isEmpty()); } + @Test + @DisplayName("Invalid subject binding returns no candidate edges") + void invalidSubjectBindingReturnsNoEdges() { + Node s = variable("s"); + Node o = variable("o"); + Environment environment = mock(Environment.class); + when(environment.getNode(s)).thenReturn(literal("not-a-subject")); + + List edges = edges(new QueryEdge(s, resource(KNOWS), null, o), null, List.of(), environment); + + assertTrue(edges.isEmpty()); + } + @Test @DisplayName("Eval executes SELECT * WHERE { ?s ?p ?o } over StorageManagerProducer") void evalExecutesSelectStarSpoOverStorageManagerProducer() throws Exception { @@ -227,6 +240,24 @@ void producerMaterializesBgpMappings() { assertContainsMapping(mappings, ALICE, KNOWS, BOB); } + @Test + @DisplayName("Producer joins BGP mappings across repeated variables") + void producerJoinsBgpMappingsAcrossRepeatedVariables() { + Node s = variable("s"); + Node o = variable("o"); + Node name = variable("name"); + Query query = selectKnowsNameQuery(s, o, name); + Environment environment = mock(Environment.class); + when(environment.getQuery()).thenReturn(query); + + Exp bgp = query.getBody().first(); + Mappings mappings = producer.getMappings(null, List.of(), bgp, environment); + + assertEquals(2, mappings.size()); + assertContainsNameMapping(mappings, ALICE, BOB, "Bob"); + assertContainsNameMapping(mappings, ALICE, CAROL, "Carol"); + } + private static void assertContainsMapping(Mappings mappings, String subject, String predicate, String object) { assertTrue(mappings.getMappingList().stream().anyMatch(mapping -> subject.equals(mapping.getValue("s").getLabel()) @@ -234,6 +265,13 @@ private static void assertContainsMapping(Mappings mappings, String subject, Str && object.equals(mapping.getValue("o").getLabel()))); } + private static void assertContainsNameMapping(Mappings mappings, String subject, String object, String name) { + assertTrue(mappings.getMappingList().stream().anyMatch(mapping -> + subject.equals(mapping.getValue("s").getLabel()) + && object.equals(mapping.getValue("o").getLabel()) + && name.equals(mapping.getValue("name").getLabel()))); + } + private void insert(Resource subject, IRI predicate, Value object, Resource... contexts) { if (contexts == null || contexts.length == 0) { storage.getMutationOperations().insertStatement(valueFactory.createStatement(subject, predicate, object)); @@ -272,9 +310,20 @@ private static Node literal(String label) { } private static Query selectStarSpoQuery(Node s, Node p, Node o) { - Exp body = Exp.create(Type.AND); Exp bgp = Exp.create(Type.BGP); bgp.add(Exp.create(Type.EDGE, new QueryEdge(s, KgramNodes.rootProperty(), p, o))); + return selectQuery(bgp); + } + + private static Query selectKnowsNameQuery(Node s, Node o, Node name) { + Exp bgp = Exp.create(Type.BGP); + bgp.add(Exp.create(Type.EDGE, new QueryEdge(s, resource(KNOWS), null, o))); + bgp.add(Exp.create(Type.EDGE, new QueryEdge(o, resource(NAME), null, name))); + return selectQuery(bgp); + } + + private static Query selectQuery(Exp bgp) { + Exp body = Exp.create(Type.AND); body.add(bgp); Query query = Query.create(body); query.collect(); From 0cffbf61733d53b697ba7c20dd20fa16609fde8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 10 Jul 2026 15:08:35 +0200 Subject: [PATCH 9/9] test(next): avoid order-dependent storage assertion --- .../next/query/kgram/tool/StorageManagerProducerTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java index c4e510a89..fb4e25ea3 100644 --- a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java @@ -29,10 +29,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -88,7 +89,9 @@ void constantPredicateFiltersStorageStatements() { assertEquals(2, edges.size()); assertTrue(edges.stream().allMatch(edge -> KNOWS.equals(edge.getEdgeNode().getLabel()))); - assertIterableEquals(List.of(BOB, CAROL), edges.stream().map(edge -> edge.getNode(1).getLabel()).toList()); + assertEquals( + Set.of(BOB, CAROL), + edges.stream().map(edge -> edge.getNode(1).getLabel()).collect(Collectors.toSet())); } @Test