From 8a3699ab4a13af6a3cce5a1331f27de9c653e046 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 10 Jul 2026 15:55:28 +0200 Subject: [PATCH 1/4] #453 [BRIDGE] Add EXISTS/NOT EXISTS to filter conversion --- .../sparql/bridge/AstBackedExistTerm.java | 47 +++++++ .../impl/sparql/bridge/AstBackedExpr.java | 13 +- .../sparql/bridge/CoreseAstQueryBuilder.java | 4 +- .../sparql/bridge/SparqlAstToExpression.java | 81 +++++++++--- .../impl/sparql/bridge/WhereCompiler.java | 4 +- .../bridge/CoreseAstQueryBuilderAskTest.java | 49 ++++++++ .../CoreseAstQueryBuilderConstructTest.java | 97 +++++++++++++++ .../bridge/CoreseAstQueryBuilderTest.java | 56 ++++++++- .../bridge/WhereCompilerExistsTest.java | 116 ++++++++++++++++++ 9 files changed, 442 insertions(+), 25 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerExistsTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java new file mode 100644 index 000000000..d64fe7c73 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java @@ -0,0 +1,47 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.sparql.triple.parser.ASTBuffer; +import fr.inria.corese.core.sparql.triple.parser.Term; + +final class AstBackedExistTerm extends Term { + + private final transient GroupGraphPatternAst patternAst; + private transient Exp compiledPattern; + + AstBackedExistTerm(GroupGraphPatternAst patternAst) { + super("exists"); + setOper(ExprType.EXIST); + this.patternAst = patternAst; + } + + GroupGraphPatternAst patternAst() { + return patternAst; + } + + void setCompiledPattern(Exp compiledPattern) { + this.compiledPattern = compiledPattern; + } + + Exp compiledPattern() { + return compiledPattern; + } + + /** + * The legacy {@code Term} reports an existence test through {@code getExist() != null}, but this + * bridge carries the pattern as a next-KGRAM {@link Exp} instead. Reporting it here keeps + * {@code isRecExist()} true, which the engine relies on to place FILTER EXISTS correctly + * (QuerySorter, in-scope filters of OPTIONAL/MINUS). + */ + @Override + public boolean isTermExist() { + return true; + } + + @Override + public ASTBuffer toString(ASTBuffer sb) { + return sb.append("exists {...}"); + } +} \ No newline at end of file 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 d475e3008..f64fcf667 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, "delegate"); - this.sourceAst = Objects.requireNonNull(sourceAst, "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()}. @@ -70,6 +70,9 @@ public Filter getFilter() { @Override public Object getPattern() { + if (delegate instanceof AstBackedExistTerm exist) { + return exist.compiledPattern(); + } return delegate.getPattern(); } 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 3029681f1..04ea7270e 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 @@ -168,7 +168,7 @@ public Filter toNextFilter(TermAst filterExpression) { */ public Filter toNextFilter(ConstraintAst filterExpression) { Objects.requireNonNull(filterExpression, "filterExpression"); - return SparqlAstToExpression.toNextFilter(filterExpression); + return SparqlAstToExpression.toNextFilter(filterExpression, whereCompiler); } /** @@ -416,7 +416,7 @@ private Exp toOrderByExpression(Query query, OrderConditionAst orderCondition, i } return Exp.create(Type.NODE, node); } - Filter filter = SparqlAstToExpression.toNextFilter(expression); + Filter filter = SparqlAstToExpression.toNextFilter(expression, whereCompiler); Exp exp = Exp.create(Type.NODE, createSyntheticOrderNode(syntheticIndex)); exp.setFilter(filter); return exp; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java index f47dcc51f..30a8bfe55 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java @@ -1,8 +1,8 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; -import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; -import fr.inria.corese.core.next.query.impl.sparql.ast.*; +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; import fr.inria.corese.core.next.query.kgram.api.core.Filter; import fr.inria.corese.core.next.util.StringUtils; @@ -49,6 +49,15 @@ public static Filter toNextFilter(FilterAst filterClause) { return toNextFilter(filterClause.operator()); } + /** + * Converts a {@code FILTER} clause, compiling the graph pattern of any embedded + * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. + */ + public static Filter toNextFilter(FilterAst filterClause, WhereCompiler whereCompiler) { + Objects.requireNonNull(filterClause, "filterClause"); + return toNextFilter(filterClause.operator(), whereCompiler); + } + /** * Converts a filter {@link TermAst} to an {@link Expression}, then wraps it as a * {@link Filter} with {@link Filter#coreseNextSource()} set to {@code filterExpression}. @@ -57,12 +66,42 @@ public static Filter toNextFilter(FilterAst filterClause) { * method is the shared implementation. For a full {@link FilterAst} node, use {@link #toNextFilter(FilterAst)}. */ public static Filter toNextFilter(TermAst filterExpression) { + return toNextFilter(filterExpression, null); + } + + /** + * Converts a filter {@link TermAst}, then compiles the graph pattern of every embedded + * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. + * + */ + public static Filter toNextFilter(TermAst filterExpression, WhereCompiler whereCompiler) { Expression exprTree = convert(filterExpression); + compileExists(exprTree, whereCompiler); initializeExpList(exprTree); AstBackedExpr expr = new AstBackedExpr(exprTree, Optional.of(filterExpression)); return expr.getFilter(); } + /** + * Second pass: compiles the graph pattern carried by every {@link AstBackedExistTerm} of the + * expression tree. Mirrors {@code compileExist} of the historical pipeline. + */ + private static void compileExists(Expression expression, WhereCompiler whereCompiler) { + if (expression instanceof AstBackedExistTerm exist) { + if (whereCompiler == null) { + throw new UnsupportedOperationException( + "EXISTS / NOT EXISTS conversion requires a WhereCompiler to compile its graph pattern"); + } + exist.setCompiledPattern(whereCompiler.compile(exist.patternAst())); + return; + } + if (expression instanceof Term term) { + for (Expression arg : term.getArgs()) { + compileExists(arg, whereCompiler); + } + } + } + /** * Initializes the {@code Expr} list ({@code lExp}) of every {@link Term} in the tree. */ @@ -212,18 +251,30 @@ private static Expression constraintToExpression(ConstraintAst constraint) { case Sha512Ast sha512Ast -> functionTerm("sha512", convert(sha512Ast.argument())); case ExistsAst existsAst -> - throw new UnsupportedQueryFeatureException( - "EXISTS filters are not supported yet by the next pipeline"); - case NotExistsAst notExistsAst -> - throw new UnsupportedQueryFeatureException( - "NOT EXISTS filters are not supported yet by the next pipeline"); - default -> - throw new UnsupportedQueryFeatureException( - "Filter expression is not supported yet by the next pipeline: " - + constraint.getClass().getSimpleName()); + new AstBackedExistTerm(existsAst.pattern()); + case NotExistsAst notExistsAst -> + notTerm(new AstBackedExistTerm(notExistsAst.pattern())); + default -> + throw new UnsupportedQueryFeatureException( + "Filter expression is not supported yet by the next pipeline: " + + constraint.getClass().getSimpleName()); }; } + /** + * {@code NOT EXISTS} is the boolean negation of {@code EXISTS}. + * + *

The interpreter resolves operator codes in a later compilation phase + * ({@code Processor.type(Term, ASTQuery)}) that this bridge never runs, so {@code oper()} is set + * explicitly here — just like {@link AstBackedExistTerm} does for {@code EXIST}. The engine reads + * {@code oper()} when collecting and indexing existence tests.

