From 2fa6a8f9e67338eba155ea3805323525b018097f Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 22 Jun 2026 09:42:00 +0200 Subject: [PATCH 1/4] #384 [BRIDGE] WHERE Compiler --- .../impl/sparql/bridge/AstBackedEdge.java | 58 +++++++ .../impl/sparql/bridge/WhereCompiler.java | 160 ++++++++++++++++++ .../core/sparql/triple/parser/Term.java | 6 +- 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java new file mode 100644 index 000000000..43302a172 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java @@ -0,0 +1,58 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.kgram.api.core.Edge; +import fr.inria.corese.core.next.query.kgram.api.core.Node; + +import java.util.Objects; + +/** + * A query-side {@link Edge} produced by the WHERE compiler from a + * {@link fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst}. + */ +final class AstBackedEdge implements Edge { + + private final Node subject; + private final Node predicate; + private final Node object; + + AstBackedEdge(Node subject, Node predicate, Node object) { + this.subject = Objects.requireNonNull(subject, "subject"); + this.predicate = Objects.requireNonNull(predicate, "predicate"); + this.object = Objects.requireNonNull(object, "object"); + } + + @Override + public Node getNode(int i) { + return switch (i) { + case 0 -> subject; + case 1 -> object; + default -> throw new IndexOutOfBoundsException( + "Triple pattern edge has nodes 0 (subject) and 1 (object), got: " + i); + }; + } + + @Override + public Node getProperty() { + return predicate; + } + + @Override + public String getEdgeLabel() { + return predicate.getLabel(); + } + + @Override + public Node getGraph() { + return null; + } + + @Override + public Edge getEdge() { + return this; + } + + @Override + public String toString() { + return subject + " " + predicate + " " + object; + } +} \ 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 new file mode 100644 index 000000000..a566f5cea --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java @@ -0,0 +1,160 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.kgram.api.core.Edge; +import fr.inria.corese.core.next.query.kgram.api.core.ExpType.Type; +import fr.inria.corese.core.next.query.kgram.api.core.Filter; +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; +import fr.inria.corese.core.sparql.triple.parser.Atom; +import fr.inria.corese.core.sparql.triple.parser.Expression; + +import java.util.Objects; + +/** + * Compiles the content of a SPARQL {@code WHERE} clause — a tree of + * {@link PatternAst} nodes — into a KGRAM {@link Exp} body that the engine can + * evaluate. + */ +public final class WhereCompiler { + + public WhereCompiler() { + } + + public Exp compile(GroupGraphPatternAst where) { + Objects.requireNonNull(where, "where"); + return compileGroup(where); + } + + /** + * Dispatch a single {@link PatternAst} element to its KGRAM {@link Exp}. + */ + public Exp compile(PatternAst pattern) { + Objects.requireNonNull(pattern, "pattern"); + return switch (pattern) { + case BgpAst bgp -> compileBgp(bgp); + case FilterAst filter -> compileFilter(filter); + case UnionAst union -> compileUnion(union); + case OptionalAst optional -> compileOptional(optional); + case MinusAst minus -> compileMinus(minus); + case BindAst bind -> compileBind(bind); + case ServiceAst service -> compileService(service); + case GroupGraphPatternAst group -> compileGroup(group); + default -> throw new UnsupportedOperationException( + "WHERE compilation not yet supported for: " + pattern.getClass().getSimpleName()); + }; + } + + + /** + * {@code { e1 e2 ... en }} becomes an {@code AND} of the compiled elements. + */ + private Exp compileGroup(GroupGraphPatternAst group) { + Exp body = Exp.create(Type.AND); + for (PatternAst element : group.patterns()) { + if (element instanceof OptionalAst(PatternAst ast)) { + Exp left = body; + Exp right = compile(ast); + Exp optionalExp = Exp.create(Type.OPTIONAL, left, right); + body = Exp.create(Type.AND); + body.add(optionalExp); + } else if (element instanceof MinusAst(GroupGraphPatternAst pattern)) { + Exp left = body; + Exp right = compile(pattern); + Exp minusExp = Exp.create(Type.MINUS, left, right); + body = Exp.create(Type.AND); + body.add(minusExp); + } else { + body.add(compile(element)); + } + } + return body; + } + + + private Exp compileBgp(BgpAst bgp) { + Exp pattern = Exp.create(Type.BGP); + for (TriplePatternAst triple : bgp.triples()) { + pattern.add(toEdge(triple)); + } + return pattern; + } + + private Edge toEdge(TriplePatternAst triple) { + Node subject = toNode(triple.subject()); + Node predicate = toNode(triple.predicate()); + Node object = toNode(triple.object()); + return new AstBackedEdge(subject, predicate, object); + } + + private Node toNode(TermAst term) { + Expression expression = SparqlAstToExpression.convert(term); + if (expression instanceof Atom atom) { + return new NodeImpl(atom); + } + throw new IllegalArgumentException( + "A triple-pattern term must be a variable, IRI or literal, got: " + + term.getClass().getSimpleName()); + } + + + private Exp compileFilter(FilterAst filter) { + Filter nextFilter = SparqlAstToExpression.toNextFilter(filter); + return Exp.create(Type.FILTER, nextFilter); + } + + + private Exp compileUnion(UnionAst union) { + Exp left = compile(union.left()); + Exp right = compile(union.right()); + return Exp.create(Type.UNION, left, right); + } + + + /** + * Compiles a bare {@link OptionalAst}, i.e. one that is not folded with a + * preceding pattern by {@link #compileGroup(GroupGraphPatternAst)} (for + * instance a lone {@code { OPTIONAL { ... } }}). The mandatory left part is + * then the empty pattern, matching SPARQL's {@code {} OPTIONAL { ... }}. + */ + private Exp compileOptional(OptionalAst optional) { + Exp left = Exp.create(Type.AND); + Exp right = compile(optional.ast()); + return Exp.create(Type.OPTIONAL, left, right); + } + + /** + * Compiles a bare {@link MinusAst} not folded with a preceding pattern. + * The mandatory left part is the empty pattern, matching {@code {} MINUS { ... }}. + */ + private Exp compileMinus(MinusAst minus) { + Exp left = Exp.create(Type.AND); + Exp right = compile(minus.pattern()); + return Exp.create(Type.MINUS, left, right); + } + + /** + * Compiles {@code BIND(expression AS ?var)} into a KGRAM {@link Exp}. + */ + private Exp compileBind(BindAst bind) { + Filter filter = SparqlAstToExpression.toNextFilter(bind.expression()); + Node variable = toNode(bind.variable()); + Exp exp = Exp.create(Type.BIND); + exp.setFilter(filter); + exp.setNode(variable); + return exp; + } + + /** + * Compiles {@code SERVICE { ... }} into a KGRAM {@link Exp}. + */ + private Exp compileService(ServiceAst service) { + Node endpoint = toNode(service.endpoint()); + Exp endpointNode = Exp.create(Type.NODE, endpoint); + Exp body = compile(service.pattern()); + Exp exp = Exp.create(Type.SERVICE, endpointNode, body); + exp.setSilent(service.silent()); + return exp; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/sparql/triple/parser/Term.java b/src/main/java/fr/inria/corese/core/sparql/triple/parser/Term.java index 5db3a3436..b778ca2f2 100755 --- a/src/main/java/fr/inria/corese/core/sparql/triple/parser/Term.java +++ b/src/main/java/fr/inria/corese/core/sparql/triple/parser/Term.java @@ -1466,7 +1466,11 @@ public boolean isRecAggregate() { if (isAggregate(getLabel())) { return true; } - for (Expr exp : getExpList()) { + List expList = getExpList(); + if (expList == null) { + return false; + } + for (Expr exp : expList) { if (exp.isRecAggregate()) { return true; } From 1c358460d859fc7cbaa5e4abd9053df725bc36e1 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 23 Jun 2026 09:59:29 +0200 Subject: [PATCH 2/4] #384 [BRIDGE] WHERE Compiler TU --- .../impl/sparql/bridge/WhereCompilerTest.java | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java new file mode 100644 index 000000000..0cdf00266 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java @@ -0,0 +1,140 @@ +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.GreaterThanAst; +import fr.inria.corese.core.next.query.kgram.core.Exp; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class WhereCompilerTest { + + private final WhereCompiler compiler = new WhereCompiler(); + + + private static GroupGraphPatternAst group(PatternAst... elements) { + return new GroupGraphPatternAst(List.of(elements)); + } + + + @Test + @DisplayName("OPTIONAL en left join (first = partie obligatoire, rest = partie optionnelle)") + void compilesOptionalAsLeftJoin() { + BgpAst mandatory = new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); + BgpAst optionalBgp = new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("q"), new VarAst("z")))); + + Exp body = compiler.compile(group(mandatory, new OptionalAst(optionalBgp))); + + assertTrue(body.isAnd()); + Exp optional = body.get(0); + assertTrue(optional.isOptional(), "OPTIONAL node"); + assertTrue(optional.first().isAnd(), "mandatory left part is the preceding group"); + assertTrue(optional.first().get(0).isBGP()); + assertTrue(optional.rest().isBGP(), "optional right part is the optional body"); + } + + + @Test + @DisplayName("UNION binaire de ses deux branches") + void compilesUnion() { + UnionAst union = new UnionAst( + group(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o"))))), + group(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("q"), new VarAst("z")))))); + + Exp body = compiler.compile(group(union)); + + Exp unionExp = body.get(0); + assertTrue(unionExp.isUnion()); + assertTrue(unionExp.first().isAnd()); + assertTrue(unionExp.rest().isAnd()); + } + + + @Test + @DisplayName("FILTER ajouté après le BGP dans le groupe") + void compilesFilter() { + BgpAst bgp = new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); + FilterAst filter = new FilterAst(new GreaterThanAst(List.of( + new VarAst("o"), new LiteralAst("5", null, null)))); + + Exp body = compiler.compile(group(bgp, filter)); + + assertEquals(2, body.size()); + assertTrue(body.get(0).isBGP()); + assertTrue(body.get(1).isFilter(), "second element is a FILTER"); + } + + + @Test + @DisplayName("MINUS folded avec le pattern précédent") + void compilesMinus() { + BgpAst main = new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); + BgpAst subtracted = new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("q"), new VarAst("z")))); + + Exp body = compiler.compile(group(main, new MinusAst(group(subtracted)))); + + assertTrue(body.isAnd()); + Exp minusExp = body.get(0); + assertTrue(minusExp.isMinus(), "MINUS node"); + assertTrue(minusExp.first().isAnd(), "left part est le pattern précédent"); + assertTrue(minusExp.first().get(0).isBGP()); + assertTrue(minusExp.rest().isAnd(), "right part est le corps du MINUS"); + } + + + @Test + @DisplayName("BIND produit un Exp avec filter et node variable") + void compilesBind() { + BindAst bind = new BindAst(new VarAst("x"), new VarAst("y")); + + Exp body = compiler.compile(group(bind)); + + assertEquals(1, body.size()); + Exp bindExp = body.get(0); + assertTrue(bindExp.isBind(), "BIND node"); + assertNotNull(bindExp.getFilter(), "filter porte l'expression"); + assertNotNull(bindExp.getNode(), "node porte la variable cible"); + } + + @Test + @DisplayName("SERVICE produit un Exp avec endpoint et corps") + void compilesService() { + ServiceAst service = new ServiceAst( + new IriAst(""), + false, + group(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))))); + + Exp body = compiler.compile(group(service)); + + Exp serviceExp = body.get(0); + assertTrue(serviceExp.isService(), "SERVICE node"); + assertFalse(serviceExp.isSilent(), "pas de flag SILENT"); + assertTrue(serviceExp.rest().isAnd(), "le corps est un groupe AND"); + } + + @Test + @DisplayName("SERVICE SILENT positionne le flag isSilent") + void compilesServiceSilent() { + ServiceAst service = new ServiceAst( + new IriAst(""), + true, + group(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))))); + + Exp serviceExp = compiler.compile(service); + + assertTrue(serviceExp.isService()); + assertTrue(serviceExp.isSilent(), "flag SILENT activé"); + } +} \ No newline at end of file From 2123acc3ee7dda73fe38cad29ab1262952208c21 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 24 Jun 2026 11:05:04 +0200 Subject: [PATCH 3/4] #384 [BRIDGE] WHERE Compiler TU --- .../impl/sparql/bridge/AstBackedEdge.java | 19 +++++ .../impl/sparql/bridge/WhereCompiler.java | 4 +- .../impl/sparql/bridge/AstBackedEdgeTest.java | 66 ++++++++++++++++++ .../impl/sparql/bridge/WhereCompilerTest.java | 69 +++++++++++++++---- 4 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java index 43302a172..3d99e24ac 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java @@ -36,6 +36,11 @@ public Node getProperty() { return predicate; } + @Override + public Node getEdgeVariable() { + return predicate.isVariable() ? predicate : null; + } + @Override public String getEdgeLabel() { return predicate.getLabel(); @@ -45,6 +50,20 @@ public String getEdgeLabel() { public Node getGraph() { return null; } + + @Override + public boolean contains(Node node) { + if (node == null) { + return false; + } + for (int i = 0; i < nbNode(); i++) { + Node n = getNode(i); + if (n != null && (n == node || n.same(node))) { + return true; + } + } + return false; + } @Override public Edge getEdge() { 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 a566f5cea..635a81392 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 @@ -6,6 +6,7 @@ import fr.inria.corese.core.next.query.kgram.api.core.Filter; import fr.inria.corese.core.next.query.kgram.api.core.Node; import fr.inria.corese.core.next.query.kgram.core.Exp; +import fr.inria.corese.core.next.query.kgram.core.Query; import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; import fr.inria.corese.core.sparql.triple.parser.Atom; import fr.inria.corese.core.sparql.triple.parser.Expression; @@ -142,6 +143,7 @@ private Exp compileBind(BindAst bind) { Node variable = toNode(bind.variable()); Exp exp = Exp.create(Type.BIND); exp.setFilter(filter); + exp.setFunctional(filter.isFunctional()); exp.setNode(variable); return exp; } @@ -152,7 +154,7 @@ private Exp compileBind(BindAst bind) { private Exp compileService(ServiceAst service) { Node endpoint = toNode(service.endpoint()); Exp endpointNode = Exp.create(Type.NODE, endpoint); - Exp body = compile(service.pattern()); + Query body = Query.create(compile(service.pattern())); Exp exp = Exp.create(Type.SERVICE, endpointNode, body); exp.setSilent(service.silent()); return exp; diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java new file mode 100644 index 000000000..709355723 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java @@ -0,0 +1,66 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.query.kgram.api.core.Node; +import fr.inria.corese.core.next.query.kgram.tool.NodeImpl; +import fr.inria.corese.core.sparql.triple.parser.Constant; +import fr.inria.corese.core.sparql.triple.parser.Variable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("AstBackedEdge: contains() and getEdgeVariable()") +class AstBackedEdgeTest { + + @Test + @DisplayName("contains() recognises the subject and the object") + void edgeContainsSubjectAndObject() { + Node subject = new NodeImpl(Variable.create("s")); + Node predicate = new NodeImpl(Variable.create("p")); + Node object = new NodeImpl(Variable.create("o")); + + AstBackedEdge edge = new AstBackedEdge(subject, predicate, object); + + assertTrue(edge.contains(subject)); + assertTrue(edge.contains(object)); + } + + @Test + @DisplayName("contains() recognises a variable by name (not only the same instance)") + void edgeContainsByVariableName() { + AstBackedEdge edge = new AstBackedEdge( + new NodeImpl(Variable.create("s")), + new NodeImpl(Variable.create("p")), + new NodeImpl(Variable.create("o"))); + + assertTrue(edge.contains(new NodeImpl(Variable.create("s")))); + assertTrue(edge.contains(new NodeImpl(Variable.create("o")))); + } + + @Test + @DisplayName("contains() returns false for an absent variable and for null") + void edgeDoesNotContainOtherNode() { + AstBackedEdge edge = new AstBackedEdge( + new NodeImpl(Variable.create("s")), + new NodeImpl(Variable.create("p")), + new NodeImpl(Variable.create("o"))); + + assertFalse(edge.contains(new NodeImpl(Variable.create("x")))); + assertFalse(edge.contains(null)); + } + + @Test + @DisplayName("getEdgeVariable() exposes the predicate when it is a variable, null otherwise") + void edgeVariableExposedOnlyForVariablePredicate() { + Node predicateVar = new NodeImpl(Variable.create("p")); + AstBackedEdge withVarPredicate = new AstBackedEdge( + new NodeImpl(Variable.create("s")), predicateVar, new NodeImpl(Variable.create("o"))); + assertSame(predicateVar, withVarPredicate.getEdgeVariable()); + + AstBackedEdge withIriPredicate = new AstBackedEdge( + new NodeImpl(Variable.create("s")), + new NodeImpl(Constant.createResource("http://example.org/p")), + new NodeImpl(Variable.create("o"))); + assertNull(withIriPredicate.getEdgeVariable()); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java index 0cdf00266..b4c6e5697 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompilerTest.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; import fr.inria.corese.core.next.query.kgram.core.Exp; import org.junit.jupiter.api.DisplayName; @@ -21,7 +22,7 @@ private static GroupGraphPatternAst group(PatternAst... elements) { @Test - @DisplayName("OPTIONAL en left join (first = partie obligatoire, rest = partie optionnelle)") + @DisplayName("OPTIONAL as a left join (first = mandatory part, rest = optional part)") void compilesOptionalAsLeftJoin() { BgpAst mandatory = new BgpAst(List.of( new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); @@ -40,7 +41,7 @@ void compilesOptionalAsLeftJoin() { @Test - @DisplayName("UNION binaire de ses deux branches") + @DisplayName("UNION of its two branches") void compilesUnion() { UnionAst union = new UnionAst( group(new BgpAst(List.of( @@ -58,7 +59,7 @@ void compilesUnion() { @Test - @DisplayName("FILTER ajouté après le BGP dans le groupe") + @DisplayName("FILTER added after the BGP in the group") void compilesFilter() { BgpAst bgp = new BgpAst(List.of( new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); @@ -74,7 +75,7 @@ void compilesFilter() { @Test - @DisplayName("MINUS folded avec le pattern précédent") + @DisplayName("MINUS folded with the preceding pattern") void compilesMinus() { BgpAst main = new BgpAst(List.of( new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); @@ -86,14 +87,14 @@ void compilesMinus() { assertTrue(body.isAnd()); Exp minusExp = body.get(0); assertTrue(minusExp.isMinus(), "MINUS node"); - assertTrue(minusExp.first().isAnd(), "left part est le pattern précédent"); + assertTrue(minusExp.first().isAnd(), "left part is the preceding pattern"); assertTrue(minusExp.first().get(0).isBGP()); - assertTrue(minusExp.rest().isAnd(), "right part est le corps du MINUS"); + assertTrue(minusExp.rest().isAnd(), "right part is the MINUS body"); } @Test - @DisplayName("BIND produit un Exp avec filter et node variable") + @DisplayName("BIND produces an Exp with a filter and a target variable node") void compilesBind() { BindAst bind = new BindAst(new VarAst("x"), new VarAst("y")); @@ -102,12 +103,12 @@ void compilesBind() { assertEquals(1, body.size()); Exp bindExp = body.get(0); assertTrue(bindExp.isBind(), "BIND node"); - assertNotNull(bindExp.getFilter(), "filter porte l'expression"); - assertNotNull(bindExp.getNode(), "node porte la variable cible"); + assertNotNull(bindExp.getFilter(), "filter carries the expression"); + assertNotNull(bindExp.getNode(), "node carries the target variable"); } @Test - @DisplayName("SERVICE produit un Exp avec endpoint et corps") + @DisplayName("SERVICE produces an Exp with an endpoint and a body") void compilesService() { ServiceAst service = new ServiceAst( new IriAst(""), @@ -119,12 +120,52 @@ void compilesService() { Exp serviceExp = body.get(0); assertTrue(serviceExp.isService(), "SERVICE node"); - assertFalse(serviceExp.isSilent(), "pas de flag SILENT"); - assertTrue(serviceExp.rest().isAnd(), "le corps est un groupe AND"); + assertFalse(serviceExp.isSilent(), "no SILENT flag"); + assertTrue(serviceExp.rest().isQuery(), "the body is a Query"); + assertTrue(serviceExp.rest().getQuery().getBody().isAnd(), "the Query body is an AND group"); } @Test - @DisplayName("SERVICE SILENT positionne le flag isSilent") + @DisplayName("SERVICE rest() exposes a Query for the runtime (exp.rest().getQuery() != null)") + void serviceRestShouldExposeAQueryForRuntime() { + ServiceAst service = new ServiceAst( + new IriAst(""), + false, + group(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))))); + + Exp serviceExp = new WhereCompiler().compile(service); + + assertTrue(serviceExp.isService()); + assertNotNull(serviceExp.rest().getQuery(), "SERVICE runtime expects a query body"); + } + + @Test + @DisplayName("BGP ?s ?p ?o — variable predicate exposed via getEdgeVariable()") + void edgeExposesVariablePredicateAsEdgeVariable() { + Exp bgp = new WhereCompiler().compile(new BgpAst(List.of( + new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o"))))); + + Exp edgeExp = bgp.get(0); + assertNotNull(edgeExp.getEdge().getEdgeVariable(), "variable predicate should be exposed as edge variable"); + assertTrue(edgeExp.getEdge().getEdgeVariable().isVariable()); + } + + @Test + @DisplayName("BIND propagates the filter's isFunctional flag (e.g. unnest)") + void bindShouldPropagateFunctionalFlag() { + BindAst bind = new BindAst( + new FunctionCallAst(new IriAst(""), List.of(new VarAst("x"))), + new VarAst("y")); + + Exp bindExp = new WhereCompiler().compile(bind); + + assertTrue(bindExp.getFilter().isFunctional()); + assertTrue(bindExp.isFunctional(), "BIND exp should propagate functional flag"); + } + + @Test + @DisplayName("SERVICE SILENT sets the isSilent flag") void compilesServiceSilent() { ServiceAst service = new ServiceAst( new IriAst(""), @@ -135,6 +176,6 @@ void compilesServiceSilent() { Exp serviceExp = compiler.compile(service); assertTrue(serviceExp.isService()); - assertTrue(serviceExp.isSilent(), "flag SILENT activé"); + assertTrue(serviceExp.isSilent(), "SILENT flag enabled"); } } \ No newline at end of file From a90e575aa76416a1418cd9df4e3dd23f7eb7126c Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 25 Jun 2026 15:06:10 +0200 Subject: [PATCH 4/4] #384 [BRIDGE] WHERE Compiler --- .../impl/sparql/bridge/AstBackedEdge.java | 14 ++++++++-- .../impl/sparql/bridge/AstBackedEdgeTest.java | 26 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java index 3d99e24ac..976165ecf 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java @@ -14,11 +14,17 @@ final class AstBackedEdge implements Edge { private final Node subject; private final Node predicate; private final Node object; + private final Node graph; AstBackedEdge(Node subject, Node predicate, Node object) { + this(subject, predicate, object, null); + } + + AstBackedEdge(Node subject, Node predicate, Node object, Node graph) { this.subject = Objects.requireNonNull(subject, "subject"); this.predicate = Objects.requireNonNull(predicate, "predicate"); this.object = Objects.requireNonNull(object, "object"); + this.graph = graph; } @Override @@ -46,11 +52,15 @@ public String getEdgeLabel() { return predicate.getLabel(); } + /** + * The graph this triple pattern is matched in, or {@code null} for the default graph. + * + */ @Override public Node getGraph() { - return null; + return graph; } - + @Override public boolean contains(Node node) { if (node == null) { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java index 709355723..4bf4f3ac4 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*; -@DisplayName("AstBackedEdge: contains() and getEdgeVariable()") +@DisplayName("AstBackedEdge: contains(), getEdgeVariable() and getGraph()") class AstBackedEdgeTest { @Test @@ -63,4 +63,28 @@ void edgeVariableExposedOnlyForVariablePredicate() { new NodeImpl(Variable.create("o"))); assertNull(withIriPredicate.getEdgeVariable()); } + + @Test + @DisplayName("getGraph() is null for the default graph (engine represents it as null)") + void defaultGraphIsNull() { + AstBackedEdge edge = new AstBackedEdge( + new NodeImpl(Variable.create("s")), + new NodeImpl(Variable.create("p")), + new NodeImpl(Variable.create("o"))); + + assertNull(edge.getGraph()); + } + + @Test + @DisplayName("getGraph() returns the graph node passed to the constructor") + void explicitGraphIsReturned() { + Node graph = new NodeImpl(Constant.createResource("http://example.org/g")); + AstBackedEdge edge = new AstBackedEdge( + new NodeImpl(Variable.create("s")), + new NodeImpl(Variable.create("p")), + new NodeImpl(Variable.create("o")), + graph); + + assertSame(graph, edge.getGraph()); + } } \ No newline at end of file