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
Expand Up @@ -287,7 +287,7 @@ private void applyProjection(Query query, ProjectionAst projection) {
} else {
selectExpressions = new ArrayList<>();
for (VarAst variable : projection.variables()) {
Node node = query.getExtNode(variable.name());
Node node = visibleBodyNode(query, variable.name());
if (node == null) {
throw new IllegalArgumentException(
"Projected variable ?" + variable.name() + " is not visible in the compiled query body");
Expand All @@ -313,7 +313,7 @@ private List<Node> describeNodes(Query query, DescribeQueryAst describeQueryAst)
List<Node> nodes = new ArrayList<>();
for (TermAst term : describeQueryAst.described()) {
if (term instanceof VarAst(String name)) {
Node node = query.getExtNode(name);
Node node = visibleBodyNode(query, name);
if (node == null) {
throw new IllegalArgumentException(
"DESCRIBE variable ?" + name + " is not visible in the compiled query body");
Expand Down Expand Up @@ -396,7 +396,7 @@ private Exp toOrderByExpression(Query query, OrderConditionAst orderCondition, i
Exp selectExpression = query.getSelectExp(name);
Node node = selectExpression != null
? selectExpression.getNode()
: query.getProperAndSubSelectNode(name);
: visibleBodyNode(query, name);
if (node == null) {
throw new IllegalArgumentException(
"ORDER BY variable ?" + name + " is not visible in the compiled query");
Expand Down Expand Up @@ -429,6 +429,20 @@ private List<Node> selectNodeList(List<Exp> selectExpressions) {
return new ArrayList<>(selectNodes);
}

/**
* Resolves only variables visible from the outer body scope. This deliberately
* excludes nodes collected from MINUS/EXISTS bodies, which KGRAM stores as query
* nodes for internal evaluation but which are not projectable SPARQL bindings.
*/
private Node visibleBodyNode(Query query, String name) {
for (Node node : query.selectNodesFromPattern()) {
if (node.getLabel().equals(name)) {
return node;
}
}
return null;
}

private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier) {
if (solutionModifier.hasLimit()) {
query.setLimit(Math.toIntExact(solutionModifier.limit()));
Expand Down Expand Up @@ -476,7 +490,7 @@ private Exp compileConstructTemplate(Query query, ConstructTemplateAst template)
*/
private Node constructNode(Query query, TermAst term) {
if (term instanceof VarAst(String name)) {
Node bound = query.getExtNode(name);
Node bound = visibleBodyNode(query, name);
return bound != null ? bound : toNode(term);
}
return toNode(term);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ private static GroupGraphPatternAst whereSPO() {
return new GroupGraphPatternAst(List.of(new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))))));
}

private static GroupGraphPatternAst whereWithMinusOnlyZ() {
return new GroupGraphPatternAst(List.of(
new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))),
new MinusAst(new GroupGraphPatternAst(List.of(
new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("q"), new VarAst("z")))))))));
}

private static ConstructTemplateAst template(TriplePatternAst... triples) {
return new ConstructTemplateAst(List.of(triples));
}
Expand Down Expand Up @@ -93,6 +100,22 @@ void templateOnlyVariableStaysFresh() {
assertTrue(object.isVariable(), "template-only variable is still a variable node");
}

@Test
@DisplayName("Template variable that only occurs in MINUS stays fresh")
void templateMinusOnlyVariableStaysFresh() {
ConstructQueryAst construct = new ConstructQueryAst(
template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("z"))),
whereWithMinusOnlyZ());

Query query = builder.toNextQuery(construct);

Node minusNode = query.getQueryNode("z");
Node templateNode = query.getConstruct().first().getEdge().getNode(1);
assertNotNull(minusNode, "MINUS-only variable is stored as an internal query node");
assertNotSame(minusNode, templateNode, "template ?z must not reuse the internal MINUS node");
assertTrue(templateNode.isVariable(), "template-only ?z remains a variable node");
}

