From 81bb2b51f88f3754c9385c375834cd7240bbd53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 2 Jul 2026 14:50:09 +0200 Subject: [PATCH 1/2] fix: expose variable predicates with KGRAM root property --- .../impl/sparql/bridge/AstBackedEdge.java | 28 +++- .../next/query/kgram/tool/KgramNodes.java | 143 ++++++++++++++++++ .../impl/sparql/bridge/AstBackedEdgeTest.java | 15 +- .../impl/sparql/bridge/WhereCompilerTest.java | 7 +- .../next/query/kgram/tool/KgramNodesTest.java | 40 +++++ 5 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodes.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/kgram/tool/KgramNodesTest.java 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/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)); + } +} From d1425ec3a2cb2bdbbc2bb4c2db13f82f396386d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 2 Jul 2026 14:50:28 +0200 Subject: [PATCH 2/2] test: execute next SELECT queries on legacy engine --- ...eseAstQueryBuilderLegacyExecutionTest.java | 466 ++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderLegacyExecutionTest.java 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 { ?s ?o }", + graphWithFamilyData()); + + assertEquals(2, mappings.size()); + assertEquals("http://example.org/alice", mappings.get(0).getValue("s").getLabel()); + assertEquals("http://example.org/bob", mappings.get(0).getValue("o").getLabel()); + assertEquals("http://example.org/alice", mappings.get(1).getValue("s").getLabel()); + assertEquals("http://example.org/carol", mappings.get(1).getValue("o").getLabel()); + } + + @Test + @DisplayName("Multiple triple patterns join after parser -> next Query -> legacy Query") + void executesJoinedBgp() throws EngineException { + Mappings mappings = execute(""" + SELECT ?friend ?name WHERE { + ?friend . + ?friend ?name + } + """, graphWithFamilyData()); + + assertEquals(2, mappings.size()); + assertEquals("Bob", mappings.get(0).getValue("name").getLabel()); + assertEquals("Carol", mappings.get(1).getValue("name").getLabel()); + } + + @Test + @DisplayName("FILTER expressions carried by the next bridge execute in the legacy engine") + void executesFilter() throws EngineException { + Mappings mappings = execute(""" + SELECT ?friend WHERE { + ?friend . + FILTER(?friend = ) + } + """, graphWithFamilyData()); + + assertEquals(1, mappings.size()); + assertEquals("http://example.org/bob", mappings.getValue("friend").getLabel()); + } + + @Test + @DisplayName("OPTIONAL patterns compiled by WhereCompiler execute in the legacy engine") + void executesOptional() throws EngineException { + Mappings mappings = execute(""" + SELECT ?friend ?name WHERE { + ?friend . + OPTIONAL { ?friend ?name } + } + """, graphWithFamilyData()); + + assertEquals(2, mappings.size()); + assertEquals("Bob", mappings.get(0).getValue("name").getLabel()); + assertEquals("Carol", mappings.get(1).getValue("name").getLabel()); + } + + @Test + @DisplayName("UNION patterns compiled by WhereCompiler execute in the legacy engine") + void executesUnion() throws EngineException { + Mappings mappings = execute(""" + SELECT ?person WHERE { + { ?person "Bob" } + UNION + { ?person "Carol" } + } + """, graphWithFamilyData()); + + assertEquals(2, mappings.size()); + assertEquals("http://example.org/bob", mappings.get(0).getValue("person").getLabel()); + assertEquals("http://example.org/carol", mappings.get(1).getValue("person").getLabel()); + } + + @Test + @DisplayName("ORDER BY copied by CoreseAstQueryBuilder executes in legacy QueryProcess") + void executesOrderBy() throws EngineException { + Mappings mappings = execute(""" + SELECT ?friend WHERE { + ?friend + } + ORDER BY DESC(?friend) + """, graphWithFamilyData()); + + assertEquals(2, mappings.size()); + assertEquals("http://example.org/carol", mappings.get(0).getValue("friend").getLabel()); + assertEquals("http://example.org/bob", mappings.get(1).getValue("friend").getLabel()); + } + + @Test + @DisplayName("DISTINCT, LIMIT and OFFSET copied by CoreseAstQueryBuilder execute in legacy QueryProcess") + void executesDistinctLimitOffset() throws EngineException { + Mappings mappings = execute(""" + SELECT DISTINCT ?friend WHERE { + ?friend + } + OFFSET 1 + LIMIT 1 + """, graphWithFamilyData()); + + assertEquals(1, mappings.size()); + assertEquals("http://example.org/carol", mappings.getValue("friend").getLabel()); + } + + private Mappings execute(String sparql, Graph graph) throws EngineException { + QueryAst ast = newParserDefault().parse(sparql); + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast); + fr.inria.corese.core.next.query.kgram.core.Query nextQuery = + new CoreseAstQueryBuilder().toNextQuery(select); + Query query = TestLegacyQueryAdapter.convert(nextQuery); + return QueryProcess.create(graph).query(query); + } + + private static Graph graphWithFamilyData() { + Graph graph = Graph.create(); + insert(graph, "http://example.org/alice", "http://example.org/knows", "http://example.org/bob"); + insert(graph, "http://example.org/alice", "http://example.org/knows", "http://example.org/carol"); + insertLiteral(graph, "http://example.org/bob", "http://example.org/name", "Bob"); + insertLiteral(graph, "http://example.org/carol", "http://example.org/name", "Carol"); + return graph; + } + + private static void insert(Graph graph, String subject, String predicate, String object) { + graph.insert( + DatatypeMap.newResource(subject), + DatatypeMap.newResource(predicate), + DatatypeMap.newResource(object)); + } + + private static void insertLiteral(Graph graph, String subject, String predicate, String object) { + graph.insert( + DatatypeMap.newResource(subject), + DatatypeMap.newResource(predicate), + DatatypeMap.newLiteral(object)); + } + + /** + * Test harness that adapts the Corese-next KGRAM query built by + * {@link CoreseAstQueryBuilder} to the legacy KGRAM query classes expected by + * {@link QueryProcess}. + * + *

