Skip to content

Commit ee3643b

Browse files
Merge pull request #399 from corese-stack/feature/332-sparql-11-parser-and-ast-bind
Feature/332 sparql 11 parser and ast bind
2 parents ff0862e + aa1fbfc commit ee3643b

11 files changed

Lines changed: 328 additions & 27 deletions

File tree

src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public static String guessLocalName(String iri) {
126126

127127
/**
128128
* Detects if an IRI is absolute according to the REGEX given in the recommendation RFC3987
129-
* @param iri any uri (expecting to be the content between < and >
129+
* @param iri any uri (expecting to be the content between &lt; and &gt;)
130130
* @return true if it is compliant with RFC3987. May accept the prefixed for of uri, as there is no way to
131131
* distinguish a prefix from a protocol
132132
*/

src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,14 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S
987987
return new ExistsAst(popCapturedExistsPattern());
988988
} else if (ctx.notExistsFunc() != null) {
989989
return new NotExistsAst(popCapturedExistsPattern());
990-
} else if (ctx.expression() != null) {
990+
} else if (ctx.regexExpression() != null) {
991+
return termFromRegex(ctx.regexExpression());
992+
} else if (ctx.BOUND() != null) {
993+
return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText())));
994+
} else if (ctx.CONCAT() != null) {
995+
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
996+
return new FunctionCallAst(new IriAst("CONCAT"), args);
997+
} else if (ctx.expression() != null && !ctx.expression().isEmpty()) {
991998
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
992999
if (ctx.STR() != null) {
9931000
return this.createConstraint(ASTConstants.FUNCTION_CALL.STR, args);
@@ -1005,12 +1012,8 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S
10051012
return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, args);
10061013
} else if (ctx.IS_LITERAL() != null) {
10071014
return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, args);
1008-
} else if (ctx.BOUND() != null) {
1009-
return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText())));
1010-
} else if (ctx.regexExpression() != null) {
1011-
return termFromRegex(ctx.regexExpression());
10121015
} else {
1013-
throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText());
1016+
throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText());
10141017
}
10151018
} else {
10161019
throw new QueryEvaluationException("Unable to resolve BuiltInCall for token " + ctx.getText());
@@ -1237,4 +1240,26 @@ public List<TermAst> termListFromObjectListPath(SparqlParser.ObjectListPathConte
12371240
}
12381241
return out;
12391242
}
1243+
1244+
/**
1245+
* Adds a BIND clause to the current group.
1246+
*
1247+
* @throws QueryValidationException if the variable introduced by BIND is already visible
1248+
* in the group graph pattern up to this point, as required
1249+
* by the SPARQL 1.1 specification.
1250+
*/
1251+
public void addBind(BindAst bind) {
1252+
if (!this.hasCurrentGroup()) {
1253+
return;
1254+
}
1255+
List<PatternAst> current = this.currentGroup();
1256+
Set<String> alreadyVisible = variableScopeAnalyzer
1257+
.collectVisibleVariables(new GroupGraphPatternAst(current));
1258+
if (alreadyVisible.contains(bind.variable().name())) {
1259+
throw new QueryValidationException(
1260+
"Variable ?" + bind.variable().name()
1261+
+ " used in BIND is already declared in the same group graph pattern");
1262+
}
1263+
current.add(bind);
1264+
}
12401265
}

src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,9 @@ public void enterExistsFunc(SparqlParser.ExistsFuncContext ctx) {
189189
public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) {
190190
for (var d : delegates) d.enterNotExistsFunc(ctx);
191191
}
192+
193+
@Override
194+
public void exitBind(SparqlParser.BindContext ctx) {
195+
for (var d : delegates) d.exitBind(ctx);
196+
}
192197
}

src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ public QueryAst parse(Reader reader, String baseIRI) {
118118
new UnionFeature(builder),
119119
new DescribeQueryFeature(builder),
120120
new DatasetClauseFeature(builder),
121-
new PrologueFeature(builder)
121+
new PrologueFeature(builder),
122+
new BindFeature(builder)
122123
));
123124

