From 8ed092bb13ef60b811ae0f1d03ff1375c892ee27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 8 Sep 2026 13:13:09 +0200 Subject: [PATCH 1/3] refactor(next): centralize SPARQL term resolution in bridge --- .../sparql/bridge/CoreseAstQueryBuilder.java | 13 +---------- .../sparql/bridge/SparqlTermResolver.java | 22 +++++++++++++++++-- .../impl/sparql/bridge/WhereCompiler.java | 12 +++++----- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index 807979405..ba32d3c52 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 @@ -184,18 +184,7 @@ static Node toNode(TermAst term) { } static Node toNode(TermAst term, SparqlTermResolver resolver) { - return switch (term) { - case VarAst(String name) -> NodeImpl.forVariable(name); - case IriAst(String raw) when raw.startsWith("_:") -> NodeImpl.forBlank(raw.substring(2)); - case IriAst(String raw) -> NodeImpl.forIRI(resolver.resolveIri(raw)); - case LiteralAst(String lexical, String lang, String datatype) -> NodeImpl.forLiteral( - resolver.unquoteLexical(lexical), - resolver.normalizeDatatypeIri(datatype), - lang); - default -> throw new IllegalArgumentException( - "A query term must be a variable, IRI or literal, got: " - + term.getClass().getSimpleName()); - }; + return resolver.toNode(term); } static TermAst simplePredicate(PathAst path) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java index af3a65b9c..9ae6af494 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java @@ -5,10 +5,14 @@ import fr.inria.corese.core.next.data.impl.namespace.PrefixHandler; import fr.inria.corese.core.next.data.spi.io.IOConstants; import fr.inria.corese.core.next.data.spi.term.IRIUtils; +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.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; import fr.inria.corese.core.next.query.impl.sparql.ast.PrefixDeclarationAst; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; - -import java.util.Objects; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; /** Resolves SPARQL terms against one immutable query-prologue snapshot. */ public final class SparqlTermResolver { @@ -45,6 +49,20 @@ public String resolveIri(String raw) { return resolvePrefixedIri(raw); } + public Node toNode(TermAst term) { + return switch (term) { + case VarAst(String name) -> NodeImpl.forVariable(name); + case IriAst(String raw) when raw.startsWith(IOConstants.BLANK_NODE_PREFIX) -> + NodeImpl.forBlank(raw.substring(IOConstants.BLANK_NODE_PREFIX.length())); + case IriAst(String raw) -> NodeImpl.forIRI(resolveIri(raw)); + case LiteralAst(String lexical, String lang, String datatype) -> NodeImpl.forLiteral( + unquoteLexical(lexical), normalizeDatatypeIri(datatype), lang); + default -> throw new IllegalArgumentException( + "A query term must be a variable, IRI or literal, got: " + + term.getClass().getSimpleName()); + }; + } + public String normalizeDatatypeIri(String datatype) { if (datatype == null || datatype.isEmpty()) { return null; 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 4e5a9568b..49744d660 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 @@ -120,10 +120,10 @@ private Exp compileBgp(BgpAst bgp) { } private Edge toEdge(TriplePatternAst triple) { - Node subject = CoreseAstQueryBuilder.toNode(triple.subject(), termResolver); - Node predicate = CoreseAstQueryBuilder.toNode( - CoreseAstQueryBuilder.simplePredicate(triple.predicate()), termResolver); - Node object = CoreseAstQueryBuilder.toNode(triple.object(), termResolver); + Node subject = termResolver.toNode(triple.subject()); + Node predicate = termResolver.toNode( + CoreseAstQueryBuilder.simplePredicate(triple.predicate())); + Node object = termResolver.toNode(triple.object()); return new AstBackedEdge(subject, predicate, object); } @@ -165,7 +165,7 @@ private Exp compileMinus(MinusAst minus) { */ private Exp compileBind(BindAst bind) { Filter filter = new AstBackedExpr(bind.expression(), this).getFilter(); - Node variable = CoreseAstQueryBuilder.toNode(bind.variable(), termResolver); + Node variable = termResolver.toNode(bind.variable()); Exp exp = Exp.create(Type.BIND); exp.setFilter(filter); exp.setFunctional(filter.isFunctional()); @@ -177,7 +177,7 @@ private Exp compileBind(BindAst bind) { * Compiles {@code SERVICE { ... }} into a KGRAM {@link Exp}. */ private Exp compileService(ServiceAst service) { - Node endpoint = CoreseAstQueryBuilder.toNode(service.endpoint(), termResolver); + Node endpoint = termResolver.toNode(service.endpoint()); Exp endpointNode = Exp.create(Type.NODE, endpoint); Query body = Query.create(compile(service.pattern())); body.setService(true); From 7484ff71d26cdd4c9a0ce9fd35ef3f6cc82eee2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 8 Sep 2026 13:31:31 +0200 Subject: [PATCH 2/3] refactor(next): resolve bridge terms without builder indirection --- .../query/impl/sparql/bridge/AstBackedExpr.java | 2 +- .../sparql/bridge/CoreseAstQueryBuilder.java | 16 ++++------------ .../sparql/bridge/NativeEvaluationContext.java | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java index 24c61c731..6a516a797 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 @@ -138,7 +138,7 @@ public DatatypeValue getValue() { SparqlTermResolver resolver = whereCompiler == null ? new SparqlTermResolver(null) : whereCompiler.termResolver(); - return CoreseAstQueryBuilder.toNode(source, resolver).getDatatypeValue(); + return resolver.toNode(source).getDatatypeValue(); } 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 ba32d3c52..d99a43f89 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 @@ -179,14 +179,6 @@ public Filter toNextFilter(ConstraintAst filterExpression) { *

This helper is package-visible because both the query builder and the * {@link WhereCompiler} need a single shared term-to-node conversion rule.

*/ - static Node toNode(TermAst term) { - return toNode(term, new SparqlTermResolver(null)); - } - - static Node toNode(TermAst term, SparqlTermResolver resolver) { - return resolver.toNode(term); - } - static TermAst simplePredicate(PathAst path) { if (path instanceof PredicatePathAst(TermAst predicate)) { return predicate; @@ -307,7 +299,7 @@ private void applyDataset(Query query, DatasetClauseAst datasetClause, WhereComp private List toNodeList(Iterable iris, WhereCompiler compiler) { List nodes = new ArrayList<>(); for (IriAst iri : iris) { - nodes.add(toNode(iri, compiler.termResolver())); + nodes.add(compiler.termResolver().toNode(iri)); } return nodes; } @@ -360,7 +352,7 @@ private List describeNodes( } nodes.add(node); } else { - nodes.add(toNode(term, compiler.termResolver())); + nodes.add(compiler.termResolver().toNode(term)); } } return nodes; @@ -545,8 +537,8 @@ private Exp compileConstructTemplate( private Node constructNode(Query query, TermAst term, WhereCompiler compiler) { if (term instanceof VarAst(String name)) { Node bound = visibleBodyNode(query, name); - return bound != null ? bound : toNode(term, compiler.termResolver()); + return bound != null ? bound : compiler.termResolver().toNode(term); } - return toNode(term, compiler.termResolver()); + return compiler.termResolver().toNode(term); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java index efe5d5e4f..5cc19693f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java @@ -60,7 +60,7 @@ DatatypeValue variable(String name) { } DatatypeValue constant(TermAst expression) { - return CoreseAstQueryBuilder.toNode(expression, termResolver()).getDatatypeValue(); + return termResolver().toNode(expression).getDatatypeValue(); } DatatypeValue required(TermAst expression) { From 1af1045eb319d14ed6755d4e92069207a9c8ba28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 8 Sep 2026 15:28:51 +0200 Subject: [PATCH 3/3] refactor(next): relocate predicate path extraction to WhereCompiler and add bridge tests (#576) --- .../sparql/bridge/CoreseAstQueryBuilder.java | 21 +---- .../impl/sparql/bridge/NextFilterFromAst.java | 7 +- .../impl/sparql/bridge/WhereCompiler.java | 14 +++- .../impl/sparql/bridge/AstBackedExprTest.java | 80 +++++++++++++++++++ 4 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index d99a43f89..a3ae1706a 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 @@ -3,8 +3,6 @@ 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.path.PathAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; 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; @@ -172,22 +170,6 @@ public Filter toNextFilter(ConstraintAst filterExpression) { return new AstBackedExpr(filterExpression, whereCompiler).getFilter(); } - /** - * Converts a query term used as subject, predicate, object, or variable reference - * into a runtime {@link Node}. - * - *

This helper is package-visible because both the query builder and the - * {@link WhereCompiler} need a single shared term-to-node conversion rule.

- */ - static TermAst simplePredicate(PathAst path) { - if (path instanceof PredicatePathAst(TermAst predicate)) { - return predicate; - } - throw new UnsupportedQueryFeatureException( - "Property path bridge compilation is not supported yet by the next pipeline for: " - + path.getClass().getSimpleName()); - } - /** * Rejects unsupported clauses for {@code ASK} queries. * @@ -521,7 +503,8 @@ private Exp compileConstructTemplate( Exp bgp = Exp.create(Type.BGP); for (TriplePatternAst triple : template.triplePatternAsts()) { Node subject = constructNode(query, triple.subject(), compiler); - Node predicate = constructNode(query, simplePredicate(triple.predicate()), compiler); + Node predicate = constructNode( + query, WhereCompiler.simplePredicate(triple.predicate()), compiler); Node object = constructNode(query, triple.object(), compiler); bgp.add(new AstBackedEdge(subject, predicate, object)); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java index 2893e6904..743bbe6e5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java @@ -1,6 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; - import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.engine.model.Expr; import fr.inria.corese.core.next.query.impl.engine.model.Filter; @@ -10,9 +9,7 @@ import java.util.List; import java.util.Optional; -/** - * {@link Filter} view exposing native AST metadata through the Corese-next {@link Expr} API. - */ +/** Filter view exposing native AST metadata through the Corese-next expression API. */ public final class NextFilterFromAst implements Filter { private final AstBackedExpr owner; @@ -71,4 +68,4 @@ public boolean isRecExist() { public Optional coreseNextSource() { return owner.sourceAst(); } -} +} \ 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 49744d660..82f268795 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 @@ -11,8 +11,11 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; import fr.inria.corese.core.next.query.impl.sparql.ast.ServiceAst; +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.UnionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; import fr.inria.corese.core.next.query.impl.engine.model.Filter; @@ -122,11 +125,20 @@ private Exp compileBgp(BgpAst bgp) { private Edge toEdge(TriplePatternAst triple) { Node subject = termResolver.toNode(triple.subject()); Node predicate = termResolver.toNode( - CoreseAstQueryBuilder.simplePredicate(triple.predicate())); + simplePredicate(triple.predicate())); Node object = termResolver.toNode(triple.object()); return new AstBackedEdge(subject, predicate, object); } + static TermAst simplePredicate(PathAst path) { + if (path instanceof PredicatePathAst(TermAst predicate)) { + return predicate; + } + throw new UnsupportedQueryFeatureException( + "Property path bridge compilation is not supported yet by the next pipeline for: " + + path.getClass().getSimpleName()); + } + private Exp compileFilter(FilterAst filter) { Filter nextFilter = new AstBackedExpr(filter.operator(), this).getFilter(); return Exp.create(Type.FILTER, nextFilter); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java new file mode 100644 index 000000000..af39c82a3 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java @@ -0,0 +1,80 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +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.PrefixDeclarationAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BoundAst; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("AstBackedExpr: AST wrapping, value resolution, and filter view") +class AstBackedExprTest { + + @Test + void resolvesConstantUsingCompilerPrologue() { + QueryPrologueAst prologue = new QueryPrologueAst( + List.of(new PrefixDeclarationAst("ex:", new IriAst("http://example.org/"))), + null); + WhereCompiler compiler = new WhereCompiler().withPrologue(prologue); + AstBackedExpr expression = new AstBackedExpr(new IriAst("ex:name"), compiler); + + assertEquals("http://example.org/name", expression.getValue().stringValue()); + assertTrue(expression.isConstant()); + assertFalse(expression.isVariable()); + } + + @Test + void resolvesLiteralValue() { + WhereCompiler compiler = new WhereCompiler(); + AstBackedExpr stringExpr = new AstBackedExpr(new LiteralAst("\"hello\"", null, null), compiler); + assertEquals("hello", stringExpr.getValue().stringValue()); + + AstBackedExpr intExpr = new AstBackedExpr(new LiteralAst("42", null, "http://www.w3.org/2001/XMLSchema#integer"), compiler); + assertEquals("42", intExpr.getValue().stringValue()); + } + + @Test + void variableExpressionMetadata() { + WhereCompiler compiler = new WhereCompiler(); + AstBackedExpr varExpr = new AstBackedExpr(new VarAst("x"), compiler); + + assertEquals("x", varExpr.getLabel()); + assertTrue(varExpr.isVariable()); + assertFalse(varExpr.isConstant()); + assertTrue(varExpr.getExpList().isEmpty()); + assertNull(varExpr.getArg()); + } + + @Test + void boundExpressionAndFilterView() { + WhereCompiler compiler = new WhereCompiler(); + BoundAst boundAst = new BoundAst(List.of(new VarAst("y"))); + AstBackedExpr boundExpr = new AstBackedExpr(boundAst, compiler); + + assertTrue(boundExpr.isBound()); + assertNotNull(boundExpr.getFilter()); + assertTrue(boundExpr.getFilter().isBound()); + assertEquals(List.of("y"), boundExpr.getFilter().getVariables()); + assertEquals(boundAst, boundExpr.getFilter().getFilterExpression()); + } + + @Test + void immutableExpressionsThrowOnModification() { + WhereCompiler compiler = new WhereCompiler(); + AstBackedExpr expr = new AstBackedExpr(new VarAst("z"), compiler); + + assertThrows(UnsupportedOperationException.class, () -> expr.setExp(0, expr)); + assertThrows(UnsupportedOperationException.class, () -> expr.setArg(expr)); + } +}