From 937fdd07a553c4cc19a4fd5b5e435a01f496f4be Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 29 Jun 2026 16:16:13 +0200 Subject: [PATCH 1/9] 390 [BRIDGE] QueryAst - Create Query from DescribeQueryAst --- .../sparql/bridge/CoreseAstQueryBuilder.java | 82 ++++++++++++- .../core/next/query/kgram/core/Query.java | 17 +++ .../bridge/CoreseAstQueryBuilderAskTest.java | 2 +- .../CoreseAstQueryBuilderDescribeTest.java | 113 ++++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.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 3f5db08b8..07fa48d10 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 @@ -16,7 +16,6 @@ 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; -import fr.inria.corese.core.next.query.kgram.core.Exp; import fr.inria.corese.core.next.query.kgram.core.Query; import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; import fr.inria.corese.core.sparql.triple.parser.Atom; @@ -317,4 +316,85 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) query.setOffset(Math.toIntExact(solutionModifier.offset())); } } + + static Node toNode(TermAst term) { + Expression expression = SparqlAstToExpression.convert(term); + if (expression instanceof Atom atom) { + return new NodeImpl(atom); + } + throw new IllegalArgumentException( + "A query term must be a variable, IRI or literal, got: " + + term.getClass().getSimpleName()); + } + + /** + * Converts a SPARQL {@code FILTER} clause by converting {@link FilterAst#operator()} the same way as + * {@link #toNextFilter(TermAst)}. + */ + public Filter toNextFilter(FilterAst filterClause) { + Objects.requireNonNull(filterClause, "filterClause"); + return toNextFilter(filterClause.operator()); + } + + /** + * Converts a filter expression carried as {@link TermAst}: must be a {@link ConstraintAst}. + */ + public Filter toNextFilter(TermAst filterExpression) { + Objects.requireNonNull(filterExpression, "filterExpression"); + if (!(filterExpression instanceof ConstraintAst constraint)) { + throw new IllegalArgumentException( + "FILTER expects a ConstraintAst, got: " + filterExpression.getClass().getName()); + } + return toNextFilter(constraint); + } + + /** + * Converts a constraint tree (boolean filter expression) into a KGRAM {@link Filter}. + */ + public Filter toNextFilter(ConstraintAst filterExpression) { + Objects.requireNonNull(filterExpression, "filterExpression"); + return SparqlAstToExpression.toNextFilter(filterExpression); + } + + public Query toQuery(DescribeQueryAst describe) { + Objects.requireNonNull(describe, "describe"); + rejectUnsupportedClauses(describe.datasetClause(), describe.valuesClause(), describe.solutionModifier()); + + Exp body = whereCompiler.compile(describe.whereClause()); + Query query = Query.create(body); + query.setDescribe(true); + + List nodes = new ArrayList<>(); + for (TermAst term : describe.described()) { + nodes.add(toNode(term)); + } + query.setDescribeList(nodes); + + return query; + } + + private static void rejectUnsupportedClauses(DatasetClauseAst dataset, ValuesAst values, SolutionModifierAst mod) { + if (!dataset.graphs().isEmpty() || !dataset.namedGraphs().isEmpty()) { + throw new UnsupportedOperationException( + "FROM / FROM NAMED is not supported yet (dataset handling is a follow-up)"); + } + if (!values.mappings().isEmpty()) { + throw new UnsupportedOperationException( + "Inline VALUES is not supported yet (values handling is a follow-up)"); + } + if (mod.hasOrderBy() || mod.hasGroupBy() || mod.hasHaving() + || mod.hasLimit() || mod.hasOffset() || mod.distinct() || mod.reduced()) { + throw new UnsupportedOperationException( + "Solution modifiers (ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT, REDUCED) are not supported for DESCRIBE"); + } + } + + private static Node toNode(TermAst term) { + Expression e = SparqlAstToExpression.convert(term); + if (e instanceof Atom atom) { + return new NodeImpl(atom); + } + throw new IllegalArgumentException( + "A DESCRIBE term must be a variable or IRI, got: " + term.getClass().getSimpleName()); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java index f3127d5b6..a591d62e4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java @@ -80,6 +80,8 @@ public static DQPFactory getFactory() { //selectWithExp, orderBy, groupBy; List failure, pathFilter, funList; + private List describeList = new ArrayList<>(); + List errors, info; Exp having, construct, delete; // gNode is a local graph node when subquery has no ?g in its select @@ -140,6 +142,7 @@ public static DQPFactory getFactory() { boolean isDelete = false; boolean isUpdate = false; boolean isAsk = false; + boolean isDescribe = false; boolean isCheckLoop = false; boolean isListGroup = false; boolean isListPath = true; @@ -693,7 +696,21 @@ public boolean isAsk() { public void setAsk(boolean b) { isAsk = b; } + public boolean isDescribe() { + return isDescribe; + } + + public void setDescribe(boolean b) { + isDescribe = b; + } + public List getDescribeList() { + return describeList; + } + + public void setDescribeList(List list) { + describeList = list; + } public boolean isTest() { return isTest; } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderAskTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderAskTest.java index b4e3a3f1a..603320a88 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderAskTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderAskTest.java @@ -123,7 +123,7 @@ void rejectsValuesClause() { } @Test - @DisplayName("toNextQuery(null) throws NullPointerException") + @DisplayName("toNextQuery((AskQueryAst) null) throws NullPointerException") void rejectsNullAsk() { assertThrows(NullPointerException.class, () -> builder.toNextQuery((AskQueryAst) null)); } 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 new file mode 100644 index 000000000..4a0b23fbc --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderDescribeTest.java @@ -0,0 +1,113 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +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 CoreseAstQueryBuilderDescribeTest { + + private final CoreseAstQueryBuilder builder = new CoreseAstQueryBuilder(); + + private static GroupGraphPatternAst whereBindingX() { + return new GroupGraphPatternAst(List.of( + new BgpAst(List.of( + new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))))); + } + + @Test + @DisplayName("DESCRIBE ?x WHERE { ... }: body compiled, DESCRIBE flag set, ?x in the describe list") + void describesVariableBoundByWhere() { + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX()); + + Query query = builder.toQuery(describe); + + assertTrue(query.isDescribe(), "query flagged as DESCRIBE"); + assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); + assertEquals(1, query.getDescribeList().size()); + assertTrue(query.getDescribeList().getFirst().isVariable(), "described term is the variable ?x"); + } + + @Test + @DisplayName("DESCRIBE with no WHERE: empty body, IRI in the describe list") + void describesFixedResourceWithoutWhere() { + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new IriAst("")), null); + + Query query = builder.toQuery(describe); + + assertTrue(query.isDescribe()); + assertTrue(query.getBody().isAnd(), "default empty WHERE compiles to an empty AND"); + assertEquals(0, query.getBody().size(), "no pattern in the body"); + assertEquals(1, query.getDescribeList().size()); + assertFalse(query.getDescribeList().getFirst().isVariable(), "described term is a fixed IRI"); + } + + @Test + @DisplayName("DESCRIBE * : DESCRIBE flag set and an empty describe list (= describe all in-scope)") + void describeAllHasEmptyList() { + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(), whereBindingX()); + + Query query = builder.toQuery(describe); + + assertTrue(query.isDescribe()); + assertTrue(query.getDescribeList().isEmpty(), "DESCRIBE * carries no explicit node"); + } + + @Test + @DisplayName("FROM / FROM NAMED is not supported yet → UnsupportedOperationException") + void rejectsDatasetClause() { + DatasetClauseAst dataset = new DatasetClauseAst( + Set.of(new IriAst("http://example.org/g")), Set.of()); + DescribeQueryAst describe = new DescribeQueryAst( + dataset, List.of(new VarAst("x")), whereBindingX()); + + assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + } + + @Test + @DisplayName("FROM NAMED is not supported yet → UnsupportedOperationException") + void rejectsFromNamedClause() { + DatasetClauseAst dataset = new DatasetClauseAst( + Set.of(), Set.of(new IriAst("http://example.org/g"))); + DescribeQueryAst describe = new DescribeQueryAst( + dataset, List.of(new VarAst("x")), whereBindingX()); + + assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + } + + @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"))))); + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), null, null, values); + + assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + } + + @Test + @DisplayName("Solution modifiers (LIMIT) are not supported yet → UnsupportedOperationException") + void rejectsSolutionModifier() { + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(), 10L, null); + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), mod); + + assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + } + + @Test + @DisplayName("toQuery(null) throws NullPointerException") + void rejectsNull() { + assertThrows(NullPointerException.class, () -> builder.toQuery((DescribeQueryAst) null)); + } +} \ No newline at end of file From 29285558846b80028691928e46f8fd82563aed20 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 30 Jun 2026 14:43:23 +0200 Subject: [PATCH 2/9] 390 [BRIDGE] QueryAst - Create Query from DescribeQueryAst --- .../sparql/bridge/CoreseAstQueryBuilder.java | 84 ++++++++----------- .../CoreseAstQueryBuilderDescribeTest.java | 48 ++++++----- 2 files changed, 62 insertions(+), 70 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 07fa48d10..084410928 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,12 @@ 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.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.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; import fr.inria.corese.core.next.query.kgram.core.Query; @@ -207,6 +200,39 @@ private Query createQuery( return query; } + /** + * Builds a KGRAM {@link Query} from a SPARQL {@code DESCRIBE} query AST. + */ + public Query toNextQuery(DescribeQueryAst describeQueryAst) { + Objects.requireNonNull(describeQueryAst, "describeQueryAst"); + rejectUnsupportedClauses(describeQueryAst); + + Query query = Query.create(whereCompiler.compile(describeQueryAst.whereClause())); + applyDataset(query, describeQueryAst.datasetClause()); + applySolutionModifier(query, describeQueryAst.solutionModifier()); + query.setDescribe(true); + + List nodes = new ArrayList<>(); + for (TermAst term : describeQueryAst.described()) { + nodes.add(toNode(term)); + } + query.setDescribeList(nodes); + return query; + } + + private static void rejectUnsupportedClauses(DescribeQueryAst describeQueryAst) { + if (!describeQueryAst.valuesClause().mappings().isEmpty()) { + throw new UnsupportedOperationException( + "Inline VALUES is not supported yet for DESCRIBE (values handling is a follow-up)"); + } + SolutionModifierAst mod = describeQueryAst.solutionModifier(); + if (mod.hasOrderBy() || mod.hasGroupBy() || mod.hasHaving() + || mod.distinct() || mod.reduced()) { + throw new UnsupportedOperationException( + "Solution modifiers (ORDER BY, GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for DESCRIBE"); + } + } + private void applyDataset(Query query, DatasetClauseAst datasetClause) { query.setFrom(toNodeList(datasetClause.graphs())); query.setNamed(toNodeList(datasetClause.namedGraphs())); @@ -355,46 +381,4 @@ public Filter toNextFilter(ConstraintAst filterExpression) { Objects.requireNonNull(filterExpression, "filterExpression"); return SparqlAstToExpression.toNextFilter(filterExpression); } - - public Query toQuery(DescribeQueryAst describe) { - Objects.requireNonNull(describe, "describe"); - rejectUnsupportedClauses(describe.datasetClause(), describe.valuesClause(), describe.solutionModifier()); - - Exp body = whereCompiler.compile(describe.whereClause()); - Query query = Query.create(body); - query.setDescribe(true); - - List nodes = new ArrayList<>(); - for (TermAst term : describe.described()) { - nodes.add(toNode(term)); - } - query.setDescribeList(nodes); - - return query; - } - - private static void rejectUnsupportedClauses(DatasetClauseAst dataset, ValuesAst values, SolutionModifierAst mod) { - if (!dataset.graphs().isEmpty() || !dataset.namedGraphs().isEmpty()) { - throw new UnsupportedOperationException( - "FROM / FROM NAMED is not supported yet (dataset handling is a follow-up)"); - } - if (!values.mappings().isEmpty()) { - throw new UnsupportedOperationException( - "Inline VALUES is not supported yet (values handling is a follow-up)"); - } - if (mod.hasOrderBy() || mod.hasGroupBy() || mod.hasHaving() - || mod.hasLimit() || mod.hasOffset() || mod.distinct() || mod.reduced()) { - throw new UnsupportedOperationException( - "Solution modifiers (ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT, REDUCED) are not supported for DESCRIBE"); - } - } - - private static Node toNode(TermAst term) { - Expression e = SparqlAstToExpression.convert(term); - if (e instanceof Atom atom) { - return new NodeImpl(atom); - } - throw new IllegalArgumentException( - "A DESCRIBE term must be a variable or IRI, got: " + term.getClass().getSimpleName()); - } -} +} \ 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 4a0b23fbc..2550ae551 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 @@ -27,7 +27,7 @@ void describesVariableBoundByWhere() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX()); - Query query = builder.toQuery(describe); + Query query = builder.toNextQuery(describe); assertTrue(query.isDescribe(), "query flagged as DESCRIBE"); assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); @@ -41,7 +41,7 @@ void describesFixedResourceWithoutWhere() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new IriAst("")), null); - Query query = builder.toQuery(describe); + Query query = builder.toNextQuery(describe); assertTrue(query.isDescribe()); assertTrue(query.getBody().isAnd(), "default empty WHERE compiles to an empty AND"); @@ -56,32 +56,39 @@ void describeAllHasEmptyList() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(), whereBindingX()); - Query query = builder.toQuery(describe); + Query query = builder.toNextQuery(describe); assertTrue(query.isDescribe()); assertTrue(query.getDescribeList().isEmpty(), "DESCRIBE * carries no explicit node"); } @Test - @DisplayName("FROM / FROM NAMED is not supported yet → UnsupportedOperationException") - void rejectsDatasetClause() { + @DisplayName("FROM / FROM NAMED are applied to the query dataset") + void appliesDatasetClause() { DatasetClauseAst dataset = new DatasetClauseAst( - Set.of(new IriAst("http://example.org/g")), Set.of()); + Set.of(new IriAst("http://example.org/g")), + Set.of(new IriAst("http://example.org/n"))); DescribeQueryAst describe = new DescribeQueryAst( dataset, List.of(new VarAst("x")), whereBindingX()); - assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + Query query = builder.toNextQuery(describe); + + assertEquals(1, query.getFrom().size(), "FROM applied"); + assertEquals(1, query.getNamed().size(), "FROM NAMED applied"); } @Test - @DisplayName("FROM NAMED is not supported yet → UnsupportedOperationException") - void rejectsFromNamedClause() { - DatasetClauseAst dataset = new DatasetClauseAst( - Set.of(), Set.of(new IriAst("http://example.org/g"))); + @DisplayName("LIMIT / OFFSET are applied to the query") + void appliesLimitAndOffset() { + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy( + false, false, List.of(), 5L, 2L); DescribeQueryAst describe = new DescribeQueryAst( - dataset, List.of(new VarAst("x")), whereBindingX()); + DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), mod); + + Query query = builder.toNextQuery(describe); - assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + assertEquals(5, query.getLimit(), "LIMIT applied"); + assertEquals(2, query.getOffset(), "OFFSET applied"); } @Test @@ -92,22 +99,23 @@ void rejectsValuesClause() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), null, null, values); - assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + assertThrows(UnsupportedOperationException.class, () -> builder.toNextQuery(describe)); } @Test - @DisplayName("Solution modifiers (LIMIT) are not supported yet → UnsupportedOperationException") - void rejectsSolutionModifier() { - SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(), 10L, null); + @DisplayName("Unsupported solution modifier (e.g. DISTINCT) → UnsupportedOperationException") + void rejectsUnsupportedModifier() { + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy( + true, false, List.of(), null, null); // DISTINCT DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), mod); - assertThrows(UnsupportedOperationException.class, () -> builder.toQuery(describe)); + assertThrows(UnsupportedOperationException.class, () -> builder.toNextQuery(describe)); } @Test - @DisplayName("toQuery(null) throws NullPointerException") + @DisplayName("toNextQuery((DescribeQueryAst) null) throws NullPointerException") void rejectsNull() { - assertThrows(NullPointerException.class, () -> builder.toQuery((DescribeQueryAst) null)); + assertThrows(NullPointerException.class, () -> builder.toNextQuery((DescribeQueryAst) null)); } } \ No newline at end of file From d2ff5af48e06d722bcd01ca897113a7505ef0594 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 1 Jul 2026 09:52:55 +0200 Subject: [PATCH 3/9] 390 [BRIDGE] QueryAst - Create Query from DescribeQueryAst --- .../sparql/bridge/CoreseAstQueryBuilder.java | 158 +++++++++--------- .../core/next/query/kgram/core/Query.java | 2 +- 2 files changed, 79 insertions(+), 81 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 084410928..11fff9413 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,14 +1,23 @@ 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.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; +import fr.inria.corese.core.next.query.kgram.core.Exp; import fr.inria.corese.core.next.query.kgram.core.Query; import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; import fr.inria.corese.core.sparql.triple.parser.Atom; @@ -24,8 +33,8 @@ * 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}) - * 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}) into runtime-ready {@link Query}/{@link Exp} structures.

*/ public final class CoreseAstQueryBuilder { @@ -92,6 +101,30 @@ public Query toNextQuery(SelectQueryAst selectQueryAst) { return query; } + /** + * Builds a KGRAM {@link Query} from a {@link DescribeQueryAst}. + * + *

Reuses the shared query shell (compiled {@code WHERE} body, dataset, LIMIT/OFFSET) and + * {@code ORDER BY} like the other forms. The described resources are resolved against the + * compiled body: a described variable reuses its runtime node (so it is bound by the body), + * a described IRI becomes a fresh constant node. {@code DESCRIBE *} reuses the in-scope nodes + * of the body, matching {@code SELECT *}. Clauses that require dedicated aggregate or values + * handling are rejected explicitly.

+ */ + public Query toNextQuery(DescribeQueryAst describeQueryAst) { + Objects.requireNonNull(describeQueryAst, "describeQueryAst"); + rejectUnsupportedDescribeClauses(describeQueryAst); + + Query query = createQuery( + describeQueryAst.whereClause(), + describeQueryAst.datasetClause(), + describeQueryAst.solutionModifier()); + applyOrderBy(query, describeQueryAst.solutionModifier()); + query.setDescribe(true); + query.setDescribeList(describeNodes(query, describeQueryAst)); + return query; + } + /** * Converts a SPARQL {@code FILTER} clause by converting {@link FilterAst#operator()} the same way as * {@link #toNextFilter(TermAst)}. @@ -157,13 +190,11 @@ private static void rejectUnsupportedSelectClauses(SelectQueryAst selectQueryAst if (!selectQueryAst.valuesClause().mappings().isEmpty()) { throw new UnsupportedOperationException("VALUES is not supported yet when building a next Query"); } - ProjectionAst projection = selectQueryAst.projection(); if (!projection.expressionTerms().isEmpty() || !projection.expressionBoundVariables().isEmpty()) { throw new UnsupportedOperationException( "SELECT expressions are not supported yet when building a next Query"); } - SolutionModifierAst solutionModifier = selectQueryAst.solutionModifier(); if (solutionModifier.reduced()) { throw new UnsupportedOperationException("REDUCED is not supported yet when building a next Query"); @@ -180,6 +211,21 @@ private static void rejectUnsupportedSelectClauses(SelectQueryAst selectQueryAst // TODO(#387): REDUCED support should be aligned with the final next-pipeline query-form policy. } + private static void rejectUnsupportedDescribeClauses(DescribeQueryAst describeQueryAst) { + if (!describeQueryAst.valuesClause().mappings().isEmpty()) { + throw new UnsupportedOperationException( + "Inline VALUES is not supported yet for DESCRIBE (values handling is a follow-up)"); + } + SolutionModifierAst mod = describeQueryAst.solutionModifier(); + if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) { + throw new UnsupportedOperationException( + "Solution modifiers (GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for DESCRIBE"); + } + // TODO(#390): inline VALUES needs a dedicated runtime mapping. + // TODO(#390): GROUP BY / HAVING would require aggregate-aware DESCRIBE semantics, not just field copying. + // TODO(#390): REDUCED support should be aligned with the final query-form policy for the next pipeline. + } + /** * Creates the runtime {@link Query} shell shared by every query form handled here. * @@ -192,7 +238,7 @@ private Query createQuery( DatasetClauseAst datasetClause, SolutionModifierAst solutionModifier) { Query query = Query.create(whereCompiler.compile(whereClause)); - // Collect visible nodes once so later clauses (projection, ORDER BY, GROUP BY) + // Collect visible nodes once so later clauses (projection, ORDER BY, DESCRIBE) // can resolve variables against the compiled runtime body. query.collect(); applyDataset(query, datasetClause); @@ -200,39 +246,6 @@ private Query createQuery( return query; } - /** - * Builds a KGRAM {@link Query} from a SPARQL {@code DESCRIBE} query AST. - */ - public Query toNextQuery(DescribeQueryAst describeQueryAst) { - Objects.requireNonNull(describeQueryAst, "describeQueryAst"); - rejectUnsupportedClauses(describeQueryAst); - - Query query = Query.create(whereCompiler.compile(describeQueryAst.whereClause())); - applyDataset(query, describeQueryAst.datasetClause()); - applySolutionModifier(query, describeQueryAst.solutionModifier()); - query.setDescribe(true); - - List nodes = new ArrayList<>(); - for (TermAst term : describeQueryAst.described()) { - nodes.add(toNode(term)); - } - query.setDescribeList(nodes); - return query; - } - - private static void rejectUnsupportedClauses(DescribeQueryAst describeQueryAst) { - if (!describeQueryAst.valuesClause().mappings().isEmpty()) { - throw new UnsupportedOperationException( - "Inline VALUES is not supported yet for DESCRIBE (values handling is a follow-up)"); - } - SolutionModifierAst mod = describeQueryAst.solutionModifier(); - if (mod.hasOrderBy() || mod.hasGroupBy() || mod.hasHaving() - || mod.distinct() || mod.reduced()) { - throw new UnsupportedOperationException( - "Solution modifiers (ORDER BY, GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for DESCRIBE"); - } - } - private void applyDataset(Query query, DatasetClauseAst datasetClause) { query.setFrom(toNodeList(datasetClause.graphs())); query.setNamed(toNodeList(datasetClause.namedGraphs())); @@ -268,11 +281,37 @@ private void applyProjection(Query query, ProjectionAst projection) { selectExpressions.add(Exp.create(Type.NODE, node)); } } - query.setSelectFun(selectExpressions); query.setSelect(selectNodeList(selectExpressions)); } + /** + * Resolves the resources of a {@code DESCRIBE} against the compiled body. + * + *

{@code DESCRIBE *} reuses the in-scope nodes of the body (like {@code SELECT *}). + * A described variable reuses its runtime node (so it is the one bound by the body) and + * fails fast when it is not visible; a described IRI becomes a fresh constant node.

+ */ + private List describeNodes(Query query, DescribeQueryAst describeQueryAst) { + if (describeQueryAst.isDescribeAll()) { + return query.selectNodesFromPattern(); + } + List nodes = new ArrayList<>(); + for (TermAst term : describeQueryAst.described()) { + if (term instanceof VarAst(String name)) { + Node node = query.getExtNode(name); + if (node == null) { + throw new IllegalArgumentException( + "DESCRIBE variable ?" + name + " is not visible in the compiled query body"); + } + nodes.add(node); + } else { + nodes.add(toNode(term)); + } + } + return nodes; + } + /** * Maps {@code ORDER BY} conditions that are already expressible in runtime KGRAM terms. * @@ -283,7 +322,6 @@ private void applyOrderBy(Query query, SolutionModifierAst solutionModifier) { if (!solutionModifier.hasOrderBy()) { return; } - List orderByExpressions = new ArrayList<>(); int syntheticIndex = 0; for (OrderConditionAst orderCondition : solutionModifier.orderBy()) { @@ -307,7 +345,6 @@ private Exp toOrderByExpression(Query query, OrderConditionAst orderCondition, i } return Exp.create(Type.NODE, node); } - Filter filter = SparqlAstToExpression.toNextFilter(expression); Exp exp = Exp.create(Type.NODE, createSyntheticOrderNode(syntheticIndex)); exp.setFilter(filter); @@ -342,43 +379,4 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) query.setOffset(Math.toIntExact(solutionModifier.offset())); } } - - static Node toNode(TermAst term) { - Expression expression = SparqlAstToExpression.convert(term); - if (expression instanceof Atom atom) { - return new NodeImpl(atom); - } - throw new IllegalArgumentException( - "A query term must be a variable, IRI or literal, got: " - + term.getClass().getSimpleName()); - } - - /** - * Converts a SPARQL {@code FILTER} clause by converting {@link FilterAst#operator()} the same way as - * {@link #toNextFilter(TermAst)}. - */ - public Filter toNextFilter(FilterAst filterClause) { - Objects.requireNonNull(filterClause, "filterClause"); - return toNextFilter(filterClause.operator()); - } - - /** - * Converts a filter expression carried as {@link TermAst}: must be a {@link ConstraintAst}. - */ - public Filter toNextFilter(TermAst filterExpression) { - Objects.requireNonNull(filterExpression, "filterExpression"); - if (!(filterExpression instanceof ConstraintAst constraint)) { - throw new IllegalArgumentException( - "FILTER expects a ConstraintAst, got: " + filterExpression.getClass().getName()); - } - return toNextFilter(constraint); - } - - /** - * Converts a constraint tree (boolean filter expression) into a KGRAM {@link Filter}. - */ - public Filter toNextFilter(ConstraintAst filterExpression) { - Objects.requireNonNull(filterExpression, "filterExpression"); - return SparqlAstToExpression.toNextFilter(filterExpression); - } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java index a591d62e4..1fb8c84cc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java @@ -662,7 +662,7 @@ public boolean isDistribute() { public boolean isSelect(){ - return ! (isConstruct() || isUpdate() || isInsert() || isDelete() || isAsk()); + return ! (isConstruct() || isUpdate() || isInsert() || isDelete() || isAsk() || isDescribe()); } public boolean isConstruct() { From 933ca79df7e206e5911b44e37efb92fc433bc930 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 1 Jul 2026 10:33:55 +0200 Subject: [PATCH 4/9] 390 [BRIDGE] QueryAst - Create Query from DescribeQueryAst --- .../CoreseAstQueryBuilderDescribeTest.java | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) 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 2550ae551..a8833abd3 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 @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.kgram.api.core.Node; import fr.inria.corese.core.next.query.kgram.core.Query; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,8 +23,8 @@ private static GroupGraphPatternAst whereBindingX() { } @Test - @DisplayName("DESCRIBE ?x WHERE { ... }: body compiled, DESCRIBE flag set, ?x in the describe list") - void describesVariableBoundByWhere() { + @DisplayName("DESCRIBE ?x WHERE { ... }: ?x resolves to the body node and is flagged DESCRIBE") + void describesVariableResolvedAgainstBody() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX()); @@ -32,11 +33,13 @@ void describesVariableBoundByWhere() { assertTrue(query.isDescribe(), "query flagged as DESCRIBE"); assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); assertEquals(1, query.getDescribeList().size()); - assertTrue(query.getDescribeList().getFirst().isVariable(), "described term is the variable ?x"); + Node described = query.getDescribeList().getFirst(); + assertTrue(described.isVariable()); + assertSame(query.getExtNode("x"), described, "described ?x is the runtime node bound by the body"); } @Test - @DisplayName("DESCRIBE with no WHERE: empty body, IRI in the describe list") + @DisplayName("DESCRIBE with no WHERE: empty body, IRI as a fresh constant node") void describesFixedResourceWithoutWhere() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new IriAst("")), null); @@ -51,15 +54,28 @@ void describesFixedResourceWithoutWhere() { } @Test - @DisplayName("DESCRIBE * : DESCRIBE flag set and an empty describe list (= describe all in-scope)") - void describeAllHasEmptyList() { + @DisplayName("DESCRIBE * reuses the in-scope variables of the body (like SELECT *)") + void describeAllUsesInScopeVariables() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(), whereBindingX()); Query query = builder.toNextQuery(describe); - assertTrue(query.isDescribe()); - assertTrue(query.getDescribeList().isEmpty(), "DESCRIBE * carries no explicit node"); + List described = query.getDescribeList(); + assertFalse(described.isEmpty(), "DESCRIBE * describes the in-scope variables"); + assertTrue(described.stream().allMatch(Node::isVariable)); + assertTrue(described.stream().anyMatch(n -> "x".equals(n.getLabel()))); + assertTrue(described.stream().anyMatch(n -> "p".equals(n.getLabel())), "variable predicate is in scope too"); + assertTrue(described.stream().anyMatch(n -> "o".equals(n.getLabel()))); + } + + @Test + @DisplayName("DESCRIBE ?y where ?y is not visible in the body → IllegalArgumentException") + void rejectsDescribeVariableNotVisible() { + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("y")), whereBindingX()); + + assertThrows(IllegalArgumentException.class, () -> builder.toNextQuery(describe)); } @Test @@ -91,6 +107,21 @@ void appliesLimitAndOffset() { assertEquals(2, query.getOffset(), "OFFSET applied"); } + @Test + @DisplayName("ORDER BY ?x is applied to the query") + void appliesOrderBy() { + SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy( + false, false, + List.of(new OrderConditionAst(ASTConstants.OrderDirection.ASC, new VarAst("x"))), + null, null); + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), mod); + + Query query = builder.toNextQuery(describe); + + assertFalse(query.getOrderBy().isEmpty(), "ORDER BY applied"); + } + @Test @DisplayName("Inline VALUES is not supported yet → UnsupportedOperationException") void rejectsValuesClause() { @@ -106,7 +137,7 @@ void rejectsValuesClause() { @DisplayName("Unsupported solution modifier (e.g. DISTINCT) → UnsupportedOperationException") void rejectsUnsupportedModifier() { SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy( - true, false, List.of(), null, null); // DISTINCT + true, false, List.of(), null, null); DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX(), mod); From 5cbd51e57cda3e4c95be90bb37f8c8ff2bbbd60c Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 1 Jul 2026 16:33:17 +0200 Subject: [PATCH 5/9] 390 [BRIDGE] QueryAst - Create Query from DescribeQueryAst correction sonar --- .../query/impl/sparql/bridge/AstBackedExpr.java | 6 +++--- .../sparql/bridge/NextDatatypeValueAdapter.java | 16 +++------------- .../SparqlBuiltinFunctionNameResolver.java | 4 ++-- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java index dfe2981df..c6aca95ed 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java @@ -38,7 +38,7 @@ public AstBackedExpr(Expression delegate) { public AstBackedExpr(Expression delegate, Optional sourceAst) { this.delegate = Objects.requireNonNull(delegate); - this.sourceAst = sourceAst == null ? Optional.empty() : sourceAst; + this.sourceAst = sourceAst.isEmpty() ? Optional.empty() : sourceAst; this.filterView = new NextFilterFromAst(this); } @@ -300,8 +300,8 @@ public IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Produ } private static Binding bindingFrom(BindingContext b) { - if (b instanceof BindingAdapter ba) { - return ba.delegate(); + if (b instanceof BindingAdapter(Binding delegate1)) { + return delegate1; } if (b instanceof Binding binding) { return binding; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java index 954241456..7464ff198 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java @@ -7,22 +7,12 @@ /** * Adapts a runtime {@link IDatatype} to the Corese-next {@link DatatypeValue} API. */ -public final class NextDatatypeValueAdapter implements DatatypeValue { - - private final IDatatype delegate; - - public NextDatatypeValueAdapter(IDatatype delegate) { - this.delegate = delegate; - } +public record NextDatatypeValueAdapter(IDatatype delegate) implements DatatypeValue { public static DatatypeValue ofNullable(IDatatype dt) { return dt == null ? null : new NextDatatypeValueAdapter(dt); } - public IDatatype delegate() { - return delegate; - } - @Override public String getLabel() { return delegate.getLabel(); @@ -90,8 +80,8 @@ public boolean isBlank() { } private static IDatatype unwrap(DatatypeValue other) throws CoreseDatatypeException { - if (other instanceof NextDatatypeValueAdapter a) { - return a.delegate; + if (other instanceof NextDatatypeValueAdapter(IDatatype delegate1)) { + return delegate1; } if (other instanceof IDatatype id) { return id; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java index 02d4c0725..eb46b4cfd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java @@ -18,10 +18,10 @@ private SparqlBuiltinFunctionNameResolver() { * Resolves the function name from a term that must be an {@link IriAst}. */ public static String fromFunctionTerm(TermAst functionName) { - if (!(functionName instanceof IriAst i)) { + if (!(functionName instanceof IriAst(String raw))) { throw new IllegalArgumentException("Function name must be IriAst, got " + functionName); } - return localNameFromIriToken(i.raw()); + return localNameFromIriToken(raw); } From 4185c10d1ea18b5774c187808e83d18101a00b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 3 Jul 2026 11:56:09 +0200 Subject: [PATCH 6/9] Clarify AstBackedExpr source AST contract --- .../next/query/impl/sparql/bridge/AstBackedExpr.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java index c6aca95ed..d475e3008 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java @@ -36,11 +36,11 @@ public AstBackedExpr(Expression delegate) { this(delegate, Optional.empty()); } - public AstBackedExpr(Expression delegate, Optional sourceAst) { - this.delegate = Objects.requireNonNull(delegate); - this.sourceAst = sourceAst.isEmpty() ? Optional.empty() : sourceAst; - this.filterView = new NextFilterFromAst(this); - } + public AstBackedExpr(Expression delegate, Optional sourceAst) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.sourceAst = Objects.requireNonNull(sourceAst, "sourceAst"); + this.filterView = new NextFilterFromAst(this); + } /** * The underlying SPARQL interpreter {@link Expression} (triple.parser), for metadata and {@link Filter#getFilterExpression()}. From 727ca34d0d337604b25a30089f65d4a45ce0589b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 3 Jul 2026 11:56:15 +0200 Subject: [PATCH 7/9] Cover parsed DESCRIBE query bridging --- .../CoreseAstQueryBuilderDescribeTest.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 a8833abd3..466549af0 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 @@ -1,5 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +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.Node; import fr.inria.corese.core.next.query.kgram.core.Query; @@ -22,6 +23,21 @@ private static GroupGraphPatternAst whereBindingX() { new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))))); } + @Test + @DisplayName("Parser -> DescribeQueryAst -> Query keeps the DESCRIBE form") + void parserDescribeAstToQuery() { + SparqlParser parser = new SparqlParser(); + DescribeQueryAst describe = (DescribeQueryAst) parser.parse( + "DESCRIBE ?x WHERE { ?x ?p ?o }"); + + Query query = builder.toNextQuery(describe); + + assertTrue(query.isDescribe()); + assertTrue(query.getBody().isAnd()); + assertEquals(1, query.getDescribeList().size()); + assertSame(query.getExtNode("x"), query.getDescribeList().getFirst()); + } + @Test @DisplayName("DESCRIBE ?x WHERE { ... }: ?x resolves to the body node and is flagged DESCRIBE") void describesVariableResolvedAgainstBody() { @@ -149,4 +165,4 @@ void rejectsUnsupportedModifier() { void rejectsNull() { assertThrows(NullPointerException.class, () -> builder.toNextQuery((DescribeQueryAst) null)); } -} \ No newline at end of file +} From b90dc0ed43daf484a44e814ff92a289afc682ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 3 Jul 2026 15:29:38 +0200 Subject: [PATCH 8/9] Lower DESCRIBE to construct-like KGRAM query --- .../sparql/bridge/CoreseAstQueryBuilder.java | 60 ++++++++++++++++--- .../core/next/query/kgram/core/Query.java | 26 +++----- .../CoreseAstQueryBuilderDescribeTest.java | 50 +++++++++++----- 3 files changed, 95 insertions(+), 41 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 11fff9413..ce4e43c57 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 @@ -105,11 +105,11 @@ public Query toNextQuery(SelectQueryAst selectQueryAst) { * Builds a KGRAM {@link Query} from a {@link DescribeQueryAst}. * *

Reuses the shared query shell (compiled {@code WHERE} body, dataset, LIMIT/OFFSET) and - * {@code ORDER BY} like the other forms. The described resources are resolved against the - * compiled body: a described variable reuses its runtime node (so it is bound by the body), - * a described IRI becomes a fresh constant node. {@code DESCRIBE *} reuses the in-scope nodes - * of the body, matching {@code SELECT *}. Clauses that require dedicated aggregate or values - * handling are rejected explicitly.

+ * {@code ORDER BY} like the other forms, then lowers {@code DESCRIBE} to the construct-like + * shape expected by the current KGRAM runtime. A described variable reuses its runtime node, + * a described IRI becomes a fresh constant node, and {@code DESCRIBE *} reuses the in-scope + * nodes of the body, matching {@code SELECT *}. Clauses that require dedicated aggregate or + * values handling are rejected explicitly.

*/ public Query toNextQuery(DescribeQueryAst describeQueryAst) { Objects.requireNonNull(describeQueryAst, "describeQueryAst"); @@ -120,8 +120,8 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) { describeQueryAst.datasetClause(), describeQueryAst.solutionModifier()); applyOrderBy(query, describeQueryAst.solutionModifier()); - query.setDescribe(true); - query.setDescribeList(describeNodes(query, describeQueryAst)); + List describedNodes = describeNodes(query, describeQueryAst); + lowerDescribeToConstructQuery(query, describedNodes); return query; } @@ -312,6 +312,50 @@ private List describeNodes(Query query, DescribeQueryAst describeQueryAst) return nodes; } + /** + * Lowers {@code DESCRIBE} to the construct-like shape expected by the current + * KGRAM-next runtime. + * + *

This keeps the current KGRAM contract, inherited from the historical pipeline: + * {@code DESCRIBE} is executed through the construct runtime path.

+ */ + private void lowerDescribeToConstructQuery(Query query, List describedNodes) { + Exp constructTemplate = Exp.create(Type.BGP); + int syntheticIndex = 0; + for (Node describedNode : describedNodes) { + DescribePattern describePattern = describePattern(describedNode, syntheticIndex++); + // KGRAM-next currently represents DESCRIBE with outgoing and incoming construct triples. + constructTemplate.add(describePattern.outgoing().getEdge()); + constructTemplate.add(describePattern.incoming().getEdge()); + query.getBody().add(Exp.create(Type.OPTIONAL, Exp.create(Type.AND), describePattern.optionalBody())); + } + query.setConstruct(constructTemplate); + query.setConstruct(true); + query.setConstructNodes(constructTemplate.getNodes()); + } + + private DescribePattern describePattern(Node describedNode, int index) { + Node outgoingPredicate = createSyntheticDescribeNode("p", index, 0); + Node outgoingValue = createSyntheticDescribeNode("v", index, 0); + Node incomingPredicate = createSyntheticDescribeNode("p", index, 1); + Node incomingValue = createSyntheticDescribeNode("v", index, 1); + + Exp outgoing = Exp.create(Type.EDGE, new AstBackedEdge(describedNode, outgoingPredicate, outgoingValue)); + Exp incoming = Exp.create(Type.EDGE, new AstBackedEdge(incomingValue, incomingPredicate, describedNode)); + Exp outgoingBgp = Exp.create(Type.BGP); + outgoingBgp.add(outgoing); + Exp incomingBgp = Exp.create(Type.BGP); + incomingBgp.add(incoming); + return new DescribePattern(outgoing, incoming, Exp.create(Type.UNION, outgoingBgp, incomingBgp)); + } + + private Node createSyntheticDescribeNode(String role, int describedIndex, int directionIndex) { + return new NodeImpl(Variable.create("__describe_" + role + "_" + describedIndex + "_" + directionIndex)); + } + + private record DescribePattern(Exp outgoing, Exp incoming, Exp optionalBody) { + } + /** * Maps {@code ORDER BY} conditions that are already expressible in runtime KGRAM terms. * @@ -379,4 +423,4 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) query.setOffset(Math.toIntExact(solutionModifier.offset())); } } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java index 1fb8c84cc..0b0f34078 100644 --- a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Query.java @@ -80,7 +80,6 @@ public static DQPFactory getFactory() { //selectWithExp, orderBy, groupBy; List failure, pathFilter, funList; - private List describeList = new ArrayList<>(); List errors, info; Exp having, construct, delete; @@ -142,7 +141,6 @@ public static DQPFactory getFactory() { boolean isDelete = false; boolean isUpdate = false; boolean isAsk = false; - boolean isDescribe = false; boolean isCheckLoop = false; boolean isListGroup = false; boolean isListPath = true; @@ -662,7 +660,7 @@ public boolean isDistribute() { public boolean isSelect(){ - return ! (isConstruct() || isUpdate() || isInsert() || isDelete() || isAsk() || isDescribe()); + return ! (isConstruct() || isUpdate() || isInsert() || isDelete() || isAsk()); } public boolean isConstruct() { @@ -696,21 +694,7 @@ public boolean isAsk() { public void setAsk(boolean b) { isAsk = b; } - public boolean isDescribe() { - return isDescribe; - } - - public void setDescribe(boolean b) { - isDescribe = b; - } - - public List getDescribeList() { - return describeList; - } - public void setDescribeList(List list) { - describeList = list; - } public boolean isTest() { return isTest; } @@ -839,6 +823,14 @@ public Exp getInsert() { return construct; } + public List getConstructNodes() { + return constructNodes; + } + + public void setConstructNodes(List constructNodes) { + this.constructNodes = constructNodes; + } + public void setDelete(Exp c) { delete = c; } 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 466549af0..ae72f28e4 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 @@ -3,6 +3,7 @@ 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.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; @@ -24,7 +25,7 @@ private static GroupGraphPatternAst whereBindingX() { } @Test - @DisplayName("Parser -> DescribeQueryAst -> Query keeps the DESCRIBE form") + @DisplayName("Parser -> DescribeQueryAst -> Query lowers DESCRIBE to a construct-like query") void parserDescribeAstToQuery() { SparqlParser parser = new SparqlParser(); DescribeQueryAst describe = (DescribeQueryAst) parser.parse( @@ -32,41 +33,42 @@ void parserDescribeAstToQuery() { Query query = builder.toNextQuery(describe); - assertTrue(query.isDescribe()); + assertLoweredDescribeConstruct(query, 1); assertTrue(query.getBody().isAnd()); - assertEquals(1, query.getDescribeList().size()); - assertSame(query.getExtNode("x"), query.getDescribeList().getFirst()); + assertEquals(2, query.getBody().size(), "WHERE plus generated DESCRIBE optional pattern"); + assertDescribeOptional(query.getBody().get(1)); + assertSame(query.getExtNode("x"), query.getConstruct().get(0).getEdge().getNode(0)); } @Test - @DisplayName("DESCRIBE ?x WHERE { ... }: ?x resolves to the body node and is flagged DESCRIBE") + @DisplayName("DESCRIBE ?x WHERE { ... }: ?x resolves to the body node") void describesVariableResolvedAgainstBody() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new VarAst("x")), whereBindingX()); Query query = builder.toNextQuery(describe); - assertTrue(query.isDescribe(), "query flagged as DESCRIBE"); + assertLoweredDescribeConstruct(query, 1); assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); - assertEquals(1, query.getDescribeList().size()); - Node described = query.getDescribeList().getFirst(); + assertEquals(2, query.getBody().size(), "WHERE plus generated DESCRIBE optional pattern"); + Node described = query.getConstruct().get(0).getEdge().getNode(0); assertTrue(described.isVariable()); assertSame(query.getExtNode("x"), described, "described ?x is the runtime node bound by the body"); } @Test - @DisplayName("DESCRIBE with no WHERE: empty body, IRI as a fresh constant node") + @DisplayName("DESCRIBE with no WHERE: IRI as a fresh constant node and lowered construct shape") void describesFixedResourceWithoutWhere() { DescribeQueryAst describe = new DescribeQueryAst( DatasetClauseAst.none(), List.of(new IriAst("")), null); Query query = builder.toNextQuery(describe); - assertTrue(query.isDescribe()); - assertTrue(query.getBody().isAnd(), "default empty WHERE compiles to an empty AND"); - assertEquals(0, query.getBody().size(), "no pattern in the body"); - assertEquals(1, query.getDescribeList().size()); - assertFalse(query.getDescribeList().getFirst().isVariable(), "described term is a fixed IRI"); + assertLoweredDescribeConstruct(query, 1); + assertTrue(query.getBody().isAnd(), "body remains an AND"); + assertEquals(1, query.getBody().size(), "generated DESCRIBE optional pattern"); + assertDescribeOptional(query.getBody().get(0)); + assertFalse(query.getConstruct().get(0).getEdge().getNode(0).isVariable(), "described term is a fixed IRI"); } @Test @@ -77,8 +79,11 @@ void describeAllUsesInScopeVariables() { Query query = builder.toNextQuery(describe); - List described = query.getDescribeList(); - assertFalse(described.isEmpty(), "DESCRIBE * describes the in-scope variables"); + assertLoweredDescribeConstruct(query, 3); + assertEquals(4, query.getBody().size(), "WHERE plus one generated optional pattern per described node"); + List described = query.getConstruct().getNodes().stream() + .filter(node -> "x".equals(node.getLabel()) || "p".equals(node.getLabel()) || "o".equals(node.getLabel())) + .toList(); assertTrue(described.stream().allMatch(Node::isVariable)); assertTrue(described.stream().anyMatch(n -> "x".equals(n.getLabel()))); assertTrue(described.stream().anyMatch(n -> "p".equals(n.getLabel())), "variable predicate is in scope too"); @@ -165,4 +170,17 @@ void rejectsUnsupportedModifier() { void rejectsNull() { assertThrows(NullPointerException.class, () -> builder.toNextQuery((DescribeQueryAst) null)); } + + private static void assertLoweredDescribeConstruct(Query query, int describedNodeCount) { + assertTrue(query.isConstruct(), "DESCRIBE is lowered to the construct runtime path"); + 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"); + } + + private static void assertDescribeOptional(Exp exp) { + assertTrue(exp.isOptional(), "generated DESCRIBE pattern is optional"); + assertTrue(exp.get(1).isUnion(), "generated DESCRIBE pattern matches outgoing or incoming triples"); + } } From 64cdf67b35f1e419f54fb02edb89247e762d498c Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 3 Jul 2026 16:11:12 +0200 Subject: [PATCH 9/9] test(bridge): count DESCRIBE optionals instead of asserting body size/index --- .../CoreseAstQueryBuilderDescribeTest.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) 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 ae72f28e4..1ec39ad97 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 @@ -35,8 +35,7 @@ void parserDescribeAstToQuery() { assertLoweredDescribeConstruct(query, 1); assertTrue(query.getBody().isAnd()); - assertEquals(2, query.getBody().size(), "WHERE plus generated DESCRIBE optional pattern"); - assertDescribeOptional(query.getBody().get(1)); + assertEquals(1, describeOptionalCount(query), "one DESCRIBE optional generated"); assertSame(query.getExtNode("x"), query.getConstruct().get(0).getEdge().getNode(0)); } @@ -50,7 +49,7 @@ void describesVariableResolvedAgainstBody() { assertLoweredDescribeConstruct(query, 1); assertTrue(query.getBody().isAnd(), "WHERE compiled into the body"); - assertEquals(2, query.getBody().size(), "WHERE plus generated DESCRIBE optional pattern"); + assertEquals(1, describeOptionalCount(query)); Node described = query.getConstruct().get(0).getEdge().getNode(0); assertTrue(described.isVariable()); assertSame(query.getExtNode("x"), described, "described ?x is the runtime node bound by the body"); @@ -66,8 +65,7 @@ void describesFixedResourceWithoutWhere() { assertLoweredDescribeConstruct(query, 1); assertTrue(query.getBody().isAnd(), "body remains an AND"); - assertEquals(1, query.getBody().size(), "generated DESCRIBE optional pattern"); - assertDescribeOptional(query.getBody().get(0)); + assertEquals(1, describeOptionalCount(query)); assertFalse(query.getConstruct().get(0).getEdge().getNode(0).isVariable(), "described term is a fixed IRI"); } @@ -80,7 +78,7 @@ void describeAllUsesInScopeVariables() { Query query = builder.toNextQuery(describe); assertLoweredDescribeConstruct(query, 3); - assertEquals(4, query.getBody().size(), "WHERE plus one generated optional pattern per described node"); + assertEquals(3, describeOptionalCount(query)); List described = query.getConstruct().getNodes().stream() .filter(node -> "x".equals(node.getLabel()) || "p".equals(node.getLabel()) || "o".equals(node.getLabel())) .toList(); @@ -173,14 +171,26 @@ void rejectsNull() { private static void assertLoweredDescribeConstruct(Query query, int describedNodeCount) { assertTrue(query.isConstruct(), "DESCRIBE is lowered to the construct runtime path"); + assertFalse(query.isSelect(), "a lowered DESCRIBE must not report itself as SELECT"); 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"); } - private static void assertDescribeOptional(Exp exp) { - assertTrue(exp.isOptional(), "generated DESCRIBE pattern is optional"); - assertTrue(exp.get(1).isUnion(), "generated DESCRIBE pattern matches outgoing or incoming triples"); + /** + * Counts the DESCRIBE-generated {@code OPTIONAL { outgoing UNION incoming }} patterns directly + * under the body — independently of how the parser shaped the original WHERE. + */ + private static long describeOptionalCount(Query query) { + Exp body = query.getBody(); + long count = 0; + for (int i = 0; i < body.size(); i++) { + Exp child = body.get(i); + if (child.isOptional() && child.size() > 1 && child.get(1).isUnion()) { + count++; + } + } + return count; } }