From d1c80a8b8a26c602b8b15f19eb43ccf7c8568688 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 3 Jul 2026 10:02:08 +0200 Subject: [PATCH 1/3] 389 [BRIDGE] QueryAst - Create Query from ConstructQueryAst --- .../sparql/bridge/CoreseAstQueryBuilder.java | 70 +++++++- .../CoreseAstQueryBuilderConstructTest.java | 149 ++++++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java 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..6ef0e4560 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 @@ -423,4 +423,72 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) query.setOffset(Math.toIntExact(solutionModifier.offset())); } } -} + + /** + * 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()); + query.setConstruct(true); + query.setConstruct(compileConstructTemplate(query, constructQueryAst.constructTemplate())); + return query; + } + + 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); + } +} \ No newline at end of file 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..622460c50 --- /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); + + 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 From abfc41f4deff4abdaf0bf03d9a17dbb811809731 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 6 Jul 2026 10:41:08 +0200 Subject: [PATCH 2/3] 389 [BRIDGE] QueryAst - Create Query from ConstructQueryAst --- .../sparql/bridge/CoreseAstQueryBuilder.java | 22 +++++-------------- .../CoreseAstQueryBuilderConstructTest.java | 2 +- .../CoreseAstQueryBuilderDescribeTest.java | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) 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 6ef0e4560..660d145cb 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,7 @@ * 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 { @@ -443,8 +429,10 @@ public Query toNextQuery(ConstructQueryAst constructQueryAst) { constructQueryAst.datasetClause(), constructQueryAst.solutionModifier()); applyOrderBy(query, constructQueryAst.solutionModifier()); + Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate()); query.setConstruct(true); - query.setConstruct(compileConstructTemplate(query, constructQueryAst.constructTemplate())); + query.setConstruct(template); + query.setConstructNodes(template.getNodes()); return query; } 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 index 622460c50..0a4518ffe 100644 --- 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 @@ -34,7 +34,7 @@ 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"); 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..d3e0d9e8c 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"); // était assertNotNull } /** From ecf60383b9ea854ef2116ad5643a3e2061184522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Mon, 6 Jul 2026 17:26:15 +0200 Subject: [PATCH 3/3] Clean up CONSTRUCT bridge review nits --- .../sparql/bridge/CoreseAstQueryBuilder.java | 58 ++++++++++--------- .../CoreseAstQueryBuilderDescribeTest.java | 2 +- 2 files changed, 31 insertions(+), 29 deletions(-) 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 660d145cb..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 @@ -20,7 +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}, {@code CONSTRUCT}) 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 { @@ -111,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)}. @@ -410,32 +438,6 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) } } - /** - * 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; - } - private static void rejectUnsupportedConstructClauses(ConstructQueryAst constructQueryAst) { if (!constructQueryAst.valuesClause().mappings().isEmpty()) { throw new UnsupportedOperationException( @@ -479,4 +481,4 @@ private Node constructNode(Query query, TermAst term) { } return toNode(term); } -} \ 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 d3e0d9e8c..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"); - assertFalse(query.getConstructNodes().isEmpty(), "construct nodes collected"); // était assertNotNull + assertFalse(query.getConstructNodes().isEmpty(), "construct nodes collected"); } /**