+ */ + private static Term notTerm(Term existTerm) { + Term not = Term.create("!", existTerm); + not.setOper(fr.inria.corese.core.next.query.kgram.api.core.ExprType.NOT); + return not; + } + private static Term functionTerm(String name, Expression arg) { Term t = Term.function(name); t.add(arg); @@ -304,4 +355,4 @@ private static final class KeywordHolder { static final String SEAND = fr.inria.corese.core.sparql.triple.cst.Keyword.SEAND; static final String SEOR = fr.inria.corese.core.sparql.triple.cst.Keyword.SEOR; } -} +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java index 9d304e573..26ed429b2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java @@ -107,7 +107,7 @@ private Edge toEdge(TriplePatternAst triple) { } private Exp compileFilter(FilterAst filter) { - Filter nextFilter = SparqlAstToExpression.toNextFilter(filter); + Filter nextFilter = SparqlAstToExpression.toNextFilter(filter, this); return Exp.create(Type.FILTER, nextFilter); } @@ -143,7 +143,7 @@ private Exp compileMinus(MinusAst minus) { * Compiles {@code BIND(expression AS ?var)} into a KGRAM {@link Exp}. */ private Exp compileBind(BindAst bind) { - Filter filter = SparqlAstToExpression.toNextFilter(bind.expression()); + Filter filter = SparqlAstToExpression.toNextFilter(bind.expression(), this); Node variable = CoreseAstQueryBuilder.toNode(bind.variable()); Exp exp = Exp.create(Type.BIND); exp.setFilter(filter); 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 7e838d10f..7568b97f3 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 @@ -15,8 +15,12 @@ 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.ExistsAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; 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; @@ -168,6 +172,51 @@ void appliesOrderBy() { assertFalse(query.getOrderBy().getFirst().status()); } + @Test + @DisplayName("ASK WHERE { FILTER EXISTS { ... } } compiles the EXISTS pattern") + void filterExistsInAskWhere() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))))); + FilterAst filter = new FilterAst(new ExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))), + filter)); + AskQueryAst ask = new AskQueryAst(DatasetClauseAst.none(), where); + + Query query = builder.toNextQuery(ask); + + assertTrue(query.isAsk()); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + assertEquals(ExprType.EXIST, filterExp.getFilter().getExp().oper(), "operator is EXIST"); + assertInstanceOf(Exp.class, filterExp.getFilter().getExp().getPattern(), "EXISTS pattern is compiled"); + } + + @Test + @DisplayName("Parser -> ASK WHERE { FILTER NOT EXISTS } produces NOT wrapping EXIST") + void parsedAskWithFilterNotExists() { + AskQueryAst ask = assertInstanceOf(AskQueryAst.class, + newParserDefault().parse("ASK WHERE { ?s ?p ?o . FILTER NOT EXISTS { ?s ?t } }")); + + Query query = builder.toNextQuery(ask); + + assertTrue(query.isAsk()); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER NOT EXISTS compiled into body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + assertEquals(ExprType.EXIST, notExpr.getExpList().getFirst().oper(), "inner expression is EXIST"); + } + + private static Exp findFilter(Exp body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i).isFilter()) { + return body.get(i); + } + } + return null; + } + private static List labels(List nodes) { return nodes.stream().map(Node::getLabel).toList(); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java index 41eb7e2ff..ba1474fa8 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderConstructTest.java @@ -4,7 +4,11 @@ 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.constraint.ExistsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NotExistsAst; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; import fr.inria.corese.core.next.query.kgram.api.core.Edge; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; 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; @@ -170,4 +174,97 @@ void rejectsValuesClause() { void rejectsNull() { assertThrows(NullPointerException.class, () -> builder.toNextQuery((ConstructQueryAst) null)); } + + + @Test + @DisplayName("CONSTRUCT WHERE { ... FILTER EXISTS { ... } } compiles the EXISTS pattern") + void filterExistsInConstructWhere() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))))); + FilterAst filter = new FilterAst(new ExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))), + filter)); + ConstructQueryAst construct = new ConstructQueryAst( + template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), + where); + + Query query = builder.toNextQuery(construct); + + assertTrue(query.isConstruct(), "query is CONSTRUCT"); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + Expr expr = filterExp.getFilter().getExp(); + assertEquals(ExprType.EXIST, expr.oper(), "FILTER operator is EXIST"); + assertInstanceOf(Exp.class, expr.getPattern(), "EXISTS pattern is compiled"); + } + + @Test + @DisplayName("CONSTRUCT WHERE { ... FILTER NOT EXISTS { ... } } compiles as NOT wrapping EXIST") + void filterNotExistsInConstructWhere() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))))); + FilterAst filter = new FilterAst(new NotExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))), + filter)); + ConstructQueryAst construct = new ConstructQueryAst( + template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), + where); + + Query query = builder.toNextQuery(construct); + + assertTrue(query.isConstruct(), "query is CONSTRUCT"); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + Expr exist = notExpr.getExpList().getFirst(); + assertEquals(ExprType.EXIST, exist.oper(), "inner expression is EXIST"); + assertInstanceOf(Exp.class, exist.getPattern(), "NOT EXISTS pattern is compiled"); + } + + @Test + @DisplayName("Parser -> CONSTRUCT WHERE { FILTER EXISTS } produces compiled EXISTS pattern") + void parsedConstructWithFilterExists() { + SparqlParser parser = newParserDefault(); + ConstructQueryAst construct = assertInstanceOf(ConstructQueryAst.class, + parser.parse("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o . FILTER EXISTS { ?x ?t } }")); + + Query query = builder.toNextQuery(construct); + + assertTrue(query.isConstruct()); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER EXISTS compiled into body"); + assertEquals(ExprType.EXIST, filterExp.getFilter().getExp().oper(), "operator is EXIST"); + } + + @Test + @DisplayName("Parser -> CONSTRUCT WHERE { FILTER NOT EXISTS } produces NOT wrapping EXIST") + void parsedConstructWithFilterNotExists() { + SparqlParser parser = newParserDefault(); + ConstructQueryAst construct = assertInstanceOf(ConstructQueryAst.class, + parser.parse("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o . FILTER NOT EXISTS { ?x ?t } }")); + + Query query = builder.toNextQuery(construct); + + assertTrue(query.isConstruct()); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER NOT EXISTS compiled into body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + assertEquals(ExprType.EXIST, notExpr.getExpList().getFirst().oper(), "inner expression is EXIST"); + } + + /** + * Finds the first FILTER {@link Exp} in a compiled AND body, or {@code null} if none. + */ + private static Exp findFilter(Exp body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i).isFilter()) { + return body.get(i); + } + } + return null; + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java index a493c21f5..8cfc87079 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java @@ -3,6 +3,7 @@ import fr.inria.corese.core.next.query.impl.parser.AbstractSparqlParserFeatureTest; 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.MinusAst; @@ -15,6 +16,9 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; import fr.inria.corese.core.next.query.impl.sparql.ast.ASTConstants; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ExistsAst; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; 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; @@ -145,7 +149,7 @@ void appliesVariableOrderBy() { assertEquals(1, query.getOrderBy().size()); assertEquals("s", query.getOrderBy().getFirst().getNode().getLabel()); - assertTrue(!query.getOrderBy().getFirst().status(), "ASC should keep the default ascending status"); + assertFalse(query.getOrderBy().getFirst().status(), "ASC should keep the default ascending status"); } @Test @@ -258,6 +262,56 @@ void rejectsUnknownOrderByVariable() { assertEquals("ORDER BY variable ?missing is not visible in the compiled query", error.getMessage()); } + @Test + @DisplayName("SELECT WHERE { FILTER EXISTS { ... } } compiles the EXISTS pattern") + void filterExistsInSelectWhere() { + SelectQueryAst select = getSelectQueryAst(); + + Query query = builder.toNextQuery(select); + + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + assertEquals(ExprType.EXIST, filterExp.getFilter().getExp().oper(), "operator is EXIST"); + assertInstanceOf(Exp.class, filterExp.getFilter().getExp().getPattern(), "EXISTS pattern is compiled"); + } + + private static SelectQueryAst getSelectQueryAst() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))))); + FilterAst filter = new FilterAst(new ExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))), + filter)); + SelectQueryAst select = new SelectQueryAst(where); + return select; + } + + @Test + @DisplayName("Parser -> SELECT WHERE { FILTER NOT EXISTS } produces NOT wrapping EXIST") + void parsedSelectWithFilterNotExists() { + QueryAst ast = newParserDefault().parse( + "SELECT * WHERE { ?s ?p ?o . FILTER NOT EXISTS { ?s ?t } }"); + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast); + + Query query = builder.toNextQuery(select); + + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER NOT EXISTS compiled into body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + assertEquals(ExprType.EXIST, notExpr.getExpList().getFirst().oper(), "inner expression is EXIST"); + assertInstanceOf(Exp.class, notExpr.getExpList().getFirst().getPattern(), "NOT EXISTS pattern is compiled"); + } + + private static Exp findFilter(Exp body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i).isFilter()) { + return body.get(i); + } + } + return null; + } + private static TriplePatternAst singleTriple() { return new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerExistsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerExistsTest.java new file mode 100644 index 000000000..52c3b18ff --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerExistsTest.java @@ -0,0 +1,116 @@ +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.impl.sparql.ast.constraint.AndAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ExistsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NotExistsAst; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.core.Query; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class WhereCompilerExistsTest { + + private final WhereCompiler compiler = new WhereCompiler(); + + private static GroupGraphPatternAst group(PatternAst... elements) { + return new GroupGraphPatternAst(List.of(elements)); + } + + private static BgpAst bgp(String s, String p, String o) { + return new BgpAst(List.of( + new TriplePatternAst(new VarAst(s), new VarAst(p), new VarAst(o)))); + } + + /** + * Returns the FILTER's inner expression (the AstBackedExpr) from a compiled body. + */ + private Expr filterExpr(Exp body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i).isFilter()) { + return body.get(i).getFilter().getExp(); + } + } + throw new AssertionError("no FILTER in body"); + } + + @Test + @DisplayName("FILTER EXISTS { ?s ?p ?o } -> Expr with oper EXIST carrying the compiled pattern") + void compilesExists() { + FilterAst filter = new FilterAst(new ExistsAst(group(bgp("s", "p", "o")))); + + Exp body = compiler.compile(group(bgp("a", "b", "c"), filter)); + + Expr exist = filterExpr(body); + assertEquals(ExprType.EXIST, exist.oper(), "operator is EXIST"); + + Object pattern = exist.getPattern(); + assertNotNull(pattern, "EXISTS pattern is attached"); + assertInstanceOf(Exp.class, pattern, "pattern is a runtime Exp"); + Exp patternExp = (Exp) pattern; + assertTrue(patternExp.isAnd() || patternExp.isBGP(), "pattern is the compiled graph pattern"); + } + + @Test + @DisplayName("FILTER NOT EXISTS { ?s ?p ?o } -> negation wrapping an EXIST expression") + void compilesNotExists() { + FilterAst filter = new FilterAst(new NotExistsAst(group(bgp("s", "p", "o")))); + + Exp body = compiler.compile(group(bgp("a", "b", "c"), filter)); + + Expr notExpr = filterExpr(body); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + assertEquals(1, notExpr.getExpList().size(), "NOT wraps a single sub-expression"); + + Expr exist = notExpr.getExpList().getFirst(); + assertEquals(ExprType.EXIST, exist.oper(), "the wrapped expression is EXIST"); + assertInstanceOf(Exp.class, exist.getPattern(), "EXIST carries the compiled pattern"); + } + + @Test + @DisplayName("EXISTS nested in a compound filter: FILTER(EXISTS { ... } && ?o > 5)") + void compilesExistsNestedInCompoundFilter() { + ExistsAst exists = new ExistsAst(group(bgp("s", "p", "o"))); + // EXISTS { ?s ?p ?o } && (?o > 5) + AndAst and = new AndAst(List.of( + exists, + new GreaterThanAst(List.of(new VarAst("o"), new LiteralAst("5", null, null))))); + FilterAst filter = new FilterAst(and); + + Exp body = compiler.compile(group(bgp("a", "b", "o"), filter)); + + Expr root = filterExpr(body); + // one of the two AND operands must be the EXIST with a compiled pattern + boolean foundExist = false; + for (Expr operand : root.getExpList()) { + if (operand.oper() == ExprType.EXIST) { + foundExist = true; + assertInstanceOf(Exp.class, operand.getPattern(), "nested EXISTS keeps its compiled pattern"); + } + } + assertTrue(foundExist, "EXISTS is preserved when nested inside a compound filter"); + } + + @Test + @DisplayName("{ ?s ?p ?o FILTER EXISTS { ?s ?q ?z } } : ?z is collected as a query node (exists scope)") + void existsVariablesAreCollectedInExistsScope() { + FilterAst filter = new FilterAst(new ExistsAst(group(bgp("s", "q", "z")))); + Exp body = compiler.compile(group(bgp("s", "p", "o"), filter)); + + Query query = Query.create(body); + query.collect(); + + assertTrue(query.getPatternNodes().stream().anyMatch(n -> "o".equals(n.getLabel()))); + assertFalse(query.getPatternNodes().stream().anyMatch(n -> "z".equals(n.getLabel())), + "?z lives inside EXISTS and must not leak into the outer pattern scope"); + assertTrue(query.getQueryNodes().stream().anyMatch(n -> "z".equals(n.getLabel())), + "?z is collected through collectExist -> getPattern()"); + } +} \ No newline at end of file From 1eab5b54e76f411e9abec8755e0ded8f7f7b33af Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 16 Jul 2026 12:14:19 +0200 Subject: [PATCH 2/4] #453 [BRIDGE] Add EXISTS/NOT EXISTS to filter conversion correction apres rebase --- .../impl/sparql/bridge/SparqlAstToExpressionTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java index bbb853039..529b5cc77 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java @@ -1,11 +1,11 @@ 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.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.VarAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ExistsAst; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; import fr.inria.corese.core.sparql.triple.parser.Constant; import fr.inria.corese.core.sparql.triple.parser.Expression; import fr.inria.corese.core.sparql.triple.parser.Variable; @@ -48,13 +48,12 @@ void varAstToExpression() { } @Test - void existsFilterFailsWithUnsupportedQueryFeatureException() { + void existsFilterConvertsToExistTerm() { ExistsAst exists = new ExistsAst(new GroupGraphPatternAst(List.of())); - UnsupportedQueryFeatureException error = assertThrows( - UnsupportedQueryFeatureException.class, - () -> SparqlAstToExpression.convert(exists)); + Expression term = SparqlAstToExpression.convert(exists); - assertTrue(error.getMessage().contains("EXISTS filters")); + assertNotNull(term); + assertEquals(ExprType.EXIST, term.oper()); } } From 48c979d33e77c0e10a3f774d0e012cc88c63f64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 16 Jul 2026 16:22:24 +0200 Subject: [PATCH 3/4] Polish EXISTS bridge behavior --- .../sparql/bridge/AstBackedExistTerm.java | 15 +- .../impl/sparql/bridge/AstBackedExpr.java | 628 +++++++-------- .../sparql/bridge/SparqlAstToExpression.java | 722 +++++++++--------- .../bridge/SparqlAstToExpressionTest.java | 17 +- 4 files changed, 704 insertions(+), 678 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java index d64fe7c73..e3de8528d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java @@ -6,15 +6,17 @@ import fr.inria.corese.core.sparql.triple.parser.ASTBuffer; import fr.inria.corese.core.sparql.triple.parser.Term; +import java.util.Objects; + final class AstBackedExistTerm extends Term { - private final transient GroupGraphPatternAst patternAst; - private transient Exp compiledPattern; + private final GroupGraphPatternAst patternAst; + private Exp compiledPattern; AstBackedExistTerm(GroupGraphPatternAst patternAst) { super("exists"); setOper(ExprType.EXIST); - this.patternAst = patternAst; + this.patternAst = Objects.requireNonNull(patternAst, "patternAst"); } GroupGraphPatternAst patternAst() { @@ -40,8 +42,13 @@ public boolean isTermExist() { return true; } + @Override + public boolean isExist() { + return true; + } + @Override public ASTBuffer toString(ASTBuffer sb) { return sb.append("exists {...}"); } -} \ No newline at end of file +} 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 f64fcf667..708473f6a 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 @@ -1,314 +1,314 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; -import fr.inria.corese.core.next.query.kgram.api.core.BindingContext; -import fr.inria.corese.core.next.query.kgram.api.core.DatatypeValue; -import fr.inria.corese.core.next.query.kgram.api.core.Expr; -import fr.inria.corese.core.next.query.kgram.api.core.Filter; -import fr.inria.corese.core.next.query.kgram.adapter.BindingAdapter; -import fr.inria.corese.core.next.query.kgram.adapter.TripleParserEvalSupport; -import fr.inria.corese.core.next.query.kgram.api.query.Environment; -import fr.inria.corese.core.next.query.kgram.api.query.Evaluator; -import fr.inria.corese.core.next.query.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.Computer; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.function.term.Binding; -import fr.inria.corese.core.sparql.triple.parser.Expression; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Wraps a {@link fr.inria.corese.core.sparql.triple.parser.Expression} (SPARQL interpreter tree) as a - * {@link fr.inria.corese.core.next.query.kgram.api.core.Expr}. - * - */ -public final class AstBackedExpr implements Expr { - - private final Expression delegate; - private final Optional sourceAst; - private final NextFilterFromAst filterView; - - public AstBackedExpr(Expression delegate) { - this(delegate, Optional.empty()); - } - - 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()}. - */ - public Expression asTripleParserExpression() { - return delegate; - } - - public Optional sourceAst() { - return sourceAst; - } - - private static Expr wrapInterpreterSubexpr(Object node) { - if (node == null) { - return null; - } - if (node instanceof Expression ex) { - return new AstBackedExpr(ex); - } - throw new IllegalArgumentException("Cannot wrap as AstBackedExpr: " + node); - } - - @Override - public Filter getFilter() { - return filterView; - } - - @Override - public Object getPattern() { - if (delegate instanceof AstBackedExistTerm exist) { - return exist.compiledPattern(); - } - return delegate.getPattern(); - } - - @Override - public boolean isSystem() { - return delegate.isSystem(); - } - - @Override - public boolean isPublic() { - return delegate.isPublic(); - } - - @Override - public void setPublic(boolean b) { - delegate.setPublic(b); - } - - @Override - public boolean isDynamic() { - return delegate.isDynamic(); - } - - @Override - public boolean isTrace() { - return delegate.isTrace(); - } - - @Override - public boolean isDebug() { - return delegate.isDebug(); - } - - @Override - public String getLabel() { - return delegate.getLabel(); - } - - @Override - public String getModality() { - return delegate.getModality(); - } - - @Override - public List getExpList() { - List in = delegate.getExpList(); - List out = new ArrayList<>(in.size()); - for (Object o : in) { - out.add(wrapInterpreterSubexpr(o)); - } - return out; - } - - @Override - public Expr getExp(int i) { - return wrapInterpreterSubexpr(delegate.getExp(i)); - } - - @Override - public void setExp(int i, Expr e) { - if (e instanceof AstBackedExpr ab) { - delegate.setExp(i, ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } - } - - @Override - public Expr getArg() { - return wrapInterpreterSubexpr(delegate.getArg()); - } - - @Override - public void setArg(Expr exp) { - if (exp instanceof AstBackedExpr ab) { - delegate.setArg(ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } - } - - @Override - public DatatypeValue getValue() { - return NextDatatypeValueAdapter.ofNullable(delegate.getValue()); - } - - @Override - public DatatypeValue getDatatypeValue() { - return NextDatatypeValueAdapter.ofNullable(delegate.getDatatypeValue()); - } - - @Override - public int type() { - return delegate.type(); - } - - @Override - public int subtype() { - return delegate.subtype(); - } - - @Override - public void setSubtype(int n) { - delegate.setSubtype(n); - } - - @Override - public int oper() { - return delegate.oper(); - } - - @Override - public boolean match(int oper) { - return delegate.match(oper); - } - - @Override - public void setOper(int n) { - delegate.setOper(n); - } - - @Override - public boolean isAggregate() { - return delegate.isAggregate(); - } - - @Override - public boolean isRecAggregate() { - return delegate.isRecAggregate(); - } - - @Override - public boolean isExist() { - return delegate.isExist(); - } - - @Override - public boolean isRecExist() { - return delegate.isRecExist(); - } - - @Override - public boolean isVariable() { - return delegate.isVariable(); - } - - @Override - public boolean isConstant() { - return delegate.isConstant(); - } - - @Override - public boolean isFuncall() { - return delegate.isFuncall(); - } - - @Override - public boolean isBound() { - return delegate.isBound(); - } - - @Override - public boolean isDistinct() { - return delegate.isDistinct(); - } - - @Override - public int arity() { - return delegate.arity(); - } - - @Override - public int getIndex() { - return delegate.getIndex(); - } - - @Override - public void setIndex(int index) { - delegate.setIndex(index); - } - - @Override - public Expr getDefine() { - return wrapInterpreterSubexpr(delegate.getDefine()); - } - - @Override - public void setDefine(Expr exp) { - if (exp instanceof AstBackedExpr ab) { - delegate.setDefine(ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } - } - - @Override - public Expr getFunction() { - return wrapInterpreterSubexpr(delegate.getFunction()); - } - - @Override - public Expr getBody() { - return wrapInterpreterSubexpr(delegate.getBody()); - } - - @Override - public Expr getVariable() { - return wrapInterpreterSubexpr(delegate.getVariable()); - } - - @Override - public Expr getDefinition() { - return wrapInterpreterSubexpr(delegate.getDefinition()); - } - - @Override - public boolean hasMetadata(String name) { - return delegate.hasMetadata(name); - } - - @Override - public IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { - if (!(eval instanceof Computer computer)) { - throw new QueryEvaluationException("Evaluator must implement Computer for triple.parser Expression evaluation"); - } - Binding binding = bindingFrom(b); - return TripleParserEvalSupport.evalWE(delegate, computer, binding, env, p); - } - - private static Binding bindingFrom(BindingContext b) { - if (b instanceof BindingAdapter(Binding delegate1)) { - return delegate1; - } - if (b instanceof Binding binding) { - return binding; - } - throw new QueryEvaluationException("BindingContext must be BindingAdapter or Binding"); - } -} +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.kgram.api.core.BindingContext; +import fr.inria.corese.core.next.query.kgram.api.core.DatatypeValue; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.kgram.api.core.Filter; +import fr.inria.corese.core.next.query.kgram.adapter.BindingAdapter; +import fr.inria.corese.core.next.query.kgram.adapter.TripleParserEvalSupport; +import fr.inria.corese.core.next.query.kgram.api.query.Environment; +import fr.inria.corese.core.next.query.kgram.api.query.Evaluator; +import fr.inria.corese.core.next.query.kgram.api.query.Producer; +import fr.inria.corese.core.sparql.api.Computer; +import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.sparql.triple.function.term.Binding; +import fr.inria.corese.core.sparql.triple.parser.Expression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Wraps a {@link fr.inria.corese.core.sparql.triple.parser.Expression} (SPARQL interpreter tree) as a + * {@link fr.inria.corese.core.next.query.kgram.api.core.Expr}. + * + */ +public final class AstBackedExpr implements Expr { + + private final Expression delegate; + private final Optional sourceAst; + private final NextFilterFromAst filterView; + + public AstBackedExpr(Expression delegate) { + this(delegate, Optional.empty()); + } + + 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()}. + */ + public Expression asTripleParserExpression() { + return delegate; + } + + public Optional sourceAst() { + return sourceAst; + } + + private static Expr wrapInterpreterSubexpr(Object node) { + if (node == null) { + return null; + } + if (node instanceof Expression ex) { + return new AstBackedExpr(ex); + } + throw new IllegalArgumentException("Cannot wrap as AstBackedExpr: " + node); + } + + @Override + public Filter getFilter() { + return filterView; + } + + @Override + public Object getPattern() { + if (delegate instanceof AstBackedExistTerm exist) { + return exist.compiledPattern(); + } + return delegate.getPattern(); + } + + @Override + public boolean isSystem() { + return delegate.isSystem(); + } + + @Override + public boolean isPublic() { + return delegate.isPublic(); + } + + @Override + public void setPublic(boolean b) { + delegate.setPublic(b); + } + + @Override + public boolean isDynamic() { + return delegate.isDynamic(); + } + + @Override + public boolean isTrace() { + return delegate.isTrace(); + } + + @Override + public boolean isDebug() { + return delegate.isDebug(); + } + + @Override + public String getLabel() { + return delegate.getLabel(); + } + + @Override + public String getModality() { + return delegate.getModality(); + } + + @Override + public List getExpList() { + List in = delegate.getExpList(); + List out = new ArrayList<>(in.size()); + for (Object o : in) { + out.add(wrapInterpreterSubexpr(o)); + } + return out; + } + + @Override + public Expr getExp(int i) { + return wrapInterpreterSubexpr(delegate.getExp(i)); + } + + @Override + public void setExp(int i, Expr e) { + if (e instanceof AstBackedExpr ab) { + delegate.setExp(i, ab.delegate); + } else { + throw new IllegalArgumentException("Expr must be AstBackedExpr"); + } + } + + @Override + public Expr getArg() { + return wrapInterpreterSubexpr(delegate.getArg()); + } + + @Override + public void setArg(Expr exp) { + if (exp instanceof AstBackedExpr ab) { + delegate.setArg(ab.delegate); + } else { + throw new IllegalArgumentException("Expr must be AstBackedExpr"); + } + } + + @Override + public DatatypeValue getValue() { + return NextDatatypeValueAdapter.ofNullable(delegate.getValue()); + } + + @Override + public DatatypeValue getDatatypeValue() { + return NextDatatypeValueAdapter.ofNullable(delegate.getDatatypeValue()); + } + + @Override + public int type() { + return delegate.type(); + } + + @Override + public int subtype() { + return delegate.subtype(); + } + + @Override + public void setSubtype(int n) { + delegate.setSubtype(n); + } + + @Override + public int oper() { + return delegate.oper(); + } + + @Override + public boolean match(int oper) { + return delegate.match(oper); + } + + @Override + public void setOper(int n) { + delegate.setOper(n); + } + + @Override + public boolean isAggregate() { + return delegate.isAggregate(); + } + + @Override + public boolean isRecAggregate() { + return delegate.isRecAggregate(); + } + + @Override + public boolean isExist() { + return delegate.isExist(); + } + + @Override + public boolean isRecExist() { + return delegate.isRecExist(); + } + + @Override + public boolean isVariable() { + return delegate.isVariable(); + } + + @Override + public boolean isConstant() { + return delegate.isConstant(); + } + + @Override + public boolean isFuncall() { + return delegate.isFuncall(); + } + + @Override + public boolean isBound() { + return delegate.isBound(); + } + + @Override + public boolean isDistinct() { + return delegate.isDistinct(); + } + + @Override + public int arity() { + return delegate.arity(); + } + + @Override + public int getIndex() { + return delegate.getIndex(); + } + + @Override + public void setIndex(int index) { + delegate.setIndex(index); + } + + @Override + public Expr getDefine() { + return wrapInterpreterSubexpr(delegate.getDefine()); + } + + @Override + public void setDefine(Expr exp) { + if (exp instanceof AstBackedExpr ab) { + delegate.setDefine(ab.delegate); + } else { + throw new IllegalArgumentException("Expr must be AstBackedExpr"); + } + } + + @Override + public Expr getFunction() { + return wrapInterpreterSubexpr(delegate.getFunction()); + } + + @Override + public Expr getBody() { + return wrapInterpreterSubexpr(delegate.getBody()); + } + + @Override + public Expr getVariable() { + return wrapInterpreterSubexpr(delegate.getVariable()); + } + + @Override + public Expr getDefinition() { + return wrapInterpreterSubexpr(delegate.getDefinition()); + } + + @Override + public boolean hasMetadata(String name) { + return delegate.hasMetadata(name); + } + + @Override + public IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { + if (!(eval instanceof Computer computer)) { + throw new QueryEvaluationException("Evaluator must implement Computer for triple.parser Expression evaluation"); + } + Binding binding = bindingFrom(b); + return TripleParserEvalSupport.evalWE(delegate, computer, binding, env, p); + } + + private static Binding bindingFrom(BindingContext b) { + if (b instanceof BindingAdapter(Binding delegate1)) { + return delegate1; + } + if (b instanceof Binding binding) { + return binding; + } + throw new QueryEvaluationException("BindingContext must be BindingAdapter or Binding"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java index 30a8bfe55..a51cf72e7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java @@ -1,358 +1,364 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; -import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; -import fr.inria.corese.core.next.query.impl.sparql.ast.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; -import fr.inria.corese.core.next.query.kgram.api.core.Filter; -import fr.inria.corese.core.next.util.StringUtils; -import fr.inria.corese.core.sparql.datatype.RDF; -import fr.inria.corese.core.sparql.triple.parser.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Converts Corese-next {@link TermAst} nodes (including {@link ConstraintAst}) into - * {@link Expression} trees for the SPARQL interpreter, consumable from KGRAM “next” via - * {@link fr.inria.corese.core.next.query.kgram.api.core.Filter} / {@link AstBackedExpr}. - * - */ -public final class SparqlAstToExpression { - - private SparqlAstToExpression() { - } - - /** - * Converts any {@link TermAst} (variable, literal, IRI, or constraint expression) to {@link Expression}. - */ - public static Expression convert(TermAst term) { - return switch (term) { - case VarAst v -> Variable.create(v.name()); - case LiteralAst l -> literalToConstant(l); - case IriAst i -> iriToConstant(i); - case ConstraintAst c -> constraintToExpression(c); - default -> throw new IllegalStateException("Unhandled TermAst: " + term.getClass()); - }; - } - - /** - * Converts the operator of a SPARQL {@code FILTER} clause ({@link FilterAst}) the same way as - * {@link #toNextFilter(TermAst)}. - * - *