The 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 Map variables = new LinkedHashMap<>(); + + private TestLegacyQueryAdapter() { + } + + private static Query convert(fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + return new TestLegacyQueryAdapter().convertQuery(nextQuery); + } + + private Query convertQuery(fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + rejectUnsupportedQueryShape(nextQuery); + + Query query = Query.create(convertBody(nextQuery.getBody())); + query.setAST(selectAst()); + query.collect(); + applySelect(query, nextQuery); + applyDataset(query, nextQuery); + applySolutionModifiers(query, nextQuery); + return query; + } + + private static void rejectUnsupportedQueryShape(fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + if (nextQuery.getSelect().isEmpty()) { + throw new IllegalArgumentException("Test adapter expects SELECT variables from the next bridge"); + } + } + + private static ASTQuery selectAst() { + ASTQuery ast = ASTQuery.create(); + ast.setResultForm(ASTQuery.QT_SELECT); + ast.setSelectAll(true); + return ast; + } + + private Exp convertBody(fr.inria.corese.core.next.query.kgram.core.Exp nextBody) { + if (!nextBody.isAnd()) { + throw new IllegalArgumentException( + "Test adapter expects an AND query body, got: " + nextBody.type()); + } + Exp body = Exp.create(Type.AND); + for (fr.inria.corese.core.next.query.kgram.core.Exp expression : nextBody) { + body.add(convertExpression(expression)); + } + return body; + } + + private Exp convertExpression(fr.inria.corese.core.next.query.kgram.core.Exp expression) { + if (expression.isAnd()) { + return convertBody(expression); + } + if (expression.isBGP()) { + return convertBgp(expression); + } + if (expression.isFilter()) { + return Exp.create(Type.FILTER, filter(expression.getFilter())); + } + if (expression.isOptional()) { + return Exp.create( + Type.OPTIONAL, + convertExpression(expression.first()), + convertExpression(expression.rest())); + } + if (expression.isUnion()) { + return Exp.create( + Type.UNION, + convertExpression(expression.first()), + convertExpression(expression.rest())); + } + if (expression.isMinus()) { + return Exp.create( + Type.MINUS, + convertExpression(expression.first()), + convertExpression(expression.rest())); + } + if (expression.isBind()) { + Exp bind = Exp.create(Type.BIND); + bind.setFilter(filter(expression.getFilter())); + bind.setFunctional(expression.isFunctional()); + bind.setNode(node(expression.getNode())); + return bind; + } + // TODO(next-query): SERVICE requires a network-aware execution policy before this adapter can copy it. + // TODO(next-query): VALUES requires converting next mappings to legacy mappings. + throw new IllegalArgumentException( + "Test adapter does not support expression type: " + expression.type()); + } + + private Exp convertBgp(fr.inria.corese.core.next.query.kgram.core.Exp nextBgp) { + Exp exp = Exp.create(Type.BGP); + for (fr.inria.corese.core.next.query.kgram.core.Exp expression : nextBgp) { + if (!expression.isEdge()) { + throw new IllegalArgumentException( + "Test adapter supports only EDGE expressions inside BGP, got: " + expression.type()); + } + exp.add(Exp.create(Type.EDGE, edge(expression.getEdge()))); + } + return exp; + } + + private Edge edge(fr.inria.corese.core.next.query.kgram.api.core.Edge nextEdge) { + return new TestLegacyEdge( + node(nextEdge.getNode(0)), + node(nextEdge.getEdgeNode()), + nextEdge.getEdgeVariable() == null ? null : node(nextEdge.getEdgeVariable()), + node(nextEdge.getNode(1))); + } + + private Node node(fr.inria.corese.core.next.query.kgram.api.core.Node nextNode) { + if (nextNode.isVariable()) { + return variables.computeIfAbsent(nextNode.getLabel(), label -> new NodeImpl(Variable.create(label))); + } + return new NodeImpl(Constant.create(nextNode.getDatatypeValue())); + } + + private static Filter filter(fr.inria.corese.core.next.query.kgram.api.core.Filter nextFilter) { + return nextFilter.getFilterExpression(); + } + + private void applySelect( + Query legacyQuery, + fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + List selectExpressions = new ArrayList<>(); + for (fr.inria.corese.core.next.query.kgram.api.core.Node nextNode : nextQuery.getSelect()) { + selectExpressions.add(Exp.create(Type.NODE, node(nextNode))); + } + + legacyQuery.setSelectFun(selectExpressions); + legacyQuery.setSelect(selectNodeList(selectExpressions)); + } + + private void applyDataset( + Query legacyQuery, + fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + legacyQuery.setFrom(nodeList(nextQuery.getFrom())); + legacyQuery.setNamed(nodeList(nextQuery.getNamed())); + } + + private void applySolutionModifiers( + Query legacyQuery, + fr.inria.corese.core.next.query.kgram.core.Query nextQuery) { + legacyQuery.setDistinct(nextQuery.isDistinct()); + legacyQuery.setLimit(nextQuery.getLimit()); + legacyQuery.setOffset(nextQuery.getOffset()); + legacyQuery.setOrderBy(orderByList(nextQuery.getOrderBy())); + } + + private List orderByList(List nextOrderBy) { + List legacyOrderBy = new ArrayList<>(); + for (fr.inria.corese.core.next.query.kgram.core.Exp nextExpression : nextOrderBy) { + Exp expression = Exp.create(Type.NODE, node(nextExpression.getNode())); + if (nextExpression.getFilter() != null) { + expression.setFilter(filter(nextExpression.getFilter())); + } + expression.status(nextExpression.status()); + legacyOrderBy.add(expression); + } + return legacyOrderBy; + } + + private static List selectNodeList(List selectExpressions) { + LinkedHashSet nodes = new LinkedHashSet<>(); + for (Exp expression : selectExpressions) { + nodes.add(expression.getNode()); + } + return new ArrayList<>(nodes); + } + + private List nodeList(List nextNodes) { + List nodes = new ArrayList<>(); + for (fr.inria.corese.core.next.query.kgram.api.core.Node nextNode : nextNodes) { + nodes.add(node(nextNode)); + } + return nodes; + } + } + + /** + * Legacy query edge converted from a Corese-next query edge. + * + *

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