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; + } + } +}