@Test
@DisplayName("Template IRIs become fixed (non-variable) nodes")
void templateWithFixedResources() {
Expand Down Expand Up @@ -146,4 +169,4 @@ void rejectsValuesClause() {
void rejectsNull() {
assertThrows(NullPointerException.class, () -> builder.toNextQuery((ConstructQueryAst) null));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ private static GroupGraphPatternAst whereBindingX() {
new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))))));
}

private static GroupGraphPatternAst whereWithMinusOnlyZ() {
return new GroupGraphPatternAst(List.of(
new BgpAst(List.of(
new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o")))),
new MinusAst(new GroupGraphPatternAst(List.of(
new BgpAst(List.of(
new TriplePatternAst(new VarAst("x"), new VarAst("q"), new VarAst("z")))))))));
}

@Test
@DisplayName("Parser -> DescribeQueryAst -> Query lowers DESCRIBE to a construct-like query")
void parserDescribeAstToQuery() {
Expand Down Expand Up @@ -97,6 +106,15 @@ void rejectsDescribeVariableNotVisible() {
assertThrows(IllegalArgumentException.class, () -> builder.toNextQuery(describe));
}

@Test
@DisplayName("DESCRIBE rejects a variable that only occurs in MINUS")
void rejectsDescribeMinusOnlyVariable() {
DescribeQueryAst describe = new DescribeQueryAst(
DatasetClauseAst.none(), List.of(new VarAst("z")), whereWithMinusOnlyZ());

assertThrows(IllegalArgumentException.class, () -> builder.toNextQuery(describe));
}

@Test
@DisplayName("FROM / FROM NAMED are applied to the query dataset")
void appliesDatasetClause() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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.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;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CoreseAstQueryBuilderMinusScopeTest {

private final WhereCompiler compiler = new WhereCompiler();

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

private static boolean hasLabel(List<Node> nodes, String label) {
return nodes.stream().anyMatch(n -> label.equals(n.getLabel()));
}

@Test
@DisplayName("{ ?s ?p ?o . MINUS { ?s ?q ?z } } : ?z (MINUS-only) is a query node, not a pattern node")
void minusRightSideVariablesStayOutOfOuterScope() {
Exp body = compiler.compile(new GroupGraphPatternAst(List.of(
bgp("s", "p", "o"),
new MinusAst(new GroupGraphPatternAst(List.of(bgp("s", "q", "z")))))));

Query query = Query.create(body);
query.collect();

// WHERE pattern variables are in scope (pattern nodes)
assertTrue(hasLabel(query.getPatternNodes(), "s"), "?s from WHERE is a pattern node");
assertTrue(hasLabel(query.getPatternNodes(), "o"), "?o from WHERE is a pattern node");

// MINUS-only variable ?z binds nothing outside: it must NOT be a pattern node
assertFalse(hasLabel(query.getPatternNodes(), "z"),
"?z appears only in the MINUS body and must not leak into the outer pattern scope");

// it is collected in the query (minus/exists) node list instead
assertTrue(hasLabel(query.getQueryNodes(), "z"),
"?z from the MINUS body is stored as a query node (minus scope), stored separately");
}

@Test
@DisplayName("{ ?s ?p ?o . OPTIONAL { ?s ?q ?z } } : ?z (OPTIONAL-only) is a pattern node visible in the outer scope")
void optionalVariablesAreInOuterScope() {
Exp body = compiler.compile(new GroupGraphPatternAst(List.of(
bgp("s", "p", "o"),
new OptionalAst(new GroupGraphPatternAst(List.of(bgp("s", "q", "z")))))));

Query query = Query.create(body);
query.collect();

assertTrue(hasLabel(query.getPatternNodes(), "s"), "?s from WHERE is a pattern node");
assertTrue(hasLabel(query.getPatternNodes(), "o"), "?o from WHERE is a pattern node");
// OPTIONAL variables ARE in the outer scope (unlike MINUS)
assertTrue(hasLabel(query.getPatternNodes(), "z"),
"?z from OPTIONAL body must be visible in the outer pattern scope");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import fr.inria.corese.core.next.query.impl.sparql.ast.DatasetClauseAst;
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;
import fr.inria.corese.core.next.query.impl.sparql.ast.OrderConditionAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.ProjectionAsts;
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst;
Expand All @@ -23,6 +25,7 @@
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -73,6 +76,43 @@ void buildsExplicitProjection() {
assertEquals(List.of("o", "s"), labels(query.getSelect()));
}

@Test
@DisplayName("SELECT * excludes variables that only occur in MINUS")
void selectStarExcludesMinusOnlyVariables() {
SelectQueryAst select = parseSelect("SELECT * WHERE { ?s ?p ?o . MINUS { ?s ?q ?z } }");

Query query = builder.toNextQuery(select);

assertEquals(List.of("s", "p", "o"), labels(query.getSelect()),
"MINUS-only variables are stored separately and must not be visible to SELECT *");
assertFalse(labels(query.getSelect()).contains("z"));
}

@Test
@DisplayName("Explicit projection rejects a variable that only occurs in MINUS")
void explicitProjectionRejectsMinusOnlyVariable() {
SelectQueryAst select = new SelectQueryAst(
ProjectionAsts.of(List.of(new VarAst("z"))),
null,
group(bgp("s", "p", "o"), new MinusAst(group(bgp("s", "q", "z")))));

IllegalArgumentException error =
assertThrows(IllegalArgumentException.class, () -> builder.toNextQuery(select));

assertEquals("Projected variable ?z is not visible in the compiled query body", error.getMessage());
}

@Test
@DisplayName("SELECT * includes variables introduced by OPTIONAL")
void selectStarIncludesOptionalVariables() {
SelectQueryAst select = parseSelect("SELECT * WHERE { ?s ?p ?o . OPTIONAL { ?s ?q ?z } }");

Query query = builder.toNextQuery(select);

assertEquals(List.of("s", "p", "o", "q", "z"), labels(query.getSelect()),
"OPTIONAL variables remain visible in the outer query scope");
}

@Test
@DisplayName("DISTINCT, LIMIT and OFFSET are copied onto the next Query")
void appliesSimpleSolutionModifiers() {
Expand Down Expand Up @@ -134,6 +174,35 @@ void appliesExpressionOrderBy() {
"non-variable ORDER BY expressions should be carried by a runtime filter");
}

@Test
@DisplayName("ORDER BY rejects a variable that only occurs in MINUS")
void orderByRejectsMinusOnlyVariable() {
SelectQueryAst select = new SelectQueryAst(
ProjectionAsts.selectAll(),
null,
group(bgp("s", "p", "o"), new MinusAst(group(bgp("s", "q", "z")))),
SolutionModifierAst.withoutGroupBy(false, false,
List.of(new OrderConditionAst(ASTConstants.OrderDirection.ASC, new VarAst("z"))),
null,
null));

IllegalArgumentException error =
assertThrows(IllegalArgumentException.class, () -> builder.toNextQuery(select));

assertEquals("ORDER BY variable ?z is not visible in the compiled query", error.getMessage());
}

@Test
@DisplayName("ORDER BY accepts a variable introduced by OPTIONAL")
void orderByAcceptsOptionalVariable() {
SelectQueryAst select = parseSelect("SELECT * WHERE { ?s ?p ?o . OPTIONAL { ?s ?q ?z } } ORDER BY ?z");

Query query = builder.toNextQuery(select);

assertEquals(1, query.getOrderBy().size());
assertEquals("z", query.getOrderBy().getFirst().getNode().getLabel());
}

@Test
@DisplayName("FROM and FROM NAMED are copied onto the next Query dataset")
void appliesDatasetClauses() {
Expand Down Expand Up @@ -197,6 +266,19 @@ private static GroupGraphPatternAst group(TriplePatternAst triple) {
return new GroupGraphPatternAst(List.of(new BgpAst(List.of(triple))));
}

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

private SelectQueryAst parseSelect(String query) {
return assertInstanceOf(SelectQueryAst.class, newParserDefault().parse(query));
}

private static List<String> labels(List<Node> nodes) {
return nodes.stream().map(Node::getLabel).toList();
}
Expand Down
Loading
Loading