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/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..e637168e6
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerEdge.java
@@ -0,0 +1,77 @@
+package fr.inria.corese.core.next.query.kgram.tool;
+
+import fr.inria.corese.core.next.data.api.Statement;
+import fr.inria.corese.core.next.query.kgram.api.core.Edge;
+import fr.inria.corese.core.next.query.kgram.api.core.Node;
+
+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 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 = 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() {
+ 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);
+ }
+}
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
new file mode 100644
index 000000000..090964c4e
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducer.java
@@ -0,0 +1,687 @@
+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.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 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;
+
+ /**
+ * 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");
+ }
+
+ @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 = StorageManagerKgramValues.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) {
+ throw new UnsupportedOperationException("StorageManagerProducer does not support path/regex edge enumeration yet");
+ }
+
+ @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 StorageManagerKgramValues.node(rdfValue);
+ }
+ if (value instanceof IDatatype datatype) {
+ return StorageManagerKgramValues.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 StorageManagerKgramValues.datatypeValue(rdfValue);
+ }
+ 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;
+ }
+
+ /**
+ * 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);
+ Node objectNode = resolve(queryEdge.getNode(1), environment);
+
+ Resource subject = null;
+ 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 = StorageManagerKgramValues.rdfValue(subjectNode, valueFactory);
+ if (!(value instanceof Resource resource)) {
+ return StorageQueryPattern.emptyResult();
+ }
+ subject = resource;
+ }
+ if (predicateNode != null) {
+ Value value = StorageManagerKgramValues.rdfValue(predicateNode, valueFactory);
+ if (!(value instanceof IRI iri)) {
+ return StorageQueryPattern.emptyResult();
+ }
+ predicate = iri;
+ }
+ if (objectNode != null) {
+ object = StorageManagerKgramValues.rdfValue(objectNode, valueFactory);
+ }
+
+ // Graph and dataset clauses become the statement contexts passed to storage.
+ ContextSelection contextSelection = contextSelection(graphNode, from, environment);
+ if (contextSelection.noMatch()) {
+ return StorageQueryPattern.emptyResult();
+ }
+ return StorageQueryPattern.of(StatementPattern.of(
+ subject,
+ predicate,
+ object,
+ 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) {
+ 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) {
+ // 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();
+ }
+ Value value = StorageManagerKgramValues.rdfValue(resolvedGraphNode, valueFactory);
+ if (!(value instanceof Resource resource)) {
+ return ContextSelection.emptyResult();
+ }
+ if (!matchesFrom(resolvedGraphNode, from, environment)) {
+ return ContextSelection.emptyResult();
+ }
+ return ContextSelection.of(List.of(resource));
+ }
+
+ 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);
+ if (resolvedNode == null) {
+ continue;
+ }
+ Value value = StorageManagerKgramValues.rdfValue(resolvedNode, valueFactory);
+ if (!(value instanceof Resource resource)) {
+ return ContextSelection.emptyResult();
+ }
+ contexts.add(resource);
+ }
+ 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;
+ }
+ for (Node fromNode : from) {
+ Node resolvedNode = resolve(fromNode, environment);
+ if (resolvedNode != null && resolvedNode.match(graphNode)) {
+ return true;
+ }
+ }
+ 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;
+ }
+ if (queryNode.isConstant()) {
+ return queryNode;
+ }
+ 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,
+ 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;
+ }
+
+ /**
+ * 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) {
+ return new StorageQueryPattern(statementPattern, false);
+ }
+
+ private static StorageQueryPattern emptyResult() {
+ return new StorageQueryPattern(StatementPattern.matchAll(), true);
+ }
+ }
+
+ /**
+ * 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) {
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+ Node current = get(queryNode);
+ if (current == null) {
+ queryNodes.add(queryNode);
+ targetNodes.add(targetNode);
+ return true;
+ }
+ 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;
+ }
+ for (int i = 0; i < queryNodes.size(); i++) {
+ if (queryNodes.get(i) == queryNode || queryNodes.get(i).same(queryNode)) {
+ return targetNodes.get(i);
+ }
+ }
+ 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;
+ 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/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());
+ }
+}
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..fb4e25ea3
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/StorageManagerProducerTest.java
@@ -0,0 +1,463 @@
+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 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.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())));
+ assertEquals(
+ Set.of(BOB, CAROL),
+ edges.stream().map(edge -> edge.getNode(1).getLabel()).collect(Collectors.toSet()));
+ }
+
+ @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("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() {
+ 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("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 {
+ 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);
+ }
+
+ @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())
+ && predicate.equals(mapping.getValue("p").getLabel())
+ && 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));
+ 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 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();
+ List select = query.selectNodesFromPattern();
+ query.setSelect(select);
+ query.setSelectFun(select.stream().map(node -> Exp.create(Type.NODE, node)).toList());
+ return query;
+ }
+
+ 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;
+ }
+ }
+}