Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AstBackedEdge currently inherits the default Edge.contains(Node) implementation, which always returns false. For query edges we probably need contains() to match subject/object, otherwise some runtime logic may not see that this pattern carries these variables.

@Test
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));
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For variable predicates, AstBackedEdge does not expose the predicate through getEdgeVariable(). next runtime uses edge.getEdgeVariable() in query bookkeeping, so a pattern like ?s ?p ?o may not register ?p the same way as existing query edges do.

@Test
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());
}

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
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 Node getEdgeVariable() {
return predicate.isVariable() ? predicate : null;
}

@Override
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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it return something ? either the default graph or the encompassing graph

SELECT * {
<a> <b> <c>
}

The <a> <b> <c> edge graph is either null of DefaultGraphURI

SELECT * {
GRAPH <g> {
<a> <b> <c>
}
}

The <a> <b> <c> edge graph is <g>

return graph;
}

@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() {
return this;
}

@Override
public String toString() {
return subject + " " + predicate + " " + object;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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.core.Query;
import fr.inria.corese.core.next.query.kgram.tool.NodeImpl;
import fr.inria.corese.core.sparql.triple.parser.Atom;
import fr.inria.corese.core.sparql.triple.parser.Expression;

import 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.setFunctional(filter.isFunctional());
exp.setNode(variable);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BIND does not propagate the filter functional flag here. For Corese-specific expressions such as unnest or sql (not standard SPARQL), filter.isFunctional() becomes true, but exp.isFunctional() stays false, so runtime may take the non-functional execution path.

@Test
void bindShouldPropagateFunctionalFlag() {
    BindAst bind = new BindAst(
            new FunctionCallAst(new IriAst("<http://example.org/unnest>"), 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");
}

return exp;
}

/**
* Compiles {@code SERVICE <endpoint> { ... }} into a KGRAM {@link Exp}.
*/
private Exp compileService(ServiceAst service) {
Node endpoint = toNode(service.endpoint());
Exp endpointNode = Exp.create(Type.NODE, endpoint);
Query body = Query.create(compile(service.pattern()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SERVICE now wraps its body in a Query, which fixes exp.rest().getQuery(), but the body query is still not marked as a service query. In the legacy compiler we explicitly call q.setService(true), and that matters because non-service queries may inherit outer FROM / NAMED clauses. So the current shape may still execute SERVICE with the wrong dataset later. For consistency with the legacy compiler, it may also be worth propagating the SILENT flag to the body query.

@Test
void serviceBodyShouldBeMarkedAsServiceQuery() {
    ServiceAst service = new ServiceAst(
            new IriAst("<http://example.org/>"),
            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.rest().getQuery().isService(),
            "SERVICE body query should be marked as service");
}
private Exp compileService(ServiceAst service) {
    Node endpoint = toNode(service.endpoint());
    Exp endpointNode = Exp.create(Type.NODE, endpoint);
    Query body = Query.create(compile(service.pattern()));
    body.setService(true);
    body.setSilent(service.silent());
    Exp exp = Exp.create(Type.SERVICE, endpointNode, body);
    exp.setSilent(service.silent());
    return exp;
}

Exp exp = Exp.create(Type.SERVICE, endpointNode, body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SERVICE is currently compiled with a plain AND body. However, next runtime seems to execute SERVICE through exp.rest().getQuery(), which is null for an AND. So the current structure looks valid in the unit test, but may not match the runtime contract expected for service execution.

Replace SERVICE(endpoint, AND(...)) by SERVICE(endpoint, QUERY(...))

@Test
void serviceRestShouldExposeAQueryForRuntime() {
    ServiceAst service = new ServiceAst(
            new IriAst("<http://example.org/>"),
            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");
}

exp.setSilent(service.silent());
return exp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,11 @@ public boolean isRecAggregate() {
if (isAggregate(getLabel())) {
return true;
}
for (Expr exp : getExpList()) {
List<Expr> expList = getExpList();
if (expList == null) {
return false;
}
for (Expr exp : expList) {
if (exp.isRecAggregate()) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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(), getEdgeVariable() and getGraph()")
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());
}

@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());
}
}
Loading
Loading