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 @@ -66,6 +66,22 @@ public abstract class SparqlAstBuilder {
*/
private final Set<String> blankNodeLabels = new HashSet<>();

/** Unique IDs for basic graph patterns, including those in nested groups. */
private int bgpScopeCounter;

/**
* Current BGP scope at each group depth. FILTER preserves the enclosing scope,
* including when its expression contains EXISTS or NOT EXISTS. Other graph
* patterns, BIND, and inline VALUES end the preceding BGP.
*/
private final Deque<Integer> bgpScopeIdStack = new ArrayDeque<>();

/**
* First BGP using each explicit label. Shared across subqueries and update
* WHERE clauses, but excludes CONSTRUCT and update templates.
*/
private final Map<String, Integer> blankNodeLabelToBgpScope = new HashMap<>();

/**
* Stack of currently open SELECT operations (top-level SELECT and nested SELECT subqueries).
*/
Expand Down Expand Up @@ -345,6 +361,7 @@ public void enterWhereClause() {
*/
public void enterGroup() {
groupStack.push(new ArrayList<>());
bgpScopeIdStack.push(++bgpScopeCounter);
if (selectWhereGroupFollows && hasCurrentSelect()) {
selectWhereGroupDepths.push(groupStack.size());
selectWhereGroupFollows = false;
Expand All @@ -369,11 +386,13 @@ && hasCurrentSelect()
&& depthBeforePop == selectWhereGroupDepths.peek()) {
selectWhereGroupDepths.pop();
List<PatternAst> popped = groupStack.pop();
bgpScopeIdStack.pop();
getCurrentSelectFrame().whereClause = new GroupGraphPatternAst(popped);
return;
}

List<PatternAst> popped = groupStack.pop();
bgpScopeIdStack.pop();
GroupGraphPatternAst group = new GroupGraphPatternAst(popped);
appendClosedGroup(group);
}
Expand All @@ -388,6 +407,8 @@ private void appendClosedGroup(GroupGraphPatternAst group) {
} else if (!existsGroupDepths.isEmpty() && groupStack.size() == existsGroupDepths.peek()) {
existsGroupDepths.pop();
capturedExistsStack.push(group);
// An expression's graph pattern does not end the enclosing BGP.
return;
} else if (!serviceStack.isEmpty() && groupStack.size() == serviceStack.peek().groupDepth()) {
ServiceEntry entry = serviceStack.pop();
currentGroup().add(new ServiceAst(entry.endpoint(), entry.silent(), group));
Expand All @@ -397,9 +418,19 @@ private void appendClosedGroup(GroupGraphPatternAst group) {
} else if (groupStack.isEmpty()) {
if (hasCurrentSelect()) getCurrentSelectFrame().whereClause = group;
else whereClause = group;
return;
} else {
currentGroup().add(group);
}
renewBgpScope();
}

/** Starts a fresh BGP after a non-FILTER graph pattern in the current group. */
private void renewBgpScope() {
if (!bgpScopeIdStack.isEmpty()) {
bgpScopeIdStack.pop();
bgpScopeIdStack.push(++bgpScopeCounter);
}
}

/**
Expand Down Expand Up @@ -458,6 +489,7 @@ public void addInlineValues(ValuesAst values) {
throw new IllegalStateException("addInlineValues() called outside of a group graph pattern");
}
currentGroup().add(values);
renewBgpScope();
}

/**
Expand All @@ -483,6 +515,7 @@ public void addBind(BindAst bind) {
"Variable ?" + name + " used in BIND is already declared in the same group graph pattern");
}
group.add(bind);
renewBgpScope();
}

// --- Optional ---
Expand Down Expand Up @@ -656,6 +689,18 @@ public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) {
}
String label = ctx.getText();
blankNodeLabels.add(label);
// Templates are built outside graph-pattern groups. Their labels are
// independent of WHERE labels; update-template restrictions are checked
// separately by UpdateTemplateValidator. CONSTRUCT WHERE opens a group
// explicitly, so its abbreviated WHERE still participates in this check.
if (!bgpScopeIdStack.isEmpty()) {
Integer currentScope = bgpScopeIdStack.peek();
Integer registeredScope = blankNodeLabelToBgpScope.putIfAbsent(label, currentScope);
if (registeredScope != null && !registeredScope.equals(currentScope)) {
throw new QuerySyntaxException(
"Blank node label '" + label + "' is used in more than one basic graph pattern");
}
}
return this.iri(label);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package fr.inria.corese.core.next.query.impl.sparql.parser.listener;

import fr.inria.corese.core.next.common.text.RdfText;
import fr.inria.corese.core.next.generated.antlr.SparqlParser;
import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlAstBuilder;
import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlQueryAstBuilder;
import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VariableScopeAnalyzer;
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -83,23 +86,29 @@ private void extractProjection(SparqlParser.SelectClauseContext ctx) {
queryBuilder().setProjectionAll();
return;
}
List<String> allVars = new ArrayList<>();
Set<String> projectedVariables = new LinkedHashSet<>();
List<String> expressionBoundVars = new ArrayList<>();
Map<String, TermAst> expressionTerms = new LinkedHashMap<>();
Map<String, Set<String>> expressionReferencedVariables = new LinkedHashMap<>();
for (SparqlParser.SelectVarContext selectVar : ctx.selectVar()) {
if (selectVar.expression() != null) {
// (expr AS ?var) — introduces a new variable, not projected from WHERE
String varName = selectVar.var_().getText();
allVars.add(varName);
String varName = RdfText.stripVariableMarker(selectVar.var_().getText());
if (!projectedVariables.add(varName)) {
throw new QuerySyntaxException(
"Variable ?" + varName + " introduced by SELECT expression is already projected");
}
expressionBoundVars.add(varName);
TermAst expressionAst = builder().termFromExpression(selectVar.expression());
expressionTerms.put(varName, expressionAst);
expressionReferencedVariables.put(varName, variableScopeAnalyzer.collectReferencedVariables(expressionAst));
} else if (selectVar.var_() != null) {
allVars.add(selectVar.var_().getText());
String varName = RdfText.stripVariableMarker(selectVar.var_().getText());
// SPARQL 1.1 section 18.2.4.4 accumulates plain variables in a set.
// A later plain reference to an earlier alias is also permitted.
projectedVariables.add(varName);
}
}
queryBuilder().setProjectionVariables(allVars, expressionBoundVars, expressionTerms, expressionReferencedVariables);
queryBuilder().setProjectionVariables(new ArrayList<>(projectedVariables), expressionBoundVars, expressionTerms, expressionReferencedVariables);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package fr.inria.corese.core.next.query.impl.sparql.execution;

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.assertTrue;

import java.util.List;

import fr.inria.corese.core.next.data.api.term.BNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/** Result-level regressions for blank-node scopes and SELECT projection sets. */
class NextSparqlPipelineScopeExecutorTest extends PipelineTestSupport {

@ParameterizedTest
@ValueSource(strings = { "?x ?x", "?x $x", "$x ?x" })
void repeatedProjectionProducesOneColumn(String projection) {
try (var result = executor.evaluateTuple("SELECT " + projection + " WHERE { VALUES ?x { 1 } }")) {
assertEquals(List.of("x"), result.getBindingNames());
assertTrue(result.hasNext());
assertEquals("1", result.next().getValue("x").stringValue());
assertFalse(result.hasNext());
}
}

@Test
void projectedAliasMayBeReferencedAgain() {
try (var result = executor.evaluateTuple("SELECT (1 AS ?x) $x (?x + 1 AS ?y) WHERE {}")) {
assertEquals(List.of("x", "y"), result.getBindingNames());
assertTrue(result.hasNext());
var row = result.next();
assertEquals("1", row.getValue("x").stringValue());
assertEquals("2", row.getValue("y").stringValue());
assertFalse(result.hasNext());
}
}

@ParameterizedTest
@ValueSource(strings = {
"FILTER EXISTS { ?s <http://example.org/knows> ?o }",
"FILTER NOT EXISTS { ?s <urn:missing> ?o }"
})
void blankNodeStillJoinsTriplesAcrossExistsFilter(String filter) {
insert(iri(ALICE), iri(NAME), valueFactory.createLiteral("Alice"));
insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob"));
try (var result = executor.evaluateTuple("""
SELECT ?name WHERE {
_:a <http://example.org/knows> ?friend .
%s
_:a <http://example.org/name> ?name
}
""".formatted(filter))) {
assertTrue(result.hasNext());
assertEquals("Alice", result.next().getValue("name").stringValue());
assertFalse(result.hasNext(), "The same blank-node label must join on the same subject");
}
}

@Test
void constructLabelCreatesAFreshNodeInsteadOfReusingTheWhereBinding() {
try (var result = executor.evaluateGraph("""
CONSTRUCT { _:a <urn:friend> ?friend }
WHERE { _:a <http://example.org/knows> ?friend }
""")) {
assertTrue(result.hasNext());
var triple = result.next();
assertInstanceOf(BNode.class, triple.getSubject());
assertEquals(iri("urn:friend"), triple.getPredicate());
assertEquals(iri(BOB), triple.getObject());
assertFalse(result.hasNext());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package fr.inria.corese.core.next.query.impl.sparql.parser;

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

import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/** SPARQL 1.1 sections 5.1.1, 16.2.1, 18.2.2, and 19.6. */
class SparqlParserBlankNodeScopeTest {

private final SparqlParser parser = new SparqlParser();

@ParameterizedTest
@ValueSource(strings = {
"",
"FILTER (?x != ?y)",
"FILTER EXISTS { ?s ?p ?o }",
"FILTER NOT EXISTS { ?s ?p ?o }",
"FILTER (EXISTS { ?s ?p ?o } && NOT EXISTS { ?t ?q ?r })",
"FILTER EXISTS { _:inner <urn:p> ?v . OPTIONAL { ?s ?p ?o } }",
"FILTER EXISTS { _:inner <urn:p> ?v . FILTER EXISTS { ?s ?p ?o } _:inner <urn:q> ?w }"
})
void filtersPreserveTheEnclosingBasicGraphPattern(String filter) {
String query = "SELECT * WHERE { _:a <urn:p> ?x . " + filter + " _:a <urn:q> ?y }";
assertDoesNotThrow(() -> parser.parse(query));
assertTrue(parser.validate(query).isValid());
}

@ParameterizedTest
@ValueSource(strings = {
"{}",
"{ ?s ?p ?o }",
"OPTIONAL { ?s ?p ?o }",
"MINUS { ?s ?p ?o }",
"GRAPH <urn:g> { ?s ?p ?o }",
"SERVICE <urn:service> { ?s ?p ?o }",
"{ ?s ?p ?o } UNION { ?t ?q ?r }",
"{ SELECT ?s WHERE { ?s ?p ?o } }",
"BIND (1 AS ?z)",
"BIND (EXISTS { ?s ?p ?o } AS ?z)",
"VALUES ?z { 1 }"
})
void nonFilterPatternsEndThePrecedingBasicGraphPattern(String pattern) {
assertInvalidLabelReuse("SELECT * WHERE { _:a <urn:p> ?x . " + pattern + " _:a <urn:q> ?y }");
}

@ParameterizedTest
@ValueSource(strings = {
"SELECT * WHERE { _:a <urn:p> ?x . OPTIONAL { _:a <urn:q> ?y } }",
"SELECT * WHERE { _:a <urn:p> ?x . FILTER EXISTS { _:a <urn:q> ?y } }",
"SELECT * WHERE { _:a <urn:p> ?x . FILTER NOT EXISTS { _:a <urn:q> ?y } }",
"SELECT * WHERE { FILTER EXISTS { _:a <urn:p> ?x } _:a <urn:q> ?y }",
"SELECT * WHERE { { _:a <urn:p> ?x } UNION { _:a <urn:q> ?y } }",
"SELECT * WHERE { { SELECT ?x WHERE { _:a <urn:p> ?x } } _:a <urn:q> ?y }",
"SELECT * WHERE { GRAPH <urn:g> { _:a <urn:p> ?x } GRAPH <urn:h> { _:a <urn:q> ?y } }",
"CONSTRUCT { _:a <urn:p> ?x } WHERE { _:a <urn:p> ?x . OPTIONAL { _:a <urn:q> ?y } }",
"INSERT {} WHERE { _:a <urn:p> ?x }; INSERT {} WHERE { _:a <urn:q> ?y }"
})
void labelsCannotBeSharedByDistinctBasicGraphPatterns(String query) {
assertInvalidLabelReuse(query);
}

@ParameterizedTest
@ValueSource(strings = {
"CONSTRUCT { _:a <urn:p> ?x . _:a <urn:q> ?x } WHERE { _:a <urn:r> ?x }",
"CONSTRUCT WHERE { _:a <urn:p> ?x . _:a <urn:q> ?y }",
"INSERT { _:a <urn:p> ?x } WHERE { _:a <urn:q> ?x }",
"INSERT { _:a <urn:p> ?x } WHERE { ?s <urn:q> ?x }; INSERT { _:a <urn:q> ?x } WHERE { ?s <urn:p> ?x }",
"INSERT DATA { _:a <urn:p> 1 . GRAPH <urn:g> { _:a <urn:q> 2 } }",
"SELECT * WHERE { [] <urn:p> ?x . OPTIONAL { [] <urn:q> ?y } }",
"SELECT * WHERE { _:a <urn:p> ?x . OPTIONAL { _:b <urn:q> ?y } _:c <urn:r> ?z }"
})
void templatesAndAnonymousNodesHaveIndependentScopes(String query) {
assertDoesNotThrow(() -> parser.parse(query));
}

@Test
void insertDataStillRejectsLabelsReusedAcrossOperations() {
assertThrows(QuerySyntaxException.class, () -> parser.parse(
"INSERT DATA { _:a <urn:p> 1 }; INSERT DATA { _:a <urn:q> 2 }"));
}

@Test
void scopeStateDoesNotLeakBetweenParserInvocations() {
String query = "SELECT * WHERE { _:a <urn:p> ?x }";
assertDoesNotThrow(() -> parser.parse(query));
assertInvalidLabelReuse("SELECT * WHERE { _:a <urn:p> ?x . OPTIONAL { _:a <urn:q> ?y } }");
assertDoesNotThrow(() -> parser.parse(query));
}

private void assertInvalidLabelReuse(String query) {
QuerySyntaxException error = assertThrows(QuerySyntaxException.class, () -> parser.parse(query));
assertTrue(error.getMessage().contains("Blank node label '_:a'"), error.getMessage());
assertFalse(parser.validate(query).isValid());
}
}
Loading
Loading