From 81dd9461938949c5c8098e1b7807bf9829e4fe3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 18 Sep 2026 15:13:42 +0200 Subject: [PATCH] refactor(next): streamline StorageManagerProducer - Extract StatementPattern translation and context selection into StoragePatternTranslator - Extract BGP join variable tracking into StorageBindingSet - Extract layered environment overlay into StorageBindingEnvironment - Reduce StorageManagerProducer to a focused Producer SPI dispatcher --- .../storage/StorageBindingEnvironment.java | 228 +++++++ .../engine/storage/StorageBindingSet.java | 70 +++ .../storage/StorageManagerProducer.java | 561 +----------------- .../storage/StoragePatternTranslator.java | 255 ++++++++ 4 files changed, 579 insertions(+), 535 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingSet.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StoragePatternTranslator.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java new file mode 100644 index 000000000..f5a7fe369 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java @@ -0,0 +1,228 @@ +package fr.inria.corese.core.next.query.impl.engine.storage; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.query.impl.engine.eval.ApproximateSearchEnv; +import fr.inria.corese.core.next.query.impl.engine.eval.Eval; +import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; +import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitor; +import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Expr; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.path.Path; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; +import fr.inria.corese.core.next.query.impl.engine.spi.Environment; + +import java.util.List; +import java.util.Map; + +/** + * 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.

+ */ +final class StorageBindingEnvironment implements Environment { + + private final Environment delegate; + private final StorageBindingSet bindings; + + StorageBindingEnvironment(Environment delegate, StorageBindingSet 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 boolean isBound(Node queryNode) { + return getNode(queryNode) != null; + } + + @Override + public Query getQuery() { + return delegate == null ? null : delegate.getQuery(); + } + + @Override + public BindingContext getBind() { + return delegate == null ? null : delegate.getBind(); + } + + @Override + public void setBind(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(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 int pathLength(Node queryNode) { + return delegate == null ? 0 : delegate.pathLength(queryNode); + } + + @Override + public Path getPath(Node queryNode) { + return delegate == null ? null : delegate.getPath(queryNode); + } + + @Override + public int count() { + return delegate == null ? 0 : delegate.count(); + } + + @Override + public KgramEventDispatcher 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 Map getMap() { + return delegate == null ? 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(Expr varExpr) { + return delegate == null ? null : delegate.get(varExpr); + } + + @Override + public ApproximateSearchEnv getAppxSearchEnv() { + return delegate == null ? null : delegate.getAppxSearchEnv(); + } + + @Override + public Eval getEval() { + return delegate == null ? null : delegate.getEval(); + } + + @Override + public void setEval(Eval eval) { + if (delegate != null) { + delegate.setEval(eval); + } + } + + @Override + public ProcessVisitor getVisitor() { + return delegate == null ? null : delegate.getVisitor(); + } + + @Override + public DatatypeValue getReport() { + return delegate == null ? null : delegate.getReport(); + } + + @Override + public void setReport(DatatypeValue datatype) { + if (delegate != null) { + delegate.setReport(datatype); + } + } + + @Override + public int size() { + return delegate == null ? 0 : delegate.size(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingSet.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingSet.java new file mode 100644 index 000000000..bbba04767 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingSet.java @@ -0,0 +1,70 @@ +package fr.inria.corese.core.next.query.impl.engine.storage; + +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tracks variable-to-value bindings during BGP join evaluation. + */ +final class StorageBindingSet { + + private final List queryNodes = new ArrayList<>(); + private final List targetNodes = new ArrayList<>(); + + StorageBindingSet copy() { + StorageBindingSet copy = new StorageBindingSet(); + 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 + */ + 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 + */ + 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 + */ + Mapping toMapping() { + return Mapping.create(queryNodes, targetNodes); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java index 347ba8887..7b523b4a0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java @@ -1,32 +1,22 @@ package fr.inria.corese.core.next.query.impl.engine.storage; -import fr.inria.corese.core.next.query.impl.engine.eval.ApproximateSearchEnv; -import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; - +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.model.Statement; import fr.inria.corese.core.next.data.api.term.IRI; import fr.inria.corese.core.next.data.api.term.Resource; import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.api.model.DatatypeValue; -import fr.inria.corese.core.next.data.api.model.Statement; import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; -import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; import fr.inria.corese.core.next.query.impl.engine.model.Edge; -import fr.inria.corese.core.next.query.impl.engine.model.Expr; import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; import fr.inria.corese.core.next.query.impl.engine.model.Regex; -import fr.inria.corese.core.next.query.impl.engine.spi.Environment; -import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitor; -import fr.inria.corese.core.next.query.impl.engine.eval.Eval; +import fr.inria.corese.core.next.query.impl.engine.path.NativePropertyPathEvaluator; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; -import fr.inria.corese.core.next.query.impl.engine.path.Path; -import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; -import fr.inria.corese.core.next.query.impl.engine.path.NativePropertyPathEvaluator; +import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; +import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.storage.api.StorageManager; -import fr.inria.corese.core.next.storage.api.model.StatementPattern; import java.util.ArrayList; import java.util.HashSet; @@ -39,8 +29,8 @@ * KGRAM producer backed by a Corese-next {@link StorageManager}. * *

The producer is responsible for translating KGRAM graph-pattern requests - * into storage-layer {@link StatementPattern} queries, then adapting returned - * RDF statements back into KGRAM edges. It does not decide the final RDF-term + * into storage-layer {@link fr.inria.corese.core.next.storage.api.model.StatementPattern} queries, + * then adapting returned RDF statements back into KGRAM edges. It does not decide the final RDF-term * matching policy; that remains the matcher responsibility.

*/ public final class StorageManagerProducer extends ProducerDefault { @@ -63,7 +53,8 @@ public Iterable getEdges(Node graphNode, List from, Edge queryEdge, return NativePropertyPathEvaluator.evaluate(this, graphNode, from, path, environment); } - StorageQueryPattern queryPattern = queryPattern(graphNode, from, queryEdge, environment); + StoragePatternTranslator.StorageQueryPattern queryPattern = + StoragePatternTranslator.translate(graphNode, from, queryEdge, environment); if (queryPattern.noMatch()) { return List.of(); } @@ -87,16 +78,16 @@ private record TripleKey(Resource subject, IRI predicate, Value object) { } @Override public Iterable getGraphNodes(Node graphNode, List from, Environment environment) { - List namedGraphs = isExplicitDataset(environment) + List namedGraphs = StoragePatternTranslator.isExplicitDataset(environment) ? environment.getQuery().getNamed() : from; - if (isExplicitDataset(environment) && (namedGraphs == null || namedGraphs.isEmpty())) { + if (StoragePatternTranslator.isExplicitDataset(environment) && (namedGraphs == null || namedGraphs.isEmpty())) { return List.of(); } List nodes = new ArrayList<>(); for (Resource context : storage.metadata().getContexts()) { Node node = NodeImpl.forValue(context); - if (matchesFrom(node, namedGraphs, environment)) { + if (StoragePatternTranslator.matchesFrom(node, namedGraphs, environment)) { nodes.add(node); } } @@ -154,14 +145,6 @@ public DatatypeValue getValue(Object value) { return getDatatypeValue(value); } - private static Value rdfValue(Node node) { - DatatypeValue value = Objects.requireNonNull(node, "node").getDatatypeValue(); - if (value instanceof Value rdfValue) { - return rdfValue; - } - throw new IllegalArgumentException("KGRAM node does not carry an RDF value: " + node); - } - @Override public boolean isBindable(Node node) { return node != null && node.isVariable(); @@ -179,8 +162,8 @@ public Mappings getMappings(Node graphNode, List from, Exp exp, Environmen throw new IllegalArgumentException("StorageManagerProducer can only materialize BGP expressions"); } - List bindings = new ArrayList<>(); - bindings.add(new BindingSet()); + List bindings = new ArrayList<>(); + bindings.add(new StorageBindingSet()); for (Exp element : exp) { if (!element.isEdge()) { throw new IllegalArgumentException( @@ -193,200 +176,18 @@ public Mappings getMappings(Node graphNode, List from, Exp exp, Environmen } Mappings mappings = Mappings.create(environment.getQuery()); - for (BindingSet binding : bindings) { + for (StorageBindingSet binding : bindings) { mappings.add(binding.toMapping()); } return mappings; } - /** - * 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 = 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); - } - - // 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) { - List activeGraphs = selectActiveGraphs(graphNode, from, environment); - if (graphNode != null) { - return selectExplicitGraphContext(graphNode, activeGraphs, environment); - } - return selectDatasetContexts(activeGraphs, environment); - } - - private List selectActiveGraphs(Node graphNode, List from, Environment environment) { - if (isExplicitDataset(environment)) { - return graphNode == null - ? environment.getQuery().getFrom() - : environment.getQuery().getNamed(); - } - return from; - } - - private ContextSelection selectExplicitGraphContext( - Node graphNode, - List activeGraphs, - Environment environment) { - Node resolvedGraphNode = resolve(graphNode, environment); - if (resolvedGraphNode == null) { - return isExplicitDataset(environment) && (activeGraphs == null || activeGraphs.isEmpty()) - ? ContextSelection.emptyResult() - : ContextSelection.allContexts(); - } - Value value = rdfValue(resolvedGraphNode); - if (!(value instanceof Resource resource)) { - return ContextSelection.emptyResult(); - } - if (!matchesFrom(resolvedGraphNode, activeGraphs, environment)) { - return ContextSelection.emptyResult(); - } - return ContextSelection.of(List.of(resource)); - } - - private ContextSelection selectDatasetContexts(List activeGraphs, Environment environment) { - if (activeGraphs == null || activeGraphs.isEmpty()) { - return isExplicitDataset(environment) - ? ContextSelection.emptyResult() - : ContextSelection.allContexts(); - } - - List contexts = new ArrayList<>(); - for (Node node : activeGraphs) { - 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); - } - - /** - * 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 !isExplicitDataset(environment); - } - for (Node fromNode : from) { - Node resolvedNode = resolve(fromNode, environment); - if (resolvedNode != null && resolvedNode.match(graphNode)) { - return true; - } - } - return false; - } - - private boolean isExplicitDataset(Environment environment) { - return environment != null - && environment.getQuery() != null - && environment.getQuery().isDatasetSpecified(); - } - - /** - * 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 layered {@link BindingEnvironment}, so already - * bound variables are pushed into {@link #queryPattern(Node, List, Edge, Environment)} before - * querying storage. + *

Each input binding is exposed through a layered {@link StorageBindingEnvironment}, so already + * bound variables are pushed into {@link StoragePatternTranslator#translate} before + * querying storage.

* * @param graphNode active graph node * @param from active dataset restriction @@ -395,17 +196,17 @@ private Node resolve(Node queryNode, Environment environment) { * @param inputBindings bindings produced by previous BGP edges * @return bindings extended with the matches of {@code queryEdge} */ - private List join( + 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); + List inputBindings) { + List results = new ArrayList<>(); + for (StorageBindingSet binding : inputBindings) { + Environment joinedEnvironment = new StorageBindingEnvironment(environment, binding); for (Edge candidate : getEdges(graphNode, from, queryEdge, joinedEnvironment)) { - BindingSet next = binding.copy(); + StorageBindingSet 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())) { @@ -415,314 +216,4 @@ private List join( } 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 BindingContext getBind() { - return delegate == null ? null : delegate.getBind(); - } - - @Override - public void setBind(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(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 Path getPath(Node queryNode) { - return delegate == null ? null : delegate.getPath(queryNode); - } - - @Override - public int count() { - return delegate == null ? 0 : delegate.count(); - } - - @Override - public KgramEventDispatcher 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(Expr varExpr) { - return delegate == null ? null : delegate.get(varExpr); - } - - @Override - public ApproximateSearchEnv getAppxSearchEnv() { - return delegate == null ? null : delegate.getAppxSearchEnv(); - } - - @Override - public Eval getEval() { - return delegate == null ? null : delegate.getEval(); - } - - @Override - public void setEval(Eval eval) { - if (delegate != null) { - delegate.setEval(eval); - } - } - - @Override - public ProcessVisitor getVisitor() { - return delegate == null ? null : delegate.getVisitor(); - } - - @Override - public DatatypeValue getReport() { - return delegate == null ? null : delegate.getReport(); - } - - @Override - public void setReport(DatatypeValue datatype) { - if (delegate != null) { - delegate.setReport(datatype); - } - } - - @Override - public int size() { - return delegate == null ? 0 : delegate.size(); - } - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StoragePatternTranslator.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StoragePatternTranslator.java new file mode 100644 index 000000000..f715d2436 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StoragePatternTranslator.java @@ -0,0 +1,255 @@ +package fr.inria.corese.core.next.query.impl.engine.storage; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Resource; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.spi.Environment; +import fr.inria.corese.core.next.storage.api.model.StatementPattern; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Translates KGRAM query edges and dataset/graph constraints into storage-layer statement patterns. + */ +final class StoragePatternTranslator { + + private StoragePatternTranslator() {} + + /** + * 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.

+ */ + record StorageQueryPattern(StatementPattern statementPattern, boolean noMatch) { + + static StorageQueryPattern of(StatementPattern statementPattern) { + return new StorageQueryPattern(statementPattern, false); + } + + 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.

+ */ + record ContextSelection(List contexts, boolean noMatch) { + + static ContextSelection of(List contexts) { + return new ContextSelection(List.copyOf(contexts), false); + } + + static ContextSelection allContexts() { + return of(List.of()); + } + + static ContextSelection emptyResult() { + return new ContextSelection(List.of(), true); + } + + Resource[] contextsArray() { + return contexts.toArray(Resource[]::new); + } + } + + /** + * 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 + */ + static StorageQueryPattern translate( + 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 = 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); + } + + // 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 + */ + static 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.

+ */ + static ContextSelection contextSelection(Node graphNode, List from, Environment environment) { + List activeGraphs = selectActiveGraphs(graphNode, from, environment); + if (graphNode != null) { + return selectExplicitGraphContext(graphNode, activeGraphs, environment); + } + return selectDatasetContexts(activeGraphs, environment); + } + + static List selectActiveGraphs(Node graphNode, List from, Environment environment) { + if (isExplicitDataset(environment)) { + return graphNode == null + ? environment.getQuery().getFrom() + : environment.getQuery().getNamed(); + } + return from; + } + + static ContextSelection selectExplicitGraphContext( + Node graphNode, + List activeGraphs, + Environment environment) { + Node resolvedGraphNode = resolve(graphNode, environment); + if (resolvedGraphNode == null) { + return isExplicitDataset(environment) && (activeGraphs == null || activeGraphs.isEmpty()) + ? ContextSelection.emptyResult() + : ContextSelection.allContexts(); + } + Value value = rdfValue(resolvedGraphNode); + if (!(value instanceof Resource resource)) { + return ContextSelection.emptyResult(); + } + if (!matchesFrom(resolvedGraphNode, activeGraphs, environment)) { + return ContextSelection.emptyResult(); + } + return ContextSelection.of(List.of(resource)); + } + + static ContextSelection selectDatasetContexts(List activeGraphs, Environment environment) { + if (activeGraphs == null || activeGraphs.isEmpty()) { + return isExplicitDataset(environment) + ? ContextSelection.emptyResult() + : ContextSelection.allContexts(); + } + + List contexts = new ArrayList<>(); + for (Node node : activeGraphs) { + 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); + } + + /** + * 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 + */ + static boolean matchesFrom(Node graphNode, List from, Environment environment) { + if (from == null || from.isEmpty()) { + return !isExplicitDataset(environment); + } + for (Node fromNode : from) { + Node resolvedNode = resolve(fromNode, environment); + if (resolvedNode != null && resolvedNode.match(graphNode)) { + return true; + } + } + return false; + } + + static boolean isExplicitDataset(Environment environment) { + return environment != null + && environment.getQuery() != null + && environment.getQuery().isDatasetSpecified(); + } + + /** + * 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 + */ + static Node resolve(Node queryNode, Environment environment) { + if (queryNode == null) { + return null; + } + if (queryNode.isConstant()) { + return queryNode; + } + return environment == null ? null : environment.getNode(queryNode); + } + + static Value rdfValue(Node node) { + DatatypeValue value = Objects.requireNonNull(node, "node").getDatatypeValue(); + if (value instanceof Value rdfValue) { + return rdfValue; + } + throw new IllegalArgumentException("KGRAM node does not carry an RDF value: " + node); + } +}