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 @@ -126,7 +126,7 @@ public static String guessLocalName(String iri) {

/**
* Detects if an IRI is absolute according to the REGEX given in the recommendation RFC3987
* @param iri any uri (expecting to be the content between < and >
* @param iri any uri (expecting to be the content between &lt; and &gt;)
* @return true if it is compliant with RFC3987. May accept the prefixed for of uri, as there is no way to
* distinguish a prefix from a protocol
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,14 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S
return new ExistsAst(popCapturedExistsPattern());
} else if (ctx.notExistsFunc() != null) {
return new NotExistsAst(popCapturedExistsPattern());
} else if (ctx.expression() != null) {
} else if (ctx.regexExpression() != null) {
return termFromRegex(ctx.regexExpression());
} else if (ctx.BOUND() != null) {
return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText())));
} else if (ctx.CONCAT() != null) {
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
return new FunctionCallAst(new IriAst("CONCAT"), args);
} else if (ctx.expression() != null && !ctx.expression().isEmpty()) {
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
if (ctx.STR() != null) {
return this.createConstraint(ASTConstants.FUNCTION_CALL.STR, args);
Expand All @@ -1005,12 +1012,8 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S
return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, args);
} else if (ctx.IS_LITERAL() != null) {
return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, args);
} else if (ctx.BOUND() != null) {
return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText())));
} else if (ctx.regexExpression() != null) {
return termFromRegex(ctx.regexExpression());
} else {
throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText());
throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText());
}
} else {
throw new QueryEvaluationException("Unable to resolve BuiltInCall for token " + ctx.getText());
Expand Down Expand Up @@ -1237,4 +1240,26 @@ public List<TermAst> termListFromObjectListPath(SparqlParser.ObjectListPathConte
}
return out;
}

/**
* Adds a BIND clause to the current group.
*
* @throws QueryValidationException if the variable introduced by BIND is already visible
* in the group graph pattern up to this point, as required
* by the SPARQL 1.1 specification.
*/
public void addBind(BindAst bind) {
if (!this.hasCurrentGroup()) {
return;
}
List<PatternAst> current = this.currentGroup();
Set<String> alreadyVisible = variableScopeAnalyzer
.collectVisibleVariables(new GroupGraphPatternAst(current));
if (alreadyVisible.contains(bind.variable().name())) {
throw new QueryValidationException(
"Variable ?" + bind.variable().name()
+ " used in BIND is already declared in the same group graph pattern");
}
current.add(bind);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,9 @@ public void enterExistsFunc(SparqlParser.ExistsFuncContext ctx) {
public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) {
for (var d : delegates) d.enterNotExistsFunc(ctx);
}

@Override
public void exitBind(SparqlParser.BindContext ctx) {
for (var d : delegates) d.exitBind(ctx);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public QueryAst parse(Reader reader, String baseIRI) {
new UnionFeature(builder),
new DescribeQueryFeature(builder),
new DatasetClauseFeature(builder),
new PrologueFeature(builder)
new PrologueFeature(builder),
new BindFeature(builder)
));

walker.walk(listener, tree);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
package fr.inria.corese.core.next.query.impl.parser;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.FilterAst;
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.LiteralAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.OptionalAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst;
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.VarAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TrinaryRegexAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UnaryConstraintAst;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* Collects visible and referenced variables from the next SPARQL AST.
* A visible variable is introduced by a graph pattern in the WHERE clause and
Expand Down Expand Up @@ -92,6 +81,8 @@ case UnionAst(GroupGraphPatternAst left, GroupGraphPatternAst right) -> {
collectVisibleVariables(left, visibleVariables);
collectVisibleVariables(right, visibleVariables);
}
case BindAst(TermAst expression, VarAst variable) ->
visibleVariables.add(variable.name());

case FilterAst ignored -> {
// FILTER does not make a variable visible by itself.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package fr.inria.corese.core.next.query.impl.parser.listener;

import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser;
import fr.inria.corese.core.next.query.impl.parser.SparqlAstBuilder;
import fr.inria.corese.core.next.query.impl.sparql.ast.BindAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst;

/**
* SPARQL {@code BIND} feature
*/
public class BindFeature extends AbstractSparqlFeature {

public BindFeature(SparqlAstBuilder builder) {
super(builder);
}

@Override
public void exitBind(SparqlParser.BindContext ctx) {
TermAst expression = builder().termFromExpression(ctx.expression());
VarAst variable = (VarAst) builder().var(ctx.var_().getText());
builder().addBind(new BindAst(expression, variable));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.inria.corese.core.next.query.impl.sparql.ast;

/**
* BIND(expression AS ?var) clause in SPARQL 1.1
*/
public record BindAst(TermAst expression, VarAst variable) implements PatternAst {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.inria.corese.core.next.query.impl.sparql.ast;

/**
* Element of a group graph pattern (BGP, optional, union, etc.).
* Element of a group graph pattern (BGP, optional, union, Bind, etc.).
*/
public sealed interface PatternAst permits BgpAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst {
public sealed interface PatternAst permits BgpAst, BindAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Abstract Syntax Tree (AST) representation of a SPARQL {@code SELECT} query.
* Holds the projection (SELECT * or SELECT ?v1 ?v2 ...) and the WHERE clause.
* <p>
* {@link #prologue()} captures PREFIX/BASE for SELECT; {@link #prefixHandler()} is derived from it
* {@link #prologue()} captures PREFIX/BASE for SELECT;
* for {@link QueryAst} compatibility.
*/
public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, QueryPrologueAst prologue) implements QueryAst {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package fr.inria.corese.core.next.query.impl.parser;

import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("SPARQL 1.1 - Parser and AST : BIND")
class SparqlParserBindTest extends AbstractSparqlParserFeatureTest {

@Test
@DisplayName("BIND(?s AS ?x) — variable expression")
void shouldParseBindWithVariable() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(?s AS ?x)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

PatternAst last = where.patterns().getLast();
assertInstanceOf(BindAst.class, last);

BindAst bindAst = (BindAst) last;
assertInstanceOf(VarAst.class, bindAst.expression());
assertEquals("s", ((VarAst) bindAst.expression()).name());
assertEquals("x", bindAst.variable().name());
}

@Test
@DisplayName("BIND(CONCAT(?a, ?b) AS ?c) — built-in function expression")
void shouldParseBindWithConcat() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(CONCAT(?a, ?b) AS ?c)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

PatternAst last = where.patterns().getLast();
assertInstanceOf(BindAst.class, last);

BindAst bindAst = (BindAst) last;
assertInstanceOf(FunctionCallAst.class, bindAst.expression());
assertEquals("c", bindAst.variable().name());

FunctionCallAst concatAst = (FunctionCallAst) bindAst.expression();
assertEquals("CONCAT", ((IriAst) concatAst.functionName()).raw());
assertEquals(2, concatAst.arguments().size());
assertEquals("a", ((VarAst) concatAst.arguments().getFirst()).name());
assertEquals("b", ((VarAst) concatAst.arguments().getLast()).name());
}

@Test
@DisplayName("BIND(?x + 1 AS ?y) — arithmetic expression")
void shouldParseBindWithArithmetic() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(?x + 1 AS ?y)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

PatternAst last = where.patterns().getLast();
assertInstanceOf(BindAst.class, last);

BindAst bindAst = (BindAst) last;
assertEquals("y", bindAst.variable().name());
assertNotNull(bindAst.expression());
}

@Test
@DisplayName("BIND(\"hello\" AS ?label) — string literal expression")
void shouldParseBindWithStringLiteral() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND("hello" AS ?label)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

PatternAst last = where.patterns().getLast();
assertInstanceOf(BindAst.class, last);

BindAst bindAst = (BindAst) last;
assertInstanceOf(LiteralAst.class, bindAst.expression());
assertEquals("label", bindAst.variable().name());
assertEquals("\"hello\"", ((LiteralAst) bindAst.expression()).lexical());
}

@Test
@DisplayName("BIND(<http://example.org> AS ?type) — IRI expression")
void shouldParseBindWithIri() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(<http://example.org> AS ?type)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

PatternAst last = where.patterns().getLast();
assertInstanceOf(BindAst.class, last);

BindAst bindAst = (BindAst) last;
assertInstanceOf(IriAst.class, bindAst.expression());
assertEquals("type", bindAst.variable().name());
assertEquals("<http://example.org>", ((IriAst) bindAst.expression()).raw());
}

@Test
@DisplayName("Multiple BIND clauses in the same group")
void shouldParseMultipleBinds() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(?s AS ?x)
BIND(?p AS ?y)
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(3, where.patterns().size());

assertInstanceOf(BgpAst.class, where.patterns().get(0));

BindAst first = assertInstanceOf(BindAst.class, where.patterns().get(1));
assertEquals("x", first.variable().name());

BindAst second = assertInstanceOf(BindAst.class, where.patterns().get(2));
assertEquals("y", second.variable().name());
}

@Test
@DisplayName("BIND inside an OPTIONAL block")
void shouldParseBindInsideOptional() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
OPTIONAL { BIND(?s AS ?x) }
}
""");

assertNotNull(ast);
GroupGraphPatternAst where = ast.whereClause();
assertEquals(2, where.patterns().size());

assertInstanceOf(BgpAst.class, where.patterns().get(0));

OptionalAst optional = assertInstanceOf(OptionalAst.class, where.patterns().get(1));
GroupGraphPatternAst optionalGroup = assertInstanceOf(GroupGraphPatternAst.class, optional.ast());

assertEquals(1, optionalGroup.patterns().size());
BindAst bind = assertInstanceOf(BindAst.class, optionalGroup.patterns().getFirst());
assertEquals("x", bind.variable().name());
}

@Test
@DisplayName("SELECT ?x with BIND(... AS ?x) — BIND variable is visible in projection")
void shouldAcceptBindVariableInSelectProjection() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT ?x WHERE {
?s ?p ?o .
BIND(?s AS ?x)
}
""");

assertNotNull(ast);
SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast);
assertEquals(1, select.projection().variables().size());
assertEquals("x", select.projection().variables().getFirst().name());
}
}
Loading
Loading