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 716054002..3f5db08b8 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,55 +1,211 @@ 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.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; import fr.inria.corese.core.sparql.triple.parser.Expression; +import fr.inria.corese.core.sparql.triple.parser.Variable; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; /** * 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.
*/ public final class CoreseAstQueryBuilder { - private final WhereCompiler whereCompiler = new WhereCompiler(); + private final WhereCompiler whereCompiler; + + /** + * Creates a bridge backed by the default {@link WhereCompiler}. + */ + public CoreseAstQueryBuilder() { + this(new WhereCompiler()); + } + + /** + * Creates a bridge with an explicit {@link WhereCompiler}. + * + *This constructor stays package-visible so tests and same-package bridge code + * can inject a specialized compiler without widening the public API surface.
+ */ + CoreseAstQueryBuilder(WhereCompiler whereCompiler) { + this.whereCompiler = Objects.requireNonNull(whereCompiler, "whereCompiler"); + } /** * Builds a KGRAM {@link Query} from a SPARQL {@code ASK} query AST. + * + *The bridge maps the query body and the modifiers that already have a direct + * runtime representation ({@code FROM}, {@code FROM NAMED}, {@code ORDER BY}, + * {@code LIMIT}, {@code OFFSET}). Clauses that require dedicated aggregate or + * values handling are rejected explicitly.
*/ public Query toNextQuery(AskQueryAst askQueryAst) { Objects.requireNonNull(askQueryAst, "askQueryAst"); - rejectUnsupportedClauses(askQueryAst); + rejectUnsupportedAskClauses(askQueryAst); - Query query = Query.create(whereCompiler.compile(askQueryAst.whereClause())); - applyDataset(query, askQueryAst.datasetClause()); - applySolutionModifier(query, askQueryAst.solutionModifier()); + Query query = createQuery( + askQueryAst.whereClause(), + askQueryAst.datasetClause(), + askQueryAst.solutionModifier()); + applyOrderBy(query, askQueryAst.solutionModifier()); query.setAsk(true); return query; } - private static void rejectUnsupportedClauses(AskQueryAst askQueryAst) { + /** + * Builds a KGRAM {@link Query} from a {@link SelectQueryAst}. + * + *The bridge maps the compiled {@code WHERE} body, plain projection, + * dataset clauses, and solution modifiers that already have a direct runtime + * representation ({@code DISTINCT}, {@code ORDER BY}, {@code LIMIT}, {@code OFFSET}). + * Clauses that require dedicated aggregate, alias, or values handling are rejected + * explicitly.
+ */ + public Query toNextQuery(SelectQueryAst selectQueryAst) { + Objects.requireNonNull(selectQueryAst, "selectQueryAst"); + rejectUnsupportedSelectClauses(selectQueryAst); + + Query query = createQuery( + selectQueryAst.whereClause(), + selectQueryAst.datasetClause(), + selectQueryAst.solutionModifier()); + applyProjection(query, selectQueryAst.projection()); + query.setDistinct(selectQueryAst.solutionModifier().distinct()); + applyOrderBy(query, selectQueryAst.solutionModifier()); + return query; + } + + /** + * 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); + } + + /** + * Converts a query term used as subject, predicate, object, or variable reference + * into a runtime {@link Node}. + * + *This helper is package-visible because both the query builder and the + * {@link WhereCompiler} need a single shared term-to-node conversion rule.
+ */ + 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()); + } + + private static void rejectUnsupportedAskClauses(AskQueryAst askQueryAst) { if (!askQueryAst.valuesClause().mappings().isEmpty()) { throw new UnsupportedOperationException( "Inline VALUES is not supported yet for ASK (values handling is a follow-up)"); } SolutionModifierAst mod = askQueryAst.solutionModifier(); - if (mod.hasOrderBy() || mod.hasGroupBy() || mod.hasHaving() - || mod.distinct() || mod.reduced()) { + if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) { throw new UnsupportedOperationException( - "Solution modifiers (ORDER BY, GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for ASK"); + "Solution modifiers (GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for ASK"); } + // TODO(#388): inline VALUES needs a dedicated runtime mapping. + // TODO(#388): GROUP BY / HAVING would require aggregate-aware ASK semantics, not just field copying. + // TODO(#388): REDUCED support should be aligned with the final query-form policy for the next pipeline. + } + + 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"); + } + if (solutionModifier.hasGroupBy()) { + throw new UnsupportedOperationException("GROUP BY is not supported yet when building a next Query"); + } + if (solutionModifier.hasHaving()) { + throw new UnsupportedOperationException("HAVING is not supported yet when building a next Query"); + } + // TODO(#387): inline VALUES needs a dedicated runtime mapping. + // TODO(#387): SELECT expressions need a clear runtime story for aliases and reuse in later clauses. + // TODO(#387): GROUP BY / HAVING require aggregate semantics, not only AST field propagation. + // TODO(#387): REDUCED support should be aligned with the final next-pipeline query-form policy. + } + + /** + * Creates the runtime {@link Query} shell shared by every query form handled here. + * + *The compiled {@code WHERE} body comes first, then the builder copies dataset + * and limit/offset information, and finally collects visible nodes so later clauses + * can resolve variables against the runtime body.
+ */ + private Query createQuery( + GroupGraphPatternAst whereClause, + DatasetClauseAst datasetClause, + SolutionModifierAst solutionModifier) { + Query query = Query.create(whereCompiler.compile(whereClause)); + // Collect visible nodes once so later clauses (projection, ORDER BY, GROUP BY) + // can resolve variables against the compiled runtime body. + query.collect(); + applyDataset(query, datasetClause); + applyLimitOffset(query, solutionModifier); + return query; } private void applyDataset(Query query, DatasetClauseAst datasetClause) { @@ -65,52 +221,100 @@ private List{@code SELECT *} reuses the visible nodes collected from the compiled + * query body. An explicit projection reuses these same runtime nodes and fails + * fast when a projected variable is not visible in the body.
+ */ + private void applyProjection(Query query, ProjectionAst projection) { + ListVariables reuse already-visible query nodes. Other expressions are wrapped as runtime + * filters and attached to synthetic internal nodes, just like the historical pipeline does.
+ */ + private void applyOrderBy(Query query, SolutionModifierAst solutionModifier) { + if (!solutionModifier.hasOrderBy()) { + return; + } + + ListThis is the main entry point of the compiler. It expects the root + * {@link GroupGraphPatternAst} produced by the parser for a complete + * {@code WHERE { ... }} block.
+ */ public Exp compile(GroupGraphPatternAst where) { Objects.requireNonNull(where, "where"); return compileGroup(where); } /** - * Dispatch a single {@link PatternAst} element to its KGRAM {@link Exp}. + * Compiles a single {@link PatternAst} node. + * + *This package-visible dispatcher is used inside the bridge while recursively + * traversing a {@code WHERE} tree, and by same-package tests that exercise one + * pattern kind in isolation.
*/ - public Exp compile(PatternAst pattern) { + Exp compile(PatternAst pattern) { Objects.requireNonNull(pattern, "pattern"); return switch (pattern) { case BgpAst bgp -> compileBgp(bgp); @@ -41,7 +62,6 @@ public Exp compile(PatternAst pattern) { }; } - /** * {@code { e1 e2 ... en }} becomes an {@code AND} of the compiled elements. */ @@ -69,7 +89,6 @@ case MinusAst(GroupGraphPatternAst pattern) -> { return body; } - private Exp compileBgp(BgpAst bgp) { Exp pattern = Exp.create(Type.BGP); for (TriplePatternAst triple : bgp.triples()) { @@ -85,20 +104,17 @@ private Edge toEdge(TriplePatternAst triple) { return new AstBackedEdge(subject, predicate, object); } - private Exp compileFilter(FilterAst filter) { Filter nextFilter = SparqlAstToExpression.toNextFilter(filter); return Exp.create(Type.FILTER, nextFilter); } - private Exp compileUnion(UnionAst union) { Exp left = compile(union.left()); Exp right = compile(union.right()); return Exp.create(Type.UNION, left, right); } - /** * Compiles a bare {@link OptionalAst}, i.e. one that is not folded with a * preceding pattern by {@link #compileGroup(GroupGraphPatternAst)} (for 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 3e99eec9f..b4e3a3f1a 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 @@ -2,7 +2,18 @@ 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.impl.sparql.ast.AskQueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst; +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.LiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.SolutionModifierAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.ValueMappingAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.ValuesAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; import fr.inria.corese.core.next.query.kgram.api.core.Node; import fr.inria.corese.core.next.query.kgram.core.Query; @@ -13,7 +24,12 @@ import java.util.Map; import java.util.Set; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class CoreseAstQueryBuilderAskTest extends AbstractSparqlParserFeatureTest { @@ -109,7 +125,7 @@ void rejectsValuesClause() { @Test @DisplayName("toNextQuery(null) throws NullPointerException") void rejectsNullAsk() { - assertThrows(NullPointerException.class, () -> builder.toNextQuery(null)); + assertThrows(NullPointerException.class, () -> builder.toNextQuery((AskQueryAst) null)); } @Test @@ -139,16 +155,16 @@ void appliesLimitAndOffset() { } @Test - @DisplayName("ORDER BY stays out of scope for this first ASK increment") - void rejectsOrderBy() { + @DisplayName("ORDER BY is mapped onto the runtime ASK query when it is already directly supported") + void appliesOrderBy() { AskQueryAst ask = assertInstanceOf(AskQueryAst.class, newParserDefault().parse("ASK WHERE { ?s ?p ?o . } ORDER BY ?s")); - UnsupportedOperationException error = - assertThrows(UnsupportedOperationException.class, () -> builder.toNextQuery(ask)); + Query query = builder.toNextQuery(ask); - assertEquals("Solution modifiers (ORDER BY, GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for ASK", - error.getMessage()); + assertEquals(1, query.getOrderBy().size()); + assertEquals("s", query.getOrderBy().getFirst().getNode().getLabel()); + assertFalse(query.getOrderBy().getFirst().status()); } private static List