diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java index 976165ecf..2f3f492ea 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.query.kgram.api.core.Edge; import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.tool.KgramNodes; import java.util.Objects; @@ -12,7 +13,8 @@ final class AstBackedEdge implements Edge { private final Node subject; - private final Node predicate; + private final Node edgeNode; + private final Node edgeVariable; private final Node object; private final Node graph; @@ -22,7 +24,14 @@ final class AstBackedEdge implements Edge { AstBackedEdge(Node subject, Node predicate, Node object, Node graph) { this.subject = Objects.requireNonNull(subject, "subject"); - this.predicate = Objects.requireNonNull(predicate, "predicate"); + Node checkedPredicate = Objects.requireNonNull(predicate, "predicate"); + if (checkedPredicate.isVariable()) { + this.edgeNode = KgramNodes.rootProperty(); + this.edgeVariable = checkedPredicate; + } else { + this.edgeNode = checkedPredicate; + this.edgeVariable = null; + } this.object = Objects.requireNonNull(object, "object"); this.graph = graph; } @@ -39,17 +48,22 @@ public Node getNode(int i) { @Override public Node getProperty() { - return predicate; + return edgeVariable == null ? edgeNode : edgeVariable; + } + + @Override + public Node getEdgeNode() { + return edgeNode; } @Override public Node getEdgeVariable() { - return predicate.isVariable() ? predicate : null; + return edgeVariable; } @Override public String getEdgeLabel() { - return predicate.getLabel(); + return edgeNode.getLabel(); } /** @@ -82,6 +96,6 @@ public Edge getEdge() { @Override public String toString() { - return subject + " " + predicate + " " + object; + return subject + " " + getProperty() + " " + object; } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodes.java b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodes.java new file mode 100644 index 000000000..5d1563b60 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodes.java @@ -0,0 +1,143 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.path.Path; +import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.sparql.datatype.DatatypeMap; + +/** + * Factory for Corese-specific KGRAM nodes used by the Corese-next runtime model. + * + *
These nodes encode runtime conventions that are not ordinary query terms. + * Keeping them here makes the convention explicit and prevents bridge classes + * from depending on historical parser constants.
+ */ +public final class KgramNodes { + + /** + * Runtime predicate used by KGRAM to represent a variable predicate edge. + */ + public static final String ROOT_PROPERTY_URI = "http://www.inria.fr/acacia/corese#Property"; + + private KgramNodes() { + } + + /** + * Creates the KGRAM root property node. + * + *KGRAM represents a triple pattern such as {@code ?s ?p ?o} with a + * root-property edge node and the predicate variable stored separately as + * the edge variable.
+ */ + public static Node rootProperty() { + return new RootPropertyNode(); + } + + private static final class RootPropertyNode implements Node { + + private static final IDatatype VALUE = DatatypeMap.newResource(ROOT_PROPERTY_URI); + + private int index = -1; + private String key = INITKEY; + + @Override + public int getIndex() { + return index; + } + + @Override + public void setIndex(int n) { + index = n; + } + + @Override + public String getKey() { + return key; + } + + @Override + public void setKey(String str) { + key = str; + } + + @Override + public boolean same(Node n) { + return n != null && !n.isVariable() && VALUE.sameTerm(n.getDatatypeValue()); + } + + @Override + public boolean match(Node n) { + return n != null && !n.isVariable() && VALUE.match(n.getDatatypeValue()); + } + + @Override + public int compare(Node node) { + return VALUE.compareTo(node.getDatatypeValue()); + } + + @Override + public String getLabel() { + return ROOT_PROPERTY_URI; + } + + @Override + public boolean isVariable() { + return false; + } + + @Override + public boolean isConstant() { + return true; + } + + @Override + public boolean isBlank() { + return false; + } + + @Override + public boolean isFuture() { + return false; + } + + @Override + public IDatatype getValue() { + return VALUE; + } + + @Override + public IDatatype getDatatypeValue() { + return getValue(); + } + + @Override + public Node getGraph() { + return null; + } + + @Override + public Node getNode() { + return this; + } + + @Override + public Object getNodeObject() { + return null; + } + + @Override + public void setObject(Object o) { + // Root property is an immutable runtime constant; no attached object is stored. + } + + @Override + public Path getPath() { + return null; + } + + @Override + public String toString() { + return '<' + ROOT_PROPERTY_URI + '>'; + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java index 4bf4f3ac4..8ea0eada2 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.tool.KgramNodes; import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; import fr.inria.corese.core.sparql.triple.parser.Constant; import fr.inria.corese.core.sparql.triple.parser.Variable; @@ -50,17 +51,25 @@ void edgeDoesNotContainOtherNode() { } @Test - @DisplayName("getEdgeVariable() exposes the predicate when it is a variable, null otherwise") + @DisplayName("Variable predicate is exposed as edge variable with root property as edge node") void edgeVariableExposedOnlyForVariablePredicate() { Node predicateVar = new NodeImpl(Variable.create("p")); AstBackedEdge withVarPredicate = new AstBackedEdge( new NodeImpl(Variable.create("s")), predicateVar, new NodeImpl(Variable.create("o"))); + + assertEquals(KgramNodes.ROOT_PROPERTY_URI, withVarPredicate.getEdgeNode().getLabel()); + assertEquals(KgramNodes.ROOT_PROPERTY_URI, withVarPredicate.getEdgeLabel()); + assertSame(predicateVar, withVarPredicate.getProperty()); assertSame(predicateVar, withVarPredicate.getEdgeVariable()); + Node iriPredicate = new NodeImpl(Constant.createResource("http://example.org/p")); AstBackedEdge withIriPredicate = new AstBackedEdge( new NodeImpl(Variable.create("s")), - new NodeImpl(Constant.createResource("http://example.org/p")), + iriPredicate, new NodeImpl(Variable.create("o"))); + assertSame(iriPredicate, withIriPredicate.getEdgeNode()); + assertSame(iriPredicate, withIriPredicate.getProperty()); + assertEquals("http://example.org/p", withIriPredicate.getEdgeLabel()); assertNull(withIriPredicate.getEdgeVariable()); } @@ -87,4 +96,4 @@ void explicitGraphIsReturned() { assertSame(graph, edge.getGraph()); } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderLegacyExecutionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderLegacyExecutionTest.java new file mode 100644 index 000000000..7892b1b3a --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderLegacyExecutionTest.java @@ -0,0 +1,466 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.Graph; +import fr.inria.corese.core.kgram.api.core.Edge; +import fr.inria.corese.core.kgram.api.core.ExpType.Type; +import fr.inria.corese.core.kgram.api.core.Filter; +import fr.inria.corese.core.kgram.api.core.Node; +import fr.inria.corese.core.kgram.core.Exp; +import fr.inria.corese.core.kgram.core.Mappings; +import fr.inria.corese.core.kgram.core.Query; +import fr.inria.corese.core.kgram.tool.NodeImpl; +import fr.inria.corese.core.next.query.impl.parser.AbstractSparqlParserFeatureTest; +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.query.QueryProcess; +import fr.inria.corese.core.sparql.datatype.DatatypeMap; +import fr.inria.corese.core.sparql.exceptions.EngineException; +import fr.inria.corese.core.sparql.triple.parser.ASTQuery; +import fr.inria.corese.core.sparql.triple.parser.Constant; +import fr.inria.corese.core.sparql.triple.parser.Variable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Verifies that {@code SELECT} queries parsed by the Corese-next SPARQL parser + * and compiled by {@link CoreseAstQueryBuilder} can be executed by the + * historical {@link QueryProcess} over a historical {@link Graph}. + * + *The legacy KGRAM adapter in this test is deliberately local to the test + * source set. It proves the interoperability point required by issue #471 without + * adding a legacy execution path to the Corese-next production bridge. When the + * historical query engine is removed, this compatibility test and its local + * adapter can be removed with it.
+ */ +class CoreseAstQueryBuilderLegacyExecutionTest extends AbstractSparqlParserFeatureTest { + + @Test + @DisplayName("Parser -> next Query -> legacy Query -> QueryProcess executes SELECT * WHERE { ?s ?p ?o }") + void executesSelectStarSpoOnLegacyGraph() throws EngineException { + Mappings mappings = execute("SELECT * WHERE { ?s ?p ?o }", graphWithFamilyData()); + + assertEquals(4, mappings.size()); + assertEquals("http://example.org/alice", mappings.getValue("s").getLabel()); + assertEquals("http://example.org/knows", mappings.getValue("p").getLabel()); + assertEquals("http://example.org/bob", mappings.getValue("o").getLabel()); + } + + @Test + @DisplayName("Explicit projection and constant predicate execute through legacy QueryProcess") + void executesExplicitProjectionWithConstantPredicate() throws EngineException { + Mappings mappings = execute( + "SELECT ?s ?o WHERE { ?sThe production bridge remains responsible for turning the parsed AST into + * the Corese-next runtime shape. This harness keeps only the legacy execution + * adaptation local to this test: it copies the runtime forms already produced + * by {@link WhereCompiler} and {@link CoreseAstQueryBuilder} when they have a + * direct counterpart in the historical KGRAM API, and rejects the rest + * explicitly. It does not repair query semantics that the Corese-next bridge + * failed to expose.
+ */ + private static final class TestLegacyQueryAdapter { + + private final MapThe adapter copies the edge node and edge variable already exposed by + * the Corese-next bridge. It does not repair predicate-variable semantics; + * the bridge is responsible for exposing the KGRAM root property as edge node + * and the predicate variable through {@link #getEdgeVariable()}.
+ */ + private static final class TestLegacyEdge implements Edge { + + private final Node subject; + private final Node edgeNode; + private final Node edgeVariable; + private final Node object; + + private TestLegacyEdge(Node subject, Node edgeNode, Node edgeVariable, Node object) { + this.subject = Objects.requireNonNull(subject, "subject"); + this.edgeNode = Objects.requireNonNull(edgeNode, "edgeNode"); + this.edgeVariable = edgeVariable; + this.object = Objects.requireNonNull(object, "object"); + } + + @Override + public Node getNode(int i) { + return switch (i) { + case 0 -> subject; + case 1 -> object; + default -> throw new IndexOutOfBoundsException( + "Triple pattern edge has nodes 0 (subject) and 1 (object), got: " + i); + }; + } + + @Override + public Node getProperty() { + return edgeVariable == null ? edgeNode : edgeVariable; + } + + @Override + public Node getEdgeNode() { + return edgeNode; + } + + @Override + public Node getEdgeVariable() { + return edgeVariable; + } + + @Override + public String getEdgeLabel() { + return edgeNode.getLabel(); + } + + @Override + public Node getGraph() { + return null; + } + + @Override + public Node getNode() { + return null; + } + + @Override + public boolean contains(Node node) { + if (node == null) { + return false; + } + for (int i = 0; i < nbNode(); i++) { + Node current = getNode(i); + if (current != null && (current == node || current.same(node))) { + return true; + } + } + return false; + } + + @Override + public Edge getEdge() { + return this; + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java index 945c59c57..f2a32ed97 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java @@ -4,6 +4,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.tool.KgramNodes; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -141,12 +142,14 @@ void serviceRestShouldExposeAQueryForRuntime() { } @Test - @DisplayName("BGP ?s ?p ?o — variable predicate exposed via getEdgeVariable()") + @DisplayName("BGP ?s ?p ?o — variable predicate uses KGRAM root property and edge variable") void edgeExposesVariablePredicateAsEdgeVariable() { Exp bgp = new WhereCompiler().compile(new BgpAst(List.of( new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o"))))); Exp edgeExp = bgp.get(0); + assertEquals(KgramNodes.ROOT_PROPERTY_URI, edgeExp.getEdge().getEdgeNode().getLabel()); + assertEquals(KgramNodes.ROOT_PROPERTY_URI, edgeExp.getEdge().getEdgeLabel()); assertNotNull(edgeExp.getEdge().getEdgeVariable(), "variable predicate should be exposed as edge variable"); assertTrue(edgeExp.getEdge().getEdgeVariable().isVariable()); } @@ -193,4 +196,4 @@ void serviceBodyShouldBeMarkedAsServiceQuery() { assertTrue(serviceExp.rest().getQuery().isService(), "SERVICE body query should be marked as service"); } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodesTest.java b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodesTest.java new file mode 100644 index 000000000..c8c57d289 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodesTest.java @@ -0,0 +1,40 @@ +package fr.inria.corese.core.next.query.kgram.tool; + +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class KgramNodesTest { + + @Test + @DisplayName("rootProperty() creates the KGRAM root property node") + void rootPropertyCreatesRootPropertyNode() { + Node rootProperty = KgramNodes.rootProperty(); + + assertEquals(KgramNodes.ROOT_PROPERTY_URI, rootProperty.getLabel()); + assertEquals(KgramNodes.ROOT_PROPERTY_URI, rootProperty.getDatatypeValue().getLabel()); + assertTrue(rootProperty.isConstant()); + assertFalse(rootProperty.isVariable()); + assertFalse(rootProperty.isBlank()); + assertNull(rootProperty.getGraph()); + assertNull(rootProperty.getPath()); + } + + @Test + @DisplayName("rootProperty() nodes compare as the same RDF term") + void rootPropertyNodesCompareAsSameTerm() { + Node left = KgramNodes.rootProperty(); + Node right = KgramNodes.rootProperty(); + + assertNotSame(left, right); + assertTrue(left.same(right)); + assertTrue(left.match(right)); + assertEquals(0, left.compare(right)); + } +}