From 95842aa0bff65d50348f4e33db4eb594c5eed9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 18 Sep 2026 15:06:53 +0200 Subject: [PATCH] refactor(bridge): extract ConstructQueryCompiler and SolutionModifierCompiler --- .../sparql/bridge/ConstructQueryCompiler.java | 98 +++++ .../sparql/bridge/CoreseAstQueryBuilder.java | 383 +----------------- .../bridge/SolutionModifierCompiler.java | 328 +++++++++++++++ 3 files changed, 440 insertions(+), 369 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/ConstructQueryCompiler.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/ConstructQueryCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/ConstructQueryCompiler.java new file mode 100644 index 000000000..cb7f06cbb --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/ConstructQueryCompiler.java @@ -0,0 +1,98 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructQueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructTemplateAst; +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.TriplePatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; + +import java.util.Objects; + +/** + * Compiles a {@link ConstructQueryAst} template and modifiers into a runtime {@link Query}. + */ +final class ConstructQueryCompiler { + + private ConstructQueryCompiler() { + } + + /** + * Compiles and attaches the construct template to the runtime query shell. + * + * @param query query shell with compiled WHERE body and dataset + * @param constructQueryAst AST of the CONSTRUCT query + * @param compiler where compiler carrying the query prologue + */ + static void compile(Query query, ConstructQueryAst constructQueryAst, WhereCompiler compiler) { + Objects.requireNonNull(query, "query"); + Objects.requireNonNull(constructQueryAst, "constructQueryAst"); + Objects.requireNonNull(compiler, "compiler"); + + rejectUnsupportedConstructClauses(constructQueryAst); + Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate(), compiler); + query.setConstruct(true); + query.setConstruct(template); + query.setConstructNodes(template.getNodes()); + } + + /** + * Defensively rejects grouping and duplicate modifiers unsupported for {@code CONSTRUCT}. + * + * @param constructQueryAst query whose modifiers are checked + * @throws UnsupportedQueryFeatureException if an unsupported modifier is present + */ + private static void rejectUnsupportedConstructClauses(ConstructQueryAst constructQueryAst) { + SolutionModifierAst mod = constructQueryAst.solutionModifier(); + if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) { + throw new UnsupportedQueryFeatureException( + "GROUP BY, HAVING, DISTINCT and REDUCED are not supported yet by the next pipeline for CONSTRUCT"); + } + } + + /** + * 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 static Exp compileConstructTemplate( + Query query, ConstructTemplateAst template, WhereCompiler compiler) { + Exp bgp = Exp.create(Type.BGP); + for (TriplePatternAst triple : template.triplePatternAsts()) { + Node subject = constructNode(query, triple.subject(), compiler); + Node predicate = constructNode( + query, WhereCompiler.simplePredicate(triple.predicate()), compiler); + Node object = constructNode(query, triple.object(), compiler); + 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 static Node constructNode(Query query, TermAst term, WhereCompiler compiler) { + if (term instanceof VarAst(String name)) { + Node bound = visibleBodyNode(query, name); + return bound != null ? bound : NodeImpl.forVariable(name); + } + return compiler.termResolver().toNode(term); + } + + private static Node visibleBodyNode(Query query, String name) { + for (Node node : query.selectNodesFromPattern()) { + if (node.getLabel().equals(name)) { + return node; + } + } + return null; + } +} 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 fce2bacab..e25db4f78 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,42 +1,26 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; - import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; -import fr.inria.corese.core.next.query.impl.sparql.ast.ASTConstants; +import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; +import fr.inria.corese.core.next.query.impl.engine.model.Filter; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructQueryAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructTemplateAst; 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.GroupByAst; import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.HavingAst; 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.TriplePatternAst; 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.AndAst; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VariableScopeAnalyzer; -import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; -import fr.inria.corese.core.next.query.impl.engine.model.Filter; -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Builds KGRAM {@code Exp} / {@code Query} structures from Corese-next query AST nodes. @@ -86,7 +70,7 @@ public Query toNextQuery(AskQueryAst askQueryAst) { askQueryAst.solutionModifier(), askQueryAst.valuesClause(), compiler); - applyOrderBy(query, askQueryAst.solutionModifier(), compiler); + SolutionModifierCompiler.applyOrderBy(query, askQueryAst.solutionModifier(), compiler); query.setAsk(true); query.setAST(askQueryAst); return query; @@ -111,12 +95,12 @@ public Query toNextQuery(SelectQueryAst selectQueryAst) { selectQueryAst.solutionModifier(), selectQueryAst.valuesClause(), compiler); - applyGroupBy(query, selectQueryAst.solutionModifier(), compiler); - applyProjection(query, selectQueryAst.projection(), compiler); + SolutionModifierCompiler.applyGroupBy(query, selectQueryAst.solutionModifier(), compiler); + SolutionModifierCompiler.applyProjection(query, selectQueryAst.projection(), compiler); // Full deduplication or preserving cardinality is permitted by SELECT REDUCED. query.setDistinct(selectQueryAst.solutionModifier().distinct()); - applyOrderBy(query, selectQueryAst.solutionModifier(), compiler); - applyHaving(query, selectQueryAst.solutionModifier(), compiler); + SolutionModifierCompiler.applyOrderBy(query, selectQueryAst.solutionModifier(), compiler); + SolutionModifierCompiler.applyHaving(query, selectQueryAst.solutionModifier(), compiler); if (query.getHaving() != null && !query.hasGroupBy()) { query.setAggregate(true); } @@ -145,7 +129,7 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) { describeQueryAst.solutionModifier(), describeQueryAst.valuesClause(), compiler); - applyOrderBy(query, describeQueryAst.solutionModifier(), compiler); + SolutionModifierCompiler.applyOrderBy(query, describeQueryAst.solutionModifier(), compiler); DescribeQueryCompiler.compile(query, describeQueryAst, compiler); query.setAST(describeQueryAst); return query; @@ -163,7 +147,6 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) { */ public Query toNextQuery(ConstructQueryAst constructQueryAst) { Objects.requireNonNull(constructQueryAst, "constructQueryAst"); - rejectUnsupportedConstructClauses(constructQueryAst); WhereCompiler compiler = whereCompiler.withPrologue(constructQueryAst.prologue()); Query query = createQuery( @@ -172,11 +155,8 @@ public Query toNextQuery(ConstructQueryAst constructQueryAst) { constructQueryAst.solutionModifier(), constructQueryAst.valuesClause(), compiler); - applyOrderBy(query, constructQueryAst.solutionModifier(), compiler); - Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate(), compiler); - query.setConstruct(true); - query.setConstruct(template); - query.setConstructNodes(template.getNodes()); + SolutionModifierCompiler.applyOrderBy(query, constructQueryAst.solutionModifier(), compiler); + ConstructQueryCompiler.compile(query, constructQueryAst, compiler); query.setAST(constructQueryAst); return query; } @@ -252,7 +232,7 @@ private Query createQuery( // can resolve variables against the compiled runtime body. query.collect(); applyDataset(query, datasetClause, compiler); - applyLimitOffset(query, solutionModifier); + SolutionModifierCompiler.applyLimitOffset(query, solutionModifier); return query; } @@ -270,339 +250,4 @@ private List toNodeList(Iterable iris, WhereCompiler compiler) { } return nodes; } - - /** - * Maps the {@code SELECT} projection onto the runtime query. - * - *

{@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, WhereCompiler compiler) { - List selectExpressions = projection.selectAll() - ? toNodeExpressions(query.selectNodesFromPattern()) - : buildExplicitProjection(query, projection, compiler); - markDependentAggregates(selectExpressions); - query.setSelectFun(selectExpressions); - query.setSelect(selectNodeList(selectExpressions)); - query.setAggregate(); - } - - private List buildExplicitProjection(Query query, ProjectionAst projection, WhereCompiler compiler) { - List selectExpressions = new ArrayList<>(); - for (VarAst variable : projection.variables()) { - TermAst expression = projection.expressionTerms().get(variable.name()); - Exp exp = expression != null - ? buildProjectedExpression(variable, expression, compiler) - : buildProjectedVariable(query, variable); - selectExpressions.add(exp); - } - return selectExpressions; - } - - private Exp buildProjectedExpression(VarAst variable, TermAst expression, WhereCompiler compiler) { - Exp selected = Exp.create(Type.NODE, compiler.termResolver().toNode(variable)); - selected.setFilter(new AstBackedExpr(expression, compiler).getFilter()); - if (new VariableScopeAnalyzer().containsAggregate(expression)) { - selected.setAggregate(true); - } - return selected; - } - - private Exp buildProjectedVariable(Query query, VarAst variable) { - Node node = resolveProjectedNode(query, variable.name()); - return Exp.create(Type.NODE, node); - } - - private Node resolveProjectedNode(Query query, String name) { - Node node = visibleBodyNode(query, name); - if (node == null) { - node = groupByNode(query, name); - } - if (node == null) { - throw new IllegalArgumentException( - "Projected variable ?" + name + " is not visible in the compiled query body"); - } - return node; - } - - private void markDependentAggregates(List selectExpressions) { - boolean changed = true; - while (changed) { - changed = false; - for (Exp exp : selectExpressions) { - if (shouldMarkAsAggregate(exp, selectExpressions)) { - exp.setAggregate(true); - changed = true; - } - } - } - } - - private boolean shouldMarkAsAggregate(Exp exp, List selectExpressions) { - if (exp.isAggregate() || exp.getFilter() == null) { - return false; - } - List vars = exp.getFilter().getVariables(); - for (Exp other : selectExpressions) { - if (other.isAggregate() && vars.contains(other.getNode().getLabel())) { - return true; - } - } - return false; - } - - private Node groupByNode(Query query, String name) { - if (query.getGroupBy() == null) { - return null; - } - for (Exp exp : query.getGroupBy()) { - if (exp.getNode() != null && name.equals(exp.getNode().getLabel())) { - return exp.getNode(); - } - } - return null; - } - - /** - * Maps {@code ORDER BY} conditions that are already expressible in runtime KGRAM terms. - * - *

Variables 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, WhereCompiler compiler) { - if (!solutionModifier.hasOrderBy()) { - return; - } - List orderByExpressions = new ArrayList<>(); - int syntheticIndex = 0; - for (OrderConditionAst orderCondition : solutionModifier.orderBy()) { - Exp orderExpression = toOrderByExpression(query, orderCondition, syntheticIndex++, compiler); - orderExpression.status(orderCondition.orderDirection() == ASTConstants.OrderDirection.DESC); - orderByExpressions.add(orderExpression); - } - query.setOrderBy(orderByExpressions); - } - - private void applyGroupBy( - Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { - if (!solutionModifier.hasGroupBy()) { - return; - } - GroupByAst groupByAst = solutionModifier.groupBy(); - List groupByExpressions = new ArrayList<>(); - Set usedAliases = new HashSet<>(); - int syntheticIndex = 0; - for (TermAst term : groupByAst.expressions()) { - Exp groupExp = toGroupByExpression(query, groupByAst, term, syntheticIndex++, usedAliases, compiler); - groupByExpressions.add(groupExp); - } - query.setGroupBy(groupByExpressions); - } - - private Exp toGroupByExpression( - Query query, - GroupByAst groupByAst, - TermAst term, - int syntheticIndex, - Set usedAliases, - WhereCompiler compiler) { - String alias = findGroupByAlias(groupByAst, term, usedAliases); - if (alias != null) { - usedAliases.add(alias); - Node node = compiler.termResolver().toNode(new VarAst(alias)); - Exp exp = Exp.create(Type.NODE, node); - exp.setFilter(new AstBackedExpr(term, compiler).getFilter()); - return exp; - } - if (term instanceof VarAst(String name)) { - Node node = visibleBodyNode(query, name); - if (node == null) { - node = compiler.termResolver().toNode(term); - } - return Exp.create(Type.NODE, node); - } - Node node = createSyntheticGroupByNode(syntheticIndex); - Exp exp = Exp.create(Type.NODE, node); - exp.setFilter(new AstBackedExpr(term, compiler).getFilter()); - return exp; - } - - private String findGroupByAlias(GroupByAst groupByAst, TermAst term, Set usedAliases) { - for (Map.Entry entry : groupByAst.expressionTerms().entrySet()) { - if (!usedAliases.contains(entry.getKey()) && Objects.equals(entry.getValue(), term)) { - return entry.getKey(); - } - } - return null; - } - - private Node createSyntheticGroupByNode(int syntheticIndex) { - return NodeImpl.forVariable("__group_by_" + syntheticIndex); - } - - private void applyHaving( - Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { - if (!solutionModifier.hasHaving()) { - return; - } - HavingAst havingAst = solutionModifier.having(); - if (havingAst.isEmpty()) { - return; - } - TermAst condition = havingAst.conditions().size() == 1 - ? havingAst.conditions().getFirst() - : new AndAst(havingAst.conditions()); - Filter filter = new AstBackedExpr(condition, compiler).getFilter(); - query.setHaving(Exp.create(Type.FILTER, filter)); - } - - private Exp toOrderByExpression( - Query query, - OrderConditionAst orderCondition, - int syntheticIndex, - WhereCompiler compiler) { - TermAst expression = orderCondition.expression(); - if (expression instanceof VarAst(String name)) { - return toOrderByVarExpression(query, name); - } - return toOrderBySyntheticExpression(query, expression, syntheticIndex, compiler); - } - - private Exp toOrderByVarExpression(Query query, String name) { - Exp selectExpression = query.getSelectExp(name); - Node node = selectExpression != null ? selectExpression.getNode() : resolveOrderByFallbackNode(query, name); - if (node == null) { - throw new IllegalArgumentException( - "ORDER BY variable ?" + name + " is not visible in the compiled query"); - } - Exp orderExp = Exp.create(Type.NODE, node); - if (selectExpression != null && selectExpression.isAggregate()) { - orderExp.setAggregate(true); - } - return orderExp; - } - - private Node resolveOrderByFallbackNode(Query query, String name) { - Node groupNode = groupByNode(query, name); - return groupNode != null ? groupNode : visibleBodyNode(query, name); - } - - private Exp toOrderBySyntheticExpression( - Query query, - TermAst expression, - int syntheticIndex, - WhereCompiler compiler) { - Filter filter = new AstBackedExpr(expression, compiler).getFilter(); - Exp exp = Exp.create(Type.NODE, createSyntheticOrderNode(syntheticIndex)); - exp.setFilter(filter); - if (isOrderByAggregate(query, expression, filter)) { - exp.setAggregate(true); - } - return exp; - } - - private boolean isOrderByAggregate(Query query, TermAst expression, Filter filter) { - if (new VariableScopeAnalyzer().containsAggregate(expression)) { - return true; - } - if (filter != null) { - for (String varName : filter.getVariables()) { - Exp selExp = query.getSelectExp(varName); - if (selExp != null && selExp.isAggregate()) { - return true; - } - } - } - return false; - } - - private Node createSyntheticOrderNode(int syntheticIndex) { - return NodeImpl.forVariable("__order_by_" + syntheticIndex); - } - - private List toNodeExpressions(List nodes) { - List expressions = new ArrayList<>(); - for (Node node : nodes) { - expressions.add(Exp.create(Type.NODE, node)); - } - return expressions; - } - - private List selectNodeList(List selectExpressions) { - LinkedHashSet selectNodes = new LinkedHashSet<>(); - for (Exp selectExpression : selectExpressions) { - selectNodes.add(selectExpression.getNode()); - } - return new ArrayList<>(selectNodes); - } - - /** - * Resolves only variables visible from the outer body scope. This deliberately - * excludes nodes collected from MINUS/EXISTS bodies, which KGRAM stores as query - * nodes for internal evaluation but which are not projectable SPARQL bindings. - */ - private Node visibleBodyNode(Query query, String name) { - for (Node node : query.selectNodesFromPattern()) { - if (node.getLabel().equals(name)) { - return node; - } - } - return null; - } - - private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) { - if (solutionModifier.hasLimit()) { - query.setLimit(Math.toIntExact(solutionModifier.limit())); - } - if (solutionModifier.hasOffset()) { - query.setOffset(Math.toIntExact(solutionModifier.offset())); - } - } - - /** - * Defensively rejects grouping and duplicate modifiers unsupported for {@code CONSTRUCT}. - * - * @param constructQueryAst query whose modifiers are checked - * @throws UnsupportedQueryFeatureException if an unsupported modifier is present - */ - private static void rejectUnsupportedConstructClauses(ConstructQueryAst constructQueryAst) { - SolutionModifierAst mod = constructQueryAst.solutionModifier(); - if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) { - throw new UnsupportedQueryFeatureException( - "GROUP BY, HAVING, DISTINCT and REDUCED are not supported yet by the next pipeline for CONSTRUCT"); - } - } - - /** - * 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, WhereCompiler compiler) { - Exp bgp = Exp.create(Type.BGP); - for (TriplePatternAst triple : template.triplePatternAsts()) { - Node subject = constructNode(query, triple.subject(), compiler); - Node predicate = constructNode( - query, WhereCompiler.simplePredicate(triple.predicate()), compiler); - Node object = constructNode(query, triple.object(), compiler); - 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, WhereCompiler compiler) { - if (term instanceof VarAst(String name)) { - Node bound = visibleBodyNode(query, name); - return bound != null ? bound : NodeImpl.forVariable(name); - } - return compiler.termResolver().toNode(term); - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java new file mode 100644 index 000000000..5a1bfbfb2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java @@ -0,0 +1,328 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; +import fr.inria.corese.core.next.query.impl.engine.model.Filter; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.sparql.ast.ASTConstants; +import fr.inria.corese.core.next.query.impl.sparql.ast.GroupByAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.HavingAst; +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.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.constraint.AndAst; +import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VariableScopeAnalyzer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Compiles SPARQL solution modifiers (projection, GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET) + * onto a runtime {@link Query}. + */ +final class SolutionModifierCompiler { + + private SolutionModifierCompiler() { + } + + /** + * Maps the {@code SELECT} projection onto the runtime query. + * + *

{@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.

+ */ + static void applyProjection(Query query, ProjectionAst projection, WhereCompiler compiler) { + List selectExpressions = projection.selectAll() + ? toNodeExpressions(query.selectNodesFromPattern()) + : buildExplicitProjection(query, projection, compiler); + markDependentAggregates(selectExpressions); + query.setSelectFun(selectExpressions); + query.setSelect(selectNodeList(selectExpressions)); + query.setAggregate(); + } + + static void applyGroupBy( + Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { + if (!solutionModifier.hasGroupBy()) { + return; + } + GroupByAst groupByAst = solutionModifier.groupBy(); + List groupByExpressions = new ArrayList<>(); + Set usedAliases = new HashSet<>(); + int syntheticIndex = 0; + for (TermAst term : groupByAst.expressions()) { + Exp groupExp = toGroupByExpression(query, groupByAst, term, syntheticIndex++, usedAliases, compiler); + groupByExpressions.add(groupExp); + } + query.setGroupBy(groupByExpressions); + } + + static void applyHaving( + Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { + if (!solutionModifier.hasHaving()) { + return; + } + HavingAst havingAst = solutionModifier.having(); + if (havingAst.isEmpty()) { + return; + } + TermAst condition = havingAst.conditions().size() == 1 + ? havingAst.conditions().getFirst() + : new AndAst(havingAst.conditions()); + Filter filter = new AstBackedExpr(condition, compiler).getFilter(); + query.setHaving(Exp.create(Type.FILTER, filter)); + } + + /** + * Maps {@code ORDER BY} conditions that are already expressible in runtime KGRAM terms. + * + *

Variables reuse already-visible query nodes. Other expressions are wrapped as runtime + * filters and attached to synthetic internal nodes, just like the historical pipeline does.

+ */ + static void applyOrderBy( + Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { + if (!solutionModifier.hasOrderBy()) { + return; + } + List orderByExpressions = new ArrayList<>(); + int syntheticIndex = 0; + for (OrderConditionAst orderCondition : solutionModifier.orderBy()) { + Exp orderExpression = toOrderByExpression(query, orderCondition, syntheticIndex++, compiler); + orderExpression.status(orderCondition.orderDirection() == ASTConstants.OrderDirection.DESC); + orderByExpressions.add(orderExpression); + } + query.setOrderBy(orderByExpressions); + } + + static void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) { + if (solutionModifier.hasLimit()) { + query.setLimit(Math.toIntExact(solutionModifier.limit())); + } + if (solutionModifier.hasOffset()) { + query.setOffset(Math.toIntExact(solutionModifier.offset())); + } + } + + private static List buildExplicitProjection( + Query query, ProjectionAst projection, WhereCompiler compiler) { + List selectExpressions = new ArrayList<>(); + for (VarAst variable : projection.variables()) { + TermAst expression = projection.expressionTerms().get(variable.name()); + Exp exp = expression != null + ? buildProjectedExpression(variable, expression, compiler) + : buildProjectedVariable(query, variable); + selectExpressions.add(exp); + } + return selectExpressions; + } + + private static Exp buildProjectedExpression( + VarAst variable, TermAst expression, WhereCompiler compiler) { + Exp selected = Exp.create(Type.NODE, compiler.termResolver().toNode(variable)); + selected.setFilter(new AstBackedExpr(expression, compiler).getFilter()); + if (new VariableScopeAnalyzer().containsAggregate(expression)) { + selected.setAggregate(true); + } + return selected; + } + + private static Exp buildProjectedVariable(Query query, VarAst variable) { + Node node = resolveProjectedNode(query, variable.name()); + return Exp.create(Type.NODE, node); + } + + private static Node resolveProjectedNode(Query query, String name) { + Node node = visibleBodyNode(query, name); + if (node == null) { + node = groupByNode(query, name); + } + if (node == null) { + throw new IllegalArgumentException( + "Projected variable ?" + name + " is not visible in the compiled query body"); + } + return node; + } + + private static void markDependentAggregates(List selectExpressions) { + boolean changed = true; + while (changed) { + changed = false; + for (Exp exp : selectExpressions) { + if (shouldMarkAsAggregate(exp, selectExpressions)) { + exp.setAggregate(true); + changed = true; + } + } + } + } + + private static boolean shouldMarkAsAggregate(Exp exp, List selectExpressions) { + if (exp.isAggregate() || exp.getFilter() == null) { + return false; + } + List vars = exp.getFilter().getVariables(); + for (Exp other : selectExpressions) { + if (other.isAggregate() && vars.contains(other.getNode().getLabel())) { + return true; + } + } + return false; + } + + private static Exp toGroupByExpression( + Query query, + GroupByAst groupByAst, + TermAst term, + int syntheticIndex, + Set usedAliases, + WhereCompiler compiler) { + String alias = findGroupByAlias(groupByAst, term, usedAliases); + if (alias != null) { + usedAliases.add(alias); + Node node = compiler.termResolver().toNode(new VarAst(alias)); + Exp exp = Exp.create(Type.NODE, node); + exp.setFilter(new AstBackedExpr(term, compiler).getFilter()); + return exp; + } + if (term instanceof VarAst(String name)) { + Node node = visibleBodyNode(query, name); + if (node == null) { + node = compiler.termResolver().toNode(term); + } + return Exp.create(Type.NODE, node); + } + Node node = createSyntheticGroupByNode(syntheticIndex); + Exp exp = Exp.create(Type.NODE, node); + exp.setFilter(new AstBackedExpr(term, compiler).getFilter()); + return exp; + } + + private static String findGroupByAlias(GroupByAst groupByAst, TermAst term, Set usedAliases) { + for (Map.Entry entry : groupByAst.expressionTerms().entrySet()) { + if (!usedAliases.contains(entry.getKey()) && Objects.equals(entry.getValue(), term)) { + return entry.getKey(); + } + } + return null; + } + + private static Node createSyntheticGroupByNode(int syntheticIndex) { + return NodeImpl.forVariable("__group_by_" + syntheticIndex); + } + + private static Exp toOrderByExpression( + Query query, + OrderConditionAst orderCondition, + int syntheticIndex, + WhereCompiler compiler) { + TermAst expression = orderCondition.expression(); + if (expression instanceof VarAst(String name)) { + return toOrderByVarExpression(query, name); + } + return toOrderBySyntheticExpression(query, expression, syntheticIndex, compiler); + } + + private static Exp toOrderByVarExpression(Query query, String name) { + Exp selectExpression = query.getSelectExp(name); + Node node = selectExpression != null ? selectExpression.getNode() : resolveOrderByFallbackNode(query, name); + if (node == null) { + throw new IllegalArgumentException( + "ORDER BY variable ?" + name + " is not visible in the compiled query"); + } + Exp orderExp = Exp.create(Type.NODE, node); + if (selectExpression != null && selectExpression.isAggregate()) { + orderExp.setAggregate(true); + } + return orderExp; + } + + private static Node resolveOrderByFallbackNode(Query query, String name) { + Node groupNode = groupByNode(query, name); + return groupNode != null ? groupNode : visibleBodyNode(query, name); + } + + private static Exp toOrderBySyntheticExpression( + Query query, + TermAst expression, + int syntheticIndex, + WhereCompiler compiler) { + Filter filter = new AstBackedExpr(expression, compiler).getFilter(); + Exp exp = Exp.create(Type.NODE, createSyntheticOrderNode(syntheticIndex)); + exp.setFilter(filter); + if (isOrderByAggregate(query, expression, filter)) { + exp.setAggregate(true); + } + return exp; + } + + private static boolean isOrderByAggregate(Query query, TermAst expression, Filter filter) { + if (new VariableScopeAnalyzer().containsAggregate(expression)) { + return true; + } + if (filter != null) { + for (String varName : filter.getVariables()) { + Exp selExp = query.getSelectExp(varName); + if (selExp != null && selExp.isAggregate()) { + return true; + } + } + } + return false; + } + + private static Node createSyntheticOrderNode(int syntheticIndex) { + return NodeImpl.forVariable("__order_by_" + syntheticIndex); + } + + private static List toNodeExpressions(List nodes) { + List expressions = new ArrayList<>(); + for (Node node : nodes) { + expressions.add(Exp.create(Type.NODE, node)); + } + return expressions; + } + + private static List selectNodeList(List selectExpressions) { + LinkedHashSet selectNodes = new LinkedHashSet<>(); + for (Exp selectExpression : selectExpressions) { + selectNodes.add(selectExpression.getNode()); + } + return new ArrayList<>(selectNodes); + } + + private static Node groupByNode(Query query, String name) { + if (query.getGroupBy() == null) { + return null; + } + for (Exp exp : query.getGroupBy()) { + if (exp.getNode() != null && name.equals(exp.getNode().getLabel())) { + return exp.getNode(); + } + } + return null; + } + + /** + * Resolves only variables visible from the outer body scope. This deliberately + * excludes nodes collected from MINUS/EXISTS bodies, which KGRAM stores as query + * nodes for internal evaluation but which are not projectable SPARQL bindings. + */ + private static Node visibleBodyNode(Query query, String name) { + for (Node node : query.selectNodesFromPattern()) { + if (node.getLabel().equals(name)) { + return node; + } + } + return null; + } +}