124125
walker.walk(listener, tree);

src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
package fr.inria.corese.core.next.query.impl.parser;
22

3-
import java.util.LinkedHashSet;
4-
import java.util.List;
5-
import java.util.Set;
6-
7-
import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst;
8-
import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;
9-
import fr.inria.corese.core.next.query.impl.sparql.ast.FilterAst;
10-
import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst;
11-
import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst;
12-
import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst;
13-
import fr.inria.corese.core.next.query.impl.sparql.ast.OptionalAst;
14-
import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst;
15-
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
16-
import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst;
17-
import fr.inria.corese.core.next.query.impl.sparql.ast.UnionAst;
18-
import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst;
3+
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
194
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst;
205
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst;
216
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TrinaryRegexAst;
227
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UnaryConstraintAst;
238

9+
import java.util.LinkedHashSet;
10+
import java.util.List;
11+
import java.util.Set;
12+
2413
/**
2514
* Collects visible and referenced variables from the next SPARQL AST.
2615
* A visible variable is introduced by a graph pattern in the WHERE clause and
@@ -92,6 +81,8 @@ case UnionAst(GroupGraphPatternAst left, GroupGraphPatternAst right) -> {
9281
collectVisibleVariables(left, visibleVariables);
9382
collectVisibleVariables(right, visibleVariables);
9483
}
84+
case BindAst(TermAst expression, VarAst variable) ->
85+
visibleVariables.add(variable.name());
9586

9687
case FilterAst ignored -> {
9788
// FILTER does not make a variable visible by itself.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package fr.inria.corese.core.next.query.impl.parser.listener;
2+
3+
import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser;
4+
import fr.inria.corese.core.next.query.impl.parser.SparqlAstBuilder;
5+
import fr.inria.corese.core.next.query.impl.sparql.ast.BindAst;
6+
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
7+
import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst;
8+
9+
/**
10+
* SPARQL {@code BIND} feature
11+
*/
12+
public class BindFeature extends AbstractSparqlFeature {
13+
14+
public BindFeature(SparqlAstBuilder builder) {
15+
super(builder);
16+
}
17+
18+
@Override
19+
public void exitBind(SparqlParser.BindContext ctx) {
20+
TermAst expression = builder().termFromExpression(ctx.expression());
21+
VarAst variable = (VarAst) builder().var(ctx.var_().getText());
22+
builder().addBind(new BindAst(expression, variable));
23+
}
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package fr.inria.corese.core.next.query.impl.sparql.ast;
2+
3+
/**
4+
* BIND(expression AS ?var) clause in SPARQL 1.1
5+
*/
6+
public record BindAst(TermAst expression, VarAst variable) implements PatternAst {}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package fr.inria.corese.core.next.query.impl.sparql.ast;
22

33
/**
4-
* Element of a group graph pattern (BGP, optional, union, etc.).
4+
* Element of a group graph pattern (BGP, optional, union, Bind, etc.).
55
*/
6-
public sealed interface PatternAst permits BgpAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst {
6+
public sealed interface PatternAst permits BgpAst, BindAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst {
77
}

src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Abstract Syntax Tree (AST) representation of a SPARQL {@code SELECT} query.
77
* Holds the projection (SELECT * or SELECT ?v1 ?v2 ...) and the WHERE clause.
88
* <p>
9-
* {@link #prologue()} captures PREFIX/BASE for SELECT; {@link #prefixHandler()} is derived from it
9+
* {@link #prologue()} captures PREFIX/BASE for SELECT;
1010
* for {@link QueryAst} compatibility.
1111
*/
1212
public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, QueryPrologueAst prologue) implements QueryAst {
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package fr.inria.corese.core.next.query.impl.parser;
2+
3+
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
4+
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
@DisplayName("SPARQL 1.1 - Parser and AST : BIND")
11+
class SparqlParserBindTest extends AbstractSparqlParserFeatureTest {
12+
13+
@Test
14+
@DisplayName("BIND(?s AS ?x) — variable expression")
15+
void shouldParseBindWithVariable() {
16+
SparqlParser parser = newParserDefault();
17+
18+
QueryAst ast = parser.parse("""
19+
SELECT * WHERE {
20+
?s ?p ?o .
21+
BIND(?s AS ?x)
22+
}
23+
""");
24+
25+
assertNotNull(ast);
26+
GroupGraphPatternAst where = ast.whereClause();
27+
assertEquals(2, where.patterns().size());
28+
29+
PatternAst last = where.patterns().getLast();
30+
assertInstanceOf(BindAst.class, last);
31+
32+
BindAst bindAst = (BindAst) last;
33+
assertInstanceOf(VarAst.class, bindAst.expression());
34+
assertEquals("s", ((VarAst) bindAst.expression()).name());
35+
assertEquals("x", bindAst.variable().name());
36+
}
37+
38+
@Test
39+
@DisplayName("BIND(CONCAT(?a, ?b) AS ?c) — built-in function expression")
40+
void shouldParseBindWithConcat() {
41+
SparqlParser parser = newParserDefault();
42+
43+
QueryAst ast = parser.parse("""
44+
SELECT * WHERE {
45+
?s ?p ?o .
46+
BIND(CONCAT(?a, ?b) AS ?c)
47+
}
48+
""");
49+
50+
assertNotNull(ast);
51+
GroupGraphPatternAst where = ast.whereClause();
52+
assertEquals(2, where.patterns().size());
53+
54+
PatternAst last = where.patterns().getLast();
55+
assertInstanceOf(BindAst.class, last);
56+
57+
BindAst bindAst = (BindAst) last;
58+
assertInstanceOf(FunctionCallAst.class, bindAst.expression());
59+
assertEquals("c", bindAst.variable().name());
60+
61+
FunctionCallAst concatAst = (FunctionCallAst) bindAst.expression();
62+
assertEquals("CONCAT", ((IriAst) concatAst.functionName()).raw());
63+
assertEquals(2, concatAst.arguments().size());
64+
assertEquals("a", ((VarAst) concatAst.arguments().getFirst()).name());
65+
assertEquals("b", ((VarAst) concatAst.arguments().getLast()).name());
66+
}
67+
68+
@Test
69+
@DisplayName("BIND(?x + 1 AS ?y) — arithmetic expression")
70+
void shouldParseBindWithArithmetic() {
71+
SparqlParser parser = newParserDefault();
72+
73+
QueryAst ast = parser.parse("""
74+
SELECT * WHERE {
75+
?s ?p ?o .
76+
BIND(?x + 1 AS ?y)
77+
}
78+
""");
79+
80+
assertNotNull(ast);
81+
GroupGraphPatternAst where = ast.whereClause();
82+
assertEquals(2, where.patterns().size());
83+
84+
PatternAst last = where.patterns().getLast();
85+
assertInstanceOf(BindAst.class, last);
86+
87+
BindAst bindAst = (BindAst) last;
88+
assertEquals("y", bindAst.variable().name());
89+
assertNotNull(bindAst.expression());
90+
}
91+
92+
@Test
93+
@DisplayName("BIND(\"hello\" AS ?label) — string literal expression")
94+
void shouldParseBindWithStringLiteral() {
95+
SparqlParser parser = newParserDefault();
96+
97+
QueryAst ast = parser.parse("""
98+
SELECT * WHERE {
99+
?s ?p ?o .
100+
BIND("hello" AS ?label)
101+
}
102+
""");
103+
104+
assertNotNull(ast);
105+
GroupGraphPatternAst where = ast.whereClause();
106+
assertEquals(2, where.patterns().size());
107+
108+
PatternAst last = where.patterns().getLast();
109+
assertInstanceOf(BindAst.class, last);
110+
111+
BindAst bindAst = (BindAst) last;
112+
assertInstanceOf(LiteralAst.class, bindAst.expression());
113+
assertEquals("label", bindAst.variable().name());
114+
assertEquals("\"hello\"", ((LiteralAst) bindAst.expression()).lexical());
115+
}
116+
117+
@Test
118+
@DisplayName("BIND(<http://example.org> AS ?type) — IRI expression")
119+
void shouldParseBindWithIri() {
120+
SparqlParser parser = newParserDefault();
121+
122+
QueryAst ast = parser.parse("""
123+
SELECT * WHERE {
124+
?s ?p ?o .
125+
BIND(<http://example.org> AS ?type)
126+
}
127+
""");
128+
129+
assertNotNull(ast);
130+
GroupGraphPatternAst where = ast.whereClause();
131+
assertEquals(2, where.patterns().size());
132+
133+
PatternAst last = where.patterns().getLast();
134+
assertInstanceOf(BindAst.class, last);
135+
136+
BindAst bindAst = (BindAst) last;
137+
assertInstanceOf(IriAst.class, bindAst.expression());
138+
assertEquals("type", bindAst.variable().name());
139+
assertEquals("<http://example.org>", ((IriAst) bindAst.expression()).raw());
140+
}
141+
142+
@Test
143+
@DisplayName("Multiple BIND clauses in the same group")
144+
void shouldParseMultipleBinds() {
145+
SparqlParser parser = newParserDefault();
146+
147+
QueryAst ast = parser.parse("""
148+
SELECT * WHERE {
149+
?s ?p ?o .
150+
BIND(?s AS ?x)
151+
BIND(?p AS ?y)
152+
}
153+
""");
154+
155+
assertNotNull(ast);
156+
GroupGraphPatternAst where = ast.whereClause();
157+
assertEquals(3, where.patterns().size());
158+
159+
assertInstanceOf(BgpAst.class, where.patterns().get(0));
160+
161+
BindAst first = assertInstanceOf(BindAst.class, where.patterns().get(1));
162+
assertEquals("x", first.variable().name());
163+
164+
BindAst second = assertInstanceOf(BindAst.class, where.patterns().get(2));
165+
assertEquals("y", second.variable().name());
166+
}
167+
168+
@Test
169+
@DisplayName("BIND inside an OPTIONAL block")
170+
void shouldParseBindInsideOptional() {
171+
SparqlParser parser = newParserDefault();
172+
173+
QueryAst ast = parser.parse("""
174+
SELECT * WHERE {
175+
?s ?p ?o .
176+
OPTIONAL { BIND(?s AS ?x) }
177+
}
178+
""");
179+
180+
assertNotNull(ast);
181+
GroupGraphPatternAst where = ast.whereClause();
182+
assertEquals(2, where.patterns().size());
183+
184+
assertInstanceOf(BgpAst.class, where.patterns().get(0));
185+
186+
OptionalAst optional = assertInstanceOf(OptionalAst.class, where.patterns().get(1));
187+
GroupGraphPatternAst optionalGroup = assertInstanceOf(GroupGraphPatternAst.class, optional.ast());
188+
189+
assertEquals(1, optionalGroup.patterns().size());
190+
BindAst bind = assertInstanceOf(BindAst.class, optionalGroup.patterns().getFirst());
191+
assertEquals("x", bind.variable().name());
192+
}
193+
194+
@Test
195+
@DisplayName("SELECT ?x with BIND(... AS ?x) — BIND variable is visible in projection")
196+
void shouldAcceptBindVariableInSelectProjection() {
197+
SparqlParser parser = newParserDefault();
198+
199+
QueryAst ast = parser.parse("""
200+
SELECT ?x WHERE {
201+
?s ?p ?o .
202+
BIND(?s AS ?x)
203+
}
204+
""");
205+
206+
assertNotNull(ast);
207+
SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast);
208+
assertEquals(1, select.projection().variables().size());
209+
assertEquals("x", select.projection().variables().getFirst().name());
210+
}
211+
}

0 commit comments

Comments
 (0)