diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index ce4e43c57..8e0ac6508 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -1,19 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; -import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.ASTConstants; -import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.DatasetClauseAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.DescribeQueryAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.FilterAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.OrderConditionAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.ProjectionAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.SolutionModifierAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.kgram.api.core.ExpType.Type; import fr.inria.corese.core.next.query.kgram.api.core.Filter; import fr.inria.corese.core.next.query.kgram.api.core.Node; @@ -33,8 +20,9 @@ * Builds KGRAM {@code Exp} / {@code Query} structures from Corese-next query AST nodes. * *
This bridge sits between the parsed SPARQL AST and the KGRAM runtime query model: - * it does not execute queries, it translates syntax-level query forms ({@code ASK}, {@code SELECT}, - * {@code DESCRIBE}) into runtime-ready {@link Query}/{@link Exp} structures.
+ * it does not execute queries, it translates syntax-level query forms ({@code ASK}, + * {@code SELECT}, {@code DESCRIBE}, {@code CONSTRUCT}) into runtime-ready + * {@link Query}/{@link Exp} structures. */ public final class CoreseAstQueryBuilder { @@ -125,6 +113,32 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) { return query; } + /** + * Builds a KGRAM {@link Query} from a {@link ConstructQueryAst}. + * + *Reuses the shared query shell (compiled {@code WHERE} body, dataset, LIMIT/OFFSET) and + * {@code ORDER BY} like the other forms, then compiles the {@code CONSTRUCT} template into a + * separate {@link Exp} carried by the query. Template variables reuse the runtime node bound by + * the body when visible; a template-only variable stays fresh (an unbound template variable is + * valid SPARQL, its triple is simply skipped at instantiation time). Clauses that require + * dedicated aggregate or values handling are rejected explicitly.
+ */ + public Query toNextQuery(ConstructQueryAst constructQueryAst) { + Objects.requireNonNull(constructQueryAst, "constructQueryAst"); + rejectUnsupportedConstructClauses(constructQueryAst); + + Query query = createQuery( + constructQueryAst.whereClause(), + constructQueryAst.datasetClause(), + constructQueryAst.solutionModifier()); + applyOrderBy(query, constructQueryAst.solutionModifier()); + Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate()); + query.setConstruct(true); + query.setConstruct(template); + query.setConstructNodes(template.getNodes()); + return query; + } + /** * Converts a SPARQL {@code FILTER} clause by converting {@link FilterAst#operator()} the same way as * {@link #toNextFilter(TermAst)}. @@ -423,4 +437,48 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) query.setOffset(Math.toIntExact(solutionModifier.offset())); } } + + private static void rejectUnsupportedConstructClauses(ConstructQueryAst constructQueryAst) { + if (!constructQueryAst.valuesClause().mappings().isEmpty()) { + throw new UnsupportedOperationException( + "Inline VALUES is not supported yet for CONSTRUCT (values handling is a follow-up)"); + } + SolutionModifierAst mod = constructQueryAst.solutionModifier(); + if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) { + throw new UnsupportedOperationException( + "Solution modifiers (GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for CONSTRUCT"); + } + // TODO(#389): inline VALUES needs a dedicated runtime mapping. + // TODO(#389): GROUP BY / HAVING would require aggregate-aware CONSTRUCT semantics, not just field copying. + // TODO(#389): DISTINCT / REDUCED are SELECT-only in the grammar; kept as a defensive guard. + } + + /** + * Compiles a {@code CONSTRUCT} template into a KGRAM {@link Exp} (a BGP of edges), kept separate + * from the {@code WHERE} body and carried by {@link Query#setConstruct(Exp)}. + */ + private Exp compileConstructTemplate(Query query, ConstructTemplateAst template) { + Exp bgp = Exp.create(Type.BGP); + for (TriplePatternAst triple : template.triplePatternAsts()) { + Node subject = constructNode(query, triple.subject()); + Node predicate = constructNode(query, triple.predicate()); + Node object = constructNode(query, triple.object()); + bgp.add(new AstBackedEdge(subject, predicate, object)); + } + return bgp; + } + + /** + * Resolves a template term to a runtime {@link Node}. A variable reuses the body node when it is + * bound by the {@code WHERE}; otherwise it stays a fresh node (an unbound template variable is + * valid SPARQL and simply skips its triple at instantiation, so this does not throw). IRIs, blank + * nodes and literals become fresh constant nodes. + */ + private Node constructNode(Query query, TermAst term) { + if (term instanceof VarAst(String name)) { + Node bound = query.getExtNode(name); + return bound != null ? bound : toNode(term); + } + return toNode(term); + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java new file mode 100644 index 000000000..0a4518ffe --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java @@ -0,0 +1,149 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.impl.parser.AbstractSparqlParserFeatureTest; +import fr.inria.corese.core.next.query.impl.parser.SparqlParser; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +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.core.Exp; +import fr.inria.corese.core.next.query.kgram.core.Query; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class CoreseAstQueryBuilderConstructTest extends AbstractSparqlParserFeatureTest { + + private final CoreseAstQueryBuilder builder = new CoreseAstQueryBuilder(); + + private static GroupGraphPatternAst whereSPO() { + return new GroupGraphPatternAst(List.of(new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))))); + } + + private static ConstructTemplateAst template(TriplePatternAst... triples) { + return new ConstructTemplateAst(List.of(triples)); + } + + @Test + @DisplayName("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o }: flagged CONSTRUCT, template is a BGP with one edge") + void templateCompiledAsBgp() { + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), whereSPO()); + + Query query = builder.toNextQuery(construct); + assertFalse(query.getConstructNodes().isEmpty(), "construct nodes populated"); + assertTrue(query.isConstruct(), "query flagged as CONSTRUCT"); + assertFalse(query.isSelect(), "CONSTRUCT should not also report itself as SELECT"); + assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); + Exp constructExp = query.getConstruct(); + assertNotNull(constructExp, "template stored via setConstruct(Exp)"); + assertTrue(constructExp.isBGP(), "template is a BGP"); + assertEquals(1, constructExp.size(), "one template triple -> one edge"); + } + + @Test + @DisplayName("Parser -> ConstructQueryAst -> Query keeps the CONSTRUCT form") + void buildsQueryFromParsedConstruct() { + SparqlParser parser = newParserDefault(); + ConstructQueryAst construct = assertInstanceOf(ConstructQueryAst.class, parser.parse("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o }")); + + Query query = builder.toNextQuery(construct); + + assertTrue(query.isConstruct()); + assertFalse(query.isSelect()); + assertTrue(query.getBody().get(0).isBGP()); + assertNotNull(query.getConstruct()); + assertEquals(1, query.getConstruct().size()); + } + + @Test + @DisplayName("Two template triples produce two edges") + void templateWithTwoTriples() { + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new IriAst("http://example.org/p"), new VarAst("o")), new TriplePatternAst(new VarAst("x"), new IriAst("http://example.org/q"), new VarAst("o"))), whereSPO()); + + Query query = builder.toNextQuery(construct); + + assertEquals(2, query.getConstruct().size()); + } + + @Test + @DisplayName("Template variable bound by WHERE reuses the runtime body node") + void templateVariableResolvesToBodyNode() { + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), whereSPO()); + + Query query = builder.toNextQuery(construct); + + Edge edge = query.getConstruct().first().getEdge(); + assertSame(query.getExtNode("x"), edge.getNode(0), "template ?x is the body node"); + assertSame(query.getExtNode("o"), edge.getNode(1), "template ?o is the body node"); + } + + @Test + @DisplayName("Template-only variable (not in WHERE) stays fresh and does not throw") + void templateOnlyVariableStaysFresh() { + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("newVar"))), whereSPO()); + + Query query = assertDoesNotThrow(() -> builder.toNextQuery(construct)); + + assertNull(query.getExtNode("newVar"), "template-only variable is not injected into the body"); + Node object = query.getConstruct().first().getEdge().getNode(1); + assertTrue(object.isVariable(), "template-only variable is still a variable node"); + } + + @Test + @DisplayName("Template IRIs become fixed (non-variable) nodes") + void templateWithFixedResources() { + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new IriAst("http://example.org/s"), new IriAst("http://example.org/p"), new VarAst("o"))), whereSPO()); + + Query query = builder.toNextQuery(construct); + + Edge edge = query.getConstruct().first().getEdge(); + assertFalse(edge.getNode(0).isVariable(), "fixed subject IRI"); + assertFalse(edge.getProperty().isVariable(), "fixed predicate IRI"); + assertTrue(edge.getNode(1).isVariable(), "object still bound to body ?o"); + } + + @Test + @DisplayName("FROM / FROM NAMED, LIMIT and OFFSET are applied") + void appliesDatasetLimitOffset() { + DatasetClauseAst dataset = new DatasetClauseAst(Set.of(new IriAst("http://example.org/g")), Set.of(new IriAst("http://example.org/n"))); + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(), 5L, 2L); + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), dataset, whereSPO(), mod); + + Query query = builder.toNextQuery(construct); + + assertEquals(List.of("http://example.org/g"), query.getFrom().stream().map(Node::getLabel).toList(), "FROM applied"); + assertEquals(List.of("http://example.org/n"), query.getNamed().stream().map(Node::getLabel).toList(), "FROM NAMED applied"); + assertEquals(5, query.getLimit(), "LIMIT applied"); + assertEquals(2, query.getOffset(), "OFFSET applied"); + } + + @Test + @DisplayName("ORDER BY ?x is applied") + void appliesOrderBy() { + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(new OrderConditionAst(ASTConstants.OrderDirection.ASC, new VarAst("x"))), null, null); + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), DatasetClauseAst.none(), whereSPO(), mod); + + Query query = builder.toNextQuery(construct); + + assertFalse(query.getOrderBy().isEmpty(), "ORDER BY applied"); + } + + @Test + @DisplayName("Inline VALUES is not supported yet -> UnsupportedOperationException") + void rejectsValuesClause() { + ValuesAst values = new ValuesAst(List.of(new ValueMappingAst(Map.of(new VarAst("x"), new IriAst("http://example.org/v"))))); + ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), DatasetClauseAst.none(), whereSPO(), null, null, values); + + assertThrows(UnsupportedOperationException.class, () -> builder.toNextQuery(construct)); + } + + @Test + @DisplayName("toNextQuery((ConstructQueryAst) null) throws NullPointerException") + void rejectsNull() { + assertThrows(NullPointerException.class, () -> builder.toNextQuery((ConstructQueryAst) null)); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.java index 1ec39ad97..68135af42 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.java @@ -175,7 +175,7 @@ private static void assertLoweredDescribeConstruct(Query query, int describedNod assertNotNull(query.getConstruct(), "construct template created"); assertEquals(2 * describedNodeCount, query.getConstruct().size(), "construct template has outgoing and incoming triple patterns per described node"); - assertNotNull(query.getConstructNodes(), "construct nodes collected"); + assertFalse(query.getConstructNodes().isEmpty(), "construct nodes collected"); } /**