Prefer {@link CoreseAstQueryBuilder#toNextFilter(FilterAst)} at call sites that build queries. - */ - public static Filter toNextFilter(FilterAst filterClause) { - Objects.requireNonNull(filterClause, "filterClause"); - return toNextFilter(filterClause.operator()); - } - - /** - * Converts a {@code FILTER} clause, compiling the graph pattern of any embedded - * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. - */ - public static Filter toNextFilter(FilterAst filterClause, WhereCompiler whereCompiler) { - Objects.requireNonNull(filterClause, "filterClause"); - return toNextFilter(filterClause.operator(), whereCompiler); - } - - /** - * Converts a filter {@link TermAst} to an {@link Expression}, then wraps it as a - * {@link Filter} with {@link Filter#coreseNextSource()} set to {@code filterExpression}. - * - *

Prefer {@link CoreseAstQueryBuilder#toNextFilter(TermAst)} at call sites that build queries; this - * method is the shared implementation. For a full {@link FilterAst} node, use {@link #toNextFilter(FilterAst)}. - */ - public static Filter toNextFilter(TermAst filterExpression) { - return toNextFilter(filterExpression, null); - } - - /** - * Converts a filter {@link TermAst}, then compiles the graph pattern of every embedded - * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. - * - */ - public static Filter toNextFilter(TermAst filterExpression, WhereCompiler whereCompiler) { - Expression exprTree = convert(filterExpression); - compileExists(exprTree, whereCompiler); - initializeExpList(exprTree); - AstBackedExpr expr = new AstBackedExpr(exprTree, Optional.of(filterExpression)); - return expr.getFilter(); - } - - /** - * Second pass: compiles the graph pattern carried by every {@link AstBackedExistTerm} of the - * expression tree. Mirrors {@code compileExist} of the historical pipeline. - */ - private static void compileExists(Expression expression, WhereCompiler whereCompiler) { - if (expression instanceof AstBackedExistTerm exist) { - if (whereCompiler == null) { - throw new UnsupportedOperationException( - "EXISTS / NOT EXISTS conversion requires a WhereCompiler to compile its graph pattern"); - } - exist.setCompiledPattern(whereCompiler.compile(exist.patternAst())); - return; - } - if (expression instanceof Term term) { - for (Expression arg : term.getArgs()) { - compileExists(arg, whereCompiler); - } - } - } - - /** - * Initializes the {@code Expr} list ({@code lExp}) of every {@link Term} in the tree. - */ - private static void initializeExpList(Expression expression) { - if (expression instanceof Term term) { - for (Expression arg : term.getArgs()) { - initializeExpList(arg); - } - term.setExpList(new ArrayList<>(term.getArgs())); - } - } - - private static Constant literalToConstant(LiteralAst l) { - if (l.lang() != null && !l.lang().isEmpty()) { - return Constant.create(unquoteLexical(l.lexical()), RDF.rdflangString, l.lang()); - } - if (l.datatype() != null && !l.datatype().isEmpty()) { - return Constant.create(unquoteLexical(l.lexical()), normalizeDatatypeIri(l.datatype()), null); - } - return Constant.createString(unquoteLexical(l.lexical())); - } - - private static String unquoteLexical(String lexical) { - if (lexical.length() >= 2 && lexical.startsWith("\"")) { - if (lexical.endsWith("\"")) { - return lexical.substring(1, lexical.length() - 1); - } - int langIdx = lexical.lastIndexOf('"'); - if (langIdx > 0) { - return lexical.substring(1, langIdx); - } - } - return lexical; - } - - private static String normalizeDatatypeIri(String dt) { - String d = StringUtils.trimChevronIRIs(dt); - return fr.inria.corese.core.sparql.triple.parser.NSManager.nsm().toNamespace(d); - } - - private static Constant iriToConstant(IriAst i) { - String raw = StringUtils.trimChevronIRIs(i.raw()); - if (raw.startsWith(IOConstants.BLANK_NODE_PREFIX)) { - return Constant.createBlank(raw.substring(IOConstants.BLANK_NODE_PREFIX.length())); - } - return Constant.createResource(raw); - } - - private static Expression constraintToExpression(ConstraintAst constraint) { - return switch (constraint) { - case AndAst andAst -> - Term.create(KeywordHolder.SEAND, convert(andAst.getLeftArgument()), convert(andAst.getRightArgument())); - case OrAst orAst -> - Term.create(KeywordHolder.SEOR, convert(orAst.getLeftArgument()), convert(orAst.getRightArgument())); - case EqualsAst equalsAst -> - Term.create("=", convert(equalsAst.getLeftArgument()), convert(equalsAst.getRightArgument())); - case DifferentAst differentAst -> - Term.create("!=", convert(differentAst.getLeftArgument()), convert(differentAst.getRightArgument())); - case LowerThanAst lowerThanAst -> - Term.create("<", convert(lowerThanAst.getLeftArgument()), convert(lowerThanAst.getRightArgument())); - case LowerOrEqualThanAst lowerOrEqualThanAst -> - Term.create("<=", convert(lowerOrEqualThanAst.getLeftArgument()), convert(lowerOrEqualThanAst.getRightArgument())); - case GreaterThanAst greaterThanAst -> - Term.create(">", convert(greaterThanAst.getLeftArgument()), convert(greaterThanAst.getRightArgument())); - case GreaterOrEqualThanAst greaterOrEqualThanAst -> - Term.create(">=", convert(greaterOrEqualThanAst.getLeftArgument()), convert(greaterOrEqualThanAst.getRightArgument())); - case AddAst addAst -> - Term.create("+", convert(addAst.getLeftArgument()), convert(addAst.getRightArgument())); - case SubtractAst subtractAst -> - Term.create("-", convert(subtractAst.getLeftArgument()), convert(subtractAst.getRightArgument())); - case MultiplyAst multiplyAst -> - Term.create("*", convert(multiplyAst.getLeftArgument()), convert(multiplyAst.getRightArgument())); - case DivideAst divideAst -> - Term.create("/", convert(divideAst.getLeftArgument()), convert(divideAst.getRightArgument())); - case UnaryPlusAst unaryPlusAst -> - Term.create("+", convert(unaryPlusAst.argument())); - case UnaryMinusAst unaryMinusAst -> - Term.create("-", convert(unaryMinusAst.argument())); - case BooleanNotAst booleanNotAst -> - Term.create("!", convert(booleanNotAst.argument())); - case BoundAst boundAst -> - functionTerm(Processor.BOUND, convert(boundAst.argument())); - case IsIriAst isIriAst -> - functionTerm("isIRI", convert(isIriAst.argument())); - case IsBlankAst isBlankAst -> - functionTerm("isBlank", convert(isBlankAst.argument())); - case IsLiteralAst isLiteralAst -> - functionTerm("isLiteral", convert(isLiteralAst.argument())); - case StrAst strAst -> - functionTerm("str", convert(strAst.argument())); - case LangAst langAst -> - functionTerm("lang", convert(langAst.argument())); - case DatatypeAst datatypeAst -> - functionTerm("datatype", convert(datatypeAst.argument())); - case SameTermAst sameTermAst -> - functionTerm("sameTerm", convert(sameTermAst.getLeftArgument()), convert(sameTermAst.getRightArgument())); - case LangMatchesAst langMatchesAst -> - functionTerm("langMatches", convert(langMatchesAst.getLeftArgument()), convert(langMatchesAst.getRightArgument())); - case BinaryRegexAst binaryRegexAst -> - regexTerm(convert(binaryRegexAst.getString()), convert(binaryRegexAst.getPattern())); - case TrinaryRegexAst trinaryRegexAst -> - regexTerm(convert(trinaryRegexAst.getString()), convert(trinaryRegexAst.getPattern()), convert(trinaryRegexAst.getFlags())); - case FunctionCallAst callAst -> - functionCallAst(callAst); - case ConcatAst concatAst -> - variadicTerm("concat", concatAst.arguments()); - case CoalesceAst coalesceAst -> - variadicTerm(Processor.COALESCE, coalesceAst.arguments()); - case IfAst ifAst -> - Term.function(Processor.IF, convert(ifAst.condition()), convert(ifAst.thenExpr()), convert(ifAst.elseExpr())); - case ReplaceAst replaceAst -> - replaceToTerm(replaceAst); - case SubstrAst substrAst -> - substrToTerm(substrAst); - case BnodeAst bnodeAst -> - bnodeToTerm(bnodeAst); - case StrStartsAst strStartsAst -> - functionTerm("strstarts", convert(strStartsAst.getLeftArgument()), convert(strStartsAst.getRightArgument())); - case StrEndsAst strEndsAst -> - functionTerm("strends", convert(strEndsAst.getLeftArgument()), convert(strEndsAst.getRightArgument())); - case ContainsAst containsAst -> - functionTerm("contains", convert(containsAst.getLeftArgument()), convert(containsAst.getRightArgument())); - case StrBeforeAst strBeforeAst -> - functionTerm("strbefore", convert(strBeforeAst.getLeftArgument()), convert(strBeforeAst.getRightArgument())); - case StrAfterAst strAfterAst -> - functionTerm("strafter", convert(strAfterAst.getLeftArgument()), convert(strAfterAst.getRightArgument())); - case StrLangAst strLangAst -> - functionTerm("strlang", convert(strLangAst.getLeftArgument()), convert(strLangAst.getRightArgument())); - case StrDtAst strDtAst -> - functionTerm(Processor.STRDT, convert(strDtAst.getLeftArgument()), convert(strDtAst.getRightArgument())); - case IriFunctionAst iriFunctionAst -> - functionTerm("iri", convert(iriFunctionAst.argument())); - case LcaseAst lcaseAst -> - functionTerm("lcase", convert(lcaseAst.argument())); - case UcaseAst ucaseAst -> - functionTerm("ucase", convert(ucaseAst.argument())); - case EncodeForUriAst encodeForUriAst -> - functionTerm("encode_for_uri", convert(encodeForUriAst.argument())); - case Md5Ast md5Ast -> - functionTerm("md5", convert(md5Ast.argument())); - case Sha1Ast sha1Ast -> - functionTerm("sha1", convert(sha1Ast.argument())); - case Sha256Ast sha256Ast -> - functionTerm("sha256", convert(sha256Ast.argument())); - case Sha384Ast sha384Ast -> - functionTerm("sha384", convert(sha384Ast.argument())); - case Sha512Ast sha512Ast -> - functionTerm("sha512", convert(sha512Ast.argument())); - case ExistsAst existsAst -> - new AstBackedExistTerm(existsAst.pattern()); - case NotExistsAst notExistsAst -> - notTerm(new AstBackedExistTerm(notExistsAst.pattern())); - default -> - throw new UnsupportedQueryFeatureException( - "Filter expression is not supported yet by the next pipeline: " - + constraint.getClass().getSimpleName()); - }; - } - - /** - * {@code NOT EXISTS} is the boolean negation of {@code EXISTS}. - * - *

The interpreter resolves operator codes in a later compilation phase - * ({@code Processor.type(Term, ASTQuery)}) that this bridge never runs, so {@code oper()} is set - * explicitly here — just like {@link AstBackedExistTerm} does for {@code EXIST}. The engine reads - * {@code oper()} when collecting and indexing existence tests.

- */ - private static Term notTerm(Term existTerm) { - Term not = Term.create("!", existTerm); - not.setOper(fr.inria.corese.core.next.query.kgram.api.core.ExprType.NOT); - return not; - } - - private static Term functionTerm(String name, Expression arg) { - Term t = Term.function(name); - t.add(arg); - return t; - } - - private static Term functionTerm(String name, Expression a1, Expression a2) { - Term t = Term.function(name); - t.add(a1); - t.add(a2); - return t; - } - - private static Term regexTerm(Expression s, Expression pattern) { - Term t = Term.function("regex"); - t.add(s); - t.add(pattern); - return t; - } - - private static Term regexTerm(Expression s, Expression pattern, Expression flags) { - Term t = Term.function("regex"); - t.add(s); - t.add(pattern); - t.add(flags); - return t; - } - - private static Term functionCallAst(FunctionCallAst f) { - String name = SparqlBuiltinFunctionNameResolver.fromFunctionTerm(f.functionName()); - Term t = Term.function(name); - for (TermAst arg : f.arguments()) { - t.add(convert(arg)); - } - return t; - } - - private static Term variadicTerm(String name, List args) { - Term t = Term.function(name); - for (TermAst arg : args) { - t.add(convert(arg)); - } - return t; - } - - private static Term replaceToTerm(ReplaceAst r) { - Term t = - Term.function( - "replace", - convert(r.getString()), - convert(r.getPattern()), - convert(r.getReplacement())); - if (r.hasFlags()) { - t.add(convert(r.getFlags())); - } - return t; - } - - private static Term substrToTerm(SubstrAst s) { - if (s.getLength() != null) { - return Term.function( - "substr", - convert(s.getString()), - convert(s.getStart()), - convert(s.getLength())); - } - return Term.function("substr", convert(s.getString()), convert(s.getStart())); - } - - private static Term bnodeToTerm(BnodeAst b) { - if (b.getLabel() == null) { - return Term.function(Processor.BNODE); - } - return Term.function(Processor.BNODE, convert(b.getLabel())); - } - - private static final class KeywordHolder { - static final String SEAND = fr.inria.corese.core.sparql.triple.cst.Keyword.SEAND; - static final String SEOR = fr.inria.corese.core.sparql.triple.cst.Keyword.SEOR; - } -} \ No newline at end of file +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import fr.inria.corese.core.next.query.kgram.api.core.Filter; +import fr.inria.corese.core.next.util.StringUtils; +import fr.inria.corese.core.sparql.datatype.RDF; +import fr.inria.corese.core.sparql.triple.parser.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Converts Corese-next {@link TermAst} nodes (including {@link ConstraintAst}) into + * {@link Expression} trees for the SPARQL interpreter, consumable from KGRAM “next” via + * {@link fr.inria.corese.core.next.query.kgram.api.core.Filter} / {@link AstBackedExpr}. + * + */ +public final class SparqlAstToExpression { + + private SparqlAstToExpression() { + } + + /** + * Converts any {@link TermAst} (variable, literal, IRI, or constraint expression) to {@link Expression}. + */ + public static Expression convert(TermAst term) { + return switch (term) { + case VarAst(String name) -> Variable.create(name); + case LiteralAst(String lexical, String lang, String datatype) -> + literalToConstant(lexical, lang, datatype); + case IriAst(String raw) -> iriToConstant(raw); + case ConstraintAst c -> constraintToExpression(c); + default -> throw new IllegalStateException("Unhandled TermAst: " + term.getClass()); + }; + } + + /** + * Converts the operator of a SPARQL {@code FILTER} clause ({@link FilterAst}) the same way as + * {@link #toNextFilter(TermAst)}. + * + *

Prefer {@link CoreseAstQueryBuilder#toNextFilter(FilterAst)} at call sites that build queries. + * A filter containing {@code EXISTS} / {@code NOT EXISTS} requires + * {@link #toNextFilter(FilterAst, WhereCompiler)} so its graph pattern can be compiled. + */ + public static Filter toNextFilter(FilterAst filterClause) { + Objects.requireNonNull(filterClause, "filterClause"); + return toNextFilter(filterClause.operator()); + } + + /** + * Converts a {@code FILTER} clause, compiling the graph pattern of any embedded + * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. + */ + public static Filter toNextFilter(FilterAst filterClause, WhereCompiler whereCompiler) { + Objects.requireNonNull(filterClause, "filterClause"); + return toNextFilter(filterClause.operator(), whereCompiler); + } + + /** + * Converts a filter {@link TermAst} to an {@link Expression}, then wraps it as a + * {@link Filter} with {@link Filter#coreseNextSource()} set to {@code filterExpression}. + * + *

Prefer {@link CoreseAstQueryBuilder#toNextFilter(TermAst)} at call sites that build queries; this + * method is the shared implementation. For a full {@link FilterAst} node, use {@link #toNextFilter(FilterAst)}. + * Filters containing {@code EXISTS} / {@code NOT EXISTS} require + * {@link #toNextFilter(TermAst, WhereCompiler)} so their graph pattern can be compiled. + */ + public static Filter toNextFilter(TermAst filterExpression) { + return toNextFilter(filterExpression, null); + } + + /** + * Converts a filter {@link TermAst}, then compiles the graph pattern of every embedded + * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. + * + */ + public static Filter toNextFilter(TermAst filterExpression, WhereCompiler whereCompiler) { + Expression exprTree = convert(filterExpression); + compileExists(exprTree, whereCompiler); + initializeExpList(exprTree); + AstBackedExpr expr = new AstBackedExpr(exprTree, Optional.of(filterExpression)); + return expr.getFilter(); + } + + /** + * Second pass: compiles the graph pattern carried by every {@link AstBackedExistTerm} of the + * expression tree. Mirrors {@code compileExist} of the historical pipeline. + */ + private static void compileExists(Expression expression, WhereCompiler whereCompiler) { + if (expression instanceof AstBackedExistTerm exist) { + if (whereCompiler == null) { + throw new IllegalArgumentException( + "EXISTS / NOT EXISTS filter conversion requires a WhereCompiler " + + "to compile its graph pattern"); + } + exist.setCompiledPattern(whereCompiler.compile(exist.patternAst())); + return; + } + if (expression instanceof Term term) { + for (Expression arg : term.getArgs()) { + compileExists(arg, whereCompiler); + } + } + } + + /** + * Initializes the {@code Expr} list ({@code lExp}) of every {@link Term} in the tree. + */ + private static void initializeExpList(Expression expression) { + if (expression instanceof Term term) { + for (Expression arg : term.getArgs()) { + initializeExpList(arg); + } + term.setExpList(new ArrayList<>(term.getArgs())); + } + } + + private static Constant literalToConstant(String lexical, String lang, String datatype) { + if (lang != null && !lang.isEmpty()) { + return Constant.create(unquoteLexical(lexical), RDF.rdflangString, lang); + } + if (datatype != null && !datatype.isEmpty()) { + return Constant.create(unquoteLexical(lexical), normalizeDatatypeIri(datatype), null); + } + return Constant.createString(unquoteLexical(lexical)); + } + + private static String unquoteLexical(String lexical) { + if (lexical.length() >= 2 && lexical.startsWith("\"")) { + if (lexical.endsWith("\"")) { + return lexical.substring(1, lexical.length() - 1); + } + int langIdx = lexical.lastIndexOf('"'); + if (langIdx > 0) { + return lexical.substring(1, langIdx); + } + } + return lexical; + } + + private static String normalizeDatatypeIri(String dt) { + String d = StringUtils.trimChevronIRIs(dt); + return fr.inria.corese.core.sparql.triple.parser.NSManager.nsm().toNamespace(d); + } + + private static Constant iriToConstant(String rawIri) { + String raw = StringUtils.trimChevronIRIs(rawIri); + if (raw.startsWith(IOConstants.BLANK_NODE_PREFIX)) { + return Constant.createBlank(raw.substring(IOConstants.BLANK_NODE_PREFIX.length())); + } + return Constant.createResource(raw); + } + + private static Expression constraintToExpression(ConstraintAst constraint) { + return switch (constraint) { + case AndAst andAst -> + Term.create(KeywordHolder.SEAND, convert(andAst.getLeftArgument()), convert(andAst.getRightArgument())); + case OrAst orAst -> + Term.create(KeywordHolder.SEOR, convert(orAst.getLeftArgument()), convert(orAst.getRightArgument())); + case EqualsAst equalsAst -> + Term.create("=", convert(equalsAst.getLeftArgument()), convert(equalsAst.getRightArgument())); + case DifferentAst differentAst -> + Term.create("!=", convert(differentAst.getLeftArgument()), convert(differentAst.getRightArgument())); + case LowerThanAst lowerThanAst -> + Term.create("<", convert(lowerThanAst.getLeftArgument()), convert(lowerThanAst.getRightArgument())); + case LowerOrEqualThanAst lowerOrEqualThanAst -> + Term.create("<=", convert(lowerOrEqualThanAst.getLeftArgument()), convert(lowerOrEqualThanAst.getRightArgument())); + case GreaterThanAst greaterThanAst -> + Term.create(">", convert(greaterThanAst.getLeftArgument()), convert(greaterThanAst.getRightArgument())); + case GreaterOrEqualThanAst greaterOrEqualThanAst -> + Term.create(">=", convert(greaterOrEqualThanAst.getLeftArgument()), convert(greaterOrEqualThanAst.getRightArgument())); + case AddAst addAst -> + Term.create("+", convert(addAst.getLeftArgument()), convert(addAst.getRightArgument())); + case SubtractAst subtractAst -> + Term.create("-", convert(subtractAst.getLeftArgument()), convert(subtractAst.getRightArgument())); + case MultiplyAst multiplyAst -> + Term.create("*", convert(multiplyAst.getLeftArgument()), convert(multiplyAst.getRightArgument())); + case DivideAst divideAst -> + Term.create("/", convert(divideAst.getLeftArgument()), convert(divideAst.getRightArgument())); + case UnaryPlusAst unaryPlusAst -> + Term.create("+", convert(unaryPlusAst.argument())); + case UnaryMinusAst unaryMinusAst -> + Term.create("-", convert(unaryMinusAst.argument())); + case BooleanNotAst booleanNotAst -> + notTerm(convert(booleanNotAst.argument())); + case BoundAst boundAst -> + functionTerm(Processor.BOUND, convert(boundAst.argument())); + case IsIriAst isIriAst -> + functionTerm("isIRI", convert(isIriAst.argument())); + case IsBlankAst isBlankAst -> + functionTerm("isBlank", convert(isBlankAst.argument())); + case IsLiteralAst isLiteralAst -> + functionTerm("isLiteral", convert(isLiteralAst.argument())); + case StrAst strAst -> + functionTerm("str", convert(strAst.argument())); + case LangAst langAst -> + functionTerm("lang", convert(langAst.argument())); + case DatatypeAst datatypeAst -> + functionTerm("datatype", convert(datatypeAst.argument())); + case SameTermAst sameTermAst -> + functionTerm("sameTerm", convert(sameTermAst.getLeftArgument()), convert(sameTermAst.getRightArgument())); + case LangMatchesAst langMatchesAst -> + functionTerm("langMatches", convert(langMatchesAst.getLeftArgument()), convert(langMatchesAst.getRightArgument())); + case BinaryRegexAst binaryRegexAst -> + regexTerm(convert(binaryRegexAst.getString()), convert(binaryRegexAst.getPattern())); + case TrinaryRegexAst trinaryRegexAst -> + regexTerm(convert(trinaryRegexAst.getString()), convert(trinaryRegexAst.getPattern()), convert(trinaryRegexAst.getFlags())); + case FunctionCallAst(TermAst functionName, List arguments) -> + functionCallAst(functionName, arguments); + case ConcatAst concatAst -> + variadicTerm("concat", concatAst.arguments()); + case CoalesceAst coalesceAst -> + variadicTerm(Processor.COALESCE, coalesceAst.arguments()); + case IfAst(TermAst condition, TermAst thenExpr, TermAst elseExpr) -> + Term.function(Processor.IF, convert(condition), convert(thenExpr), convert(elseExpr)); + case ReplaceAst replaceAst -> + replaceToTerm(replaceAst); + case SubstrAst substrAst -> + substrToTerm(substrAst); + case BnodeAst bnodeAst -> + bnodeToTerm(bnodeAst); + case StrStartsAst strStartsAst -> + functionTerm("strstarts", convert(strStartsAst.getLeftArgument()), convert(strStartsAst.getRightArgument())); + case StrEndsAst strEndsAst -> + functionTerm("strends", convert(strEndsAst.getLeftArgument()), convert(strEndsAst.getRightArgument())); + case ContainsAst containsAst -> + functionTerm("contains", convert(containsAst.getLeftArgument()), convert(containsAst.getRightArgument())); + case StrBeforeAst strBeforeAst -> + functionTerm("strbefore", convert(strBeforeAst.getLeftArgument()), convert(strBeforeAst.getRightArgument())); + case StrAfterAst strAfterAst -> + functionTerm("strafter", convert(strAfterAst.getLeftArgument()), convert(strAfterAst.getRightArgument())); + case StrLangAst strLangAst -> + functionTerm("strlang", convert(strLangAst.getLeftArgument()), convert(strLangAst.getRightArgument())); + case StrDtAst strDtAst -> + functionTerm(Processor.STRDT, convert(strDtAst.getLeftArgument()), convert(strDtAst.getRightArgument())); + case IriFunctionAst iriFunctionAst -> + functionTerm("iri", convert(iriFunctionAst.argument())); + case LcaseAst lcaseAst -> + functionTerm("lcase", convert(lcaseAst.argument())); + case UcaseAst ucaseAst -> + functionTerm("ucase", convert(ucaseAst.argument())); + case EncodeForUriAst encodeForUriAst -> + functionTerm("encode_for_uri", convert(encodeForUriAst.argument())); + case Md5Ast md5Ast -> + functionTerm("md5", convert(md5Ast.argument())); + case Sha1Ast sha1Ast -> + functionTerm("sha1", convert(sha1Ast.argument())); + case Sha256Ast sha256Ast -> + functionTerm("sha256", convert(sha256Ast.argument())); + case Sha384Ast sha384Ast -> + functionTerm("sha384", convert(sha384Ast.argument())); + case Sha512Ast sha512Ast -> + functionTerm("sha512", convert(sha512Ast.argument())); + case ExistsAst(GroupGraphPatternAst pattern) -> + new AstBackedExistTerm(pattern); + case NotExistsAst(GroupGraphPatternAst pattern) -> + notTerm(new AstBackedExistTerm(pattern)); + default -> + throw new UnsupportedQueryFeatureException( + "Filter expression is not supported yet by the next pipeline: " + + constraint.getClass().getSimpleName()); + }; + } + + /** + * Builds a boolean negation with the runtime operator code already set. + * + *

The interpreter resolves operator codes in a later compilation phase + * ({@code Processor.type(Term, ASTQuery)}) that this bridge never runs, so {@code oper()} is set + * explicitly here. This keeps {@code !EXISTS { ... }} and {@code NOT EXISTS { ... }} + * equivalent to KGRAM.

+ */ + private static Term notTerm(Expression expression) { + Term not = Term.create("!", expression); + not.setOper(fr.inria.corese.core.next.query.kgram.api.core.ExprType.NOT); + return not; + } + + private static Term functionTerm(String name, Expression arg) { + Term t = Term.function(name); + t.add(arg); + return t; + } + + private static Term functionTerm(String name, Expression a1, Expression a2) { + Term t = Term.function(name); + t.add(a1); + t.add(a2); + return t; + } + + private static Term regexTerm(Expression s, Expression pattern) { + Term t = Term.function("regex"); + t.add(s); + t.add(pattern); + return t; + } + + private static Term regexTerm(Expression s, Expression pattern, Expression flags) { + Term t = Term.function("regex"); + t.add(s); + t.add(pattern); + t.add(flags); + return t; + } + + private static Term functionCallAst(TermAst functionName, List arguments) { + String name = SparqlBuiltinFunctionNameResolver.fromFunctionTerm(functionName); + Term t = Term.function(name); + for (TermAst arg : arguments) { + t.add(convert(arg)); + } + return t; + } + + private static Term variadicTerm(String name, List args) { + Term t = Term.function(name); + for (TermAst arg : args) { + t.add(convert(arg)); + } + return t; + } + + private static Term replaceToTerm(ReplaceAst r) { + Term t = + Term.function( + "replace", + convert(r.getString()), + convert(r.getPattern()), + convert(r.getReplacement())); + if (r.hasFlags()) { + t.add(convert(r.getFlags())); + } + return t; + } + + private static Term substrToTerm(SubstrAst s) { + if (s.getLength() != null) { + return Term.function( + "substr", + convert(s.getString()), + convert(s.getStart()), + convert(s.getLength())); + } + return Term.function("substr", convert(s.getString()), convert(s.getStart())); + } + + private static Term bnodeToTerm(BnodeAst b) { + if (b.getLabel() == null) { + return Term.function(Processor.BNODE); + } + return Term.function(Processor.BNODE, convert(b.getLabel())); + } + + private static final class KeywordHolder { + static final String SEAND = fr.inria.corese.core.sparql.triple.cst.Keyword.SEAND; + static final String SEOR = fr.inria.corese.core.sparql.triple.cst.Keyword.SEOR; + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java index 529b5cc77..9eb68c7c1 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java @@ -40,8 +40,8 @@ void literalAstToExpression() { @Test void varAstToExpression() { - VarAst var = new VarAst("var1"); - Expression varNode = SparqlAstToExpression.convert(var); + VarAst variableAst = new VarAst("var1"); + Expression varNode = SparqlAstToExpression.convert(variableAst); assertNotNull(varNode); assertInstanceOf(Variable.class, varNode); assertEquals("var1", varNode.getLabel()); @@ -55,5 +55,18 @@ void existsFilterConvertsToExistTerm() { assertNotNull(term); assertEquals(ExprType.EXIST, term.oper()); + assertTrue(term.isExist()); + assertTrue(term.isRecExist()); + } + + @Test + void existsFilterRequiresWhereCompilerWhenConvertedToFilter() { + ExistsAst exists = new ExistsAst(new GroupGraphPatternAst(List.of())); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> SparqlAstToExpression.toNextFilter(exists)); + + assertTrue(error.getMessage().contains("WhereCompiler")); } } From fb9cfe07155ad96ccf0a88eb4bc153efe7801ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 16 Jul 2026 16:22:39 +0200 Subject: [PATCH 4/4] Expand EXISTS bridge tests --- .../CoreseAstQueryBuilderDescribeTest.java | 59 +++++++++++++++++++ .../bridge/CoreseAstQueryBuilderTest.java | 21 ++++++- 2 files changed, 78 insertions(+), 2 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 9400bab37..b7e149e0c 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,10 @@ import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; 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.constraint.ExistsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NotExistsAst; +import fr.inria.corese.core.next.query.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.kgram.api.core.ExprType; 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; @@ -160,6 +164,61 @@ void appliesOrderBy() { assertFalse(query.getOrderBy().isEmpty(), "ORDER BY applied"); } + @Test + @DisplayName("DESCRIBE WHERE { FILTER EXISTS { ... } } compiles the EXISTS pattern") + void filterExistsInDescribeWhere() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("q"), new VarAst("z")))))); + FilterAst filter = new FilterAst(new ExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))), + filter)); + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), where); + + Query query = builder.toNextQuery(describe); + + assertLoweredDescribeConstruct(query, 1); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + Expr expr = filterExp.getFilter().getExp(); + assertEquals(ExprType.EXIST, expr.oper(), "FILTER operator is EXIST"); + assertInstanceOf(Exp.class, expr.getPattern(), "EXISTS pattern is compiled"); + } + + @Test + @DisplayName("DESCRIBE WHERE { FILTER NOT EXISTS { ... } } compiles as NOT wrapping EXIST") + void filterNotExistsInDescribeWhere() { + GroupGraphPatternAst existsPattern = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("q"), new VarAst("z")))))); + FilterAst filter = new FilterAst(new NotExistsAst(existsPattern)); + GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( + new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))), + filter)); + DescribeQueryAst describe = new DescribeQueryAst( + DatasetClauseAst.none(), List.of(new VarAst("x")), where); + + Query query = builder.toNextQuery(describe); + + assertLoweredDescribeConstruct(query, 1); + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER is in the compiled body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + Expr exist = notExpr.getExpList().getFirst(); + assertEquals(ExprType.EXIST, exist.oper(), "inner expression is EXIST"); + assertInstanceOf(Exp.class, exist.getPattern(), "NOT EXISTS pattern is compiled"); + } + + private static Exp findFilter(Exp body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i).isFilter()) { + return body.get(i); + } + } + return null; + } + @Test @DisplayName("Inline VALUES is not supported yet -> UnsupportedQueryFeatureException") void rejectsValuesClause() { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java index 8cfc87079..c8e5c1198 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java @@ -282,8 +282,7 @@ private static SelectQueryAst getSelectQueryAst() { GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))), filter)); - SelectQueryAst select = new SelectQueryAst(where); - return select; + return new SelectQueryAst(where); } @Test @@ -303,6 +302,24 @@ void parsedSelectWithFilterNotExists() { assertInstanceOf(Exp.class, notExpr.getExpList().getFirst().getPattern(), "NOT EXISTS pattern is compiled"); } + @Test + @DisplayName("Parser -> SELECT WHERE { FILTER(!EXISTS { ... }) } produces NOT wrapping EXIST") + void parsedSelectWithNegatedExists() { + QueryAst ast = newParserDefault().parse( + "SELECT * WHERE { ?s ?p ?o . FILTER(!EXISTS { ?s ?t }) }"); + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast); + + Query query = builder.toNextQuery(select); + + Exp filterExp = findFilter(query.getBody()); + assertNotNull(filterExp, "FILTER !EXISTS compiled into body"); + Expr notExpr = filterExp.getFilter().getExp(); + assertEquals(ExprType.NOT, notExpr.oper(), "top operator is boolean NOT"); + Expr exist = notExpr.getExpList().getFirst(); + assertEquals(ExprType.EXIST, exist.oper(), "inner expression is EXIST"); + assertInstanceOf(Exp.class, exist.getPattern(), "negated EXISTS pattern is compiled"); + } + private static Exp findFilter(Exp body) { for (int i = 0; i < body.size(); i++) { if (body.get(i).isFilter()) {