Skip to content

Commit f892f9f

Browse files
[SPARQL 1.1] - Parser and AST : EXISTS
1 parent 063d41d commit f892f9f

6 files changed

Lines changed: 181 additions & 6 deletions

File tree

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.antlr.v4.runtime.tree.ParseTree;
1010
import org.antlr.v4.runtime.tree.TerminalNode;
1111

12-
1312
import java.util.*;
1413

1514
/**
@@ -55,6 +54,18 @@ public final class SparqlAstBuilder {
5554
*/
5655
private final Deque<Integer> optionalGroupDepths = new ArrayDeque<>();
5756

57+
/**
58+
* At enterExistsFunc()/enterNotExistsFunc(), we push groupStack.size().
59+
* At exitGroup(), if groupStack.size() equals peek, we capture in capturedExistsPattern.
60+
*/
61+
private final Deque<Integer> existsGroupDepths = new ArrayDeque<>();
62+
63+
/**
64+
* Captured GroupGraphPattern for the last closed EXISTS/NOT EXISTS block.
65+
* Consumed by termFromBuiltInCall via popCapturedExistsPattern().
66+
*/
67+
private GroupGraphPatternAst capturedExistsPattern;
68+
5869
/**
5970
* Top-level WHERE clause, set when the root group is closed in exitGroup().
6071
*/
@@ -245,6 +256,9 @@ public void exitGroup() {
245256
if (!optionalGroupDepths.isEmpty() && groupStack.size() == optionalGroupDepths.peek()) {
246257
optionalGroupDepths.pop();
247258
currentGroup().add(new OptionalAst(group));
259+
} else if (!existsGroupDepths.isEmpty() && groupStack.size() == existsGroupDepths.peek()) {
260+
existsGroupDepths.pop();
261+
capturedExistsPattern = group;
248262
} else if (groupStack.isEmpty()) {
249263
whereClause = group;
250264
} else {
@@ -311,6 +325,32 @@ public void enterOptional() {
311325
public void exitOptional() {
312326
}
313327

328+
329+
/**
330+
* Enter EXISTS scope. Records current group stack size so that when we exitGroup() and the stack
331+
* is back to that size, we capture the group in {@link #capturedExistsPattern}.
332+
*/
333+
public void enterExistsFunc() {
334+
existsGroupDepths.push(groupStack.size());
335+
}
336+
337+
/**
338+
* Enter NOT EXISTS scope. Same mechanism as EXISTS.
339+
*/
340+
public void enterNotExistsFunc() {
341+
existsGroupDepths.push(groupStack.size());
342+
}
343+
344+
/**
345+
* Pops the captured GroupGraphPattern from the last closed EXISTS/NOT EXISTS block.
346+
* Called by {@link #termFromBuiltInCall} after the listener has closed the group.
347+
*/
348+
public GroupGraphPatternAst popCapturedExistsPattern() {
349+
GroupGraphPatternAst p = capturedExistsPattern;
350+
capturedExistsPattern = null;
351+
return p;
352+
}
353+
314354
// --- Result ---
315355

316356
/**
@@ -429,7 +469,7 @@ private SolutionModifierAst buildSolutionModifier() {
429469
}
430470

431471
public boolean isOrdered() {
432-
return ! this.orderConditions.isEmpty();
472+
return !this.orderConditions.isEmpty();
433473
}
434474

435475
/**
@@ -803,7 +843,11 @@ public TermAst termFromConstraint(fr.inria.corese.core.next.impl.parser.antlr.Sp
803843
}
804844

805845
public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.SparqlParser.BuiltInCallContext ctx) {
806-
if (ctx.expression() != null) {
846+
if (ctx.existsFunc() != null) {
847+
return new ExistsAst(popCapturedExistsPattern());
848+
} else if (ctx.notExistsFunc() != null) {
849+
return new NotExistsAst(popCapturedExistsPattern());
850+
} else if (ctx.expression() != null) {
807851
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
808852
if (ctx.STR() != null) {
809853
return this.createConstraint(ASTConstants.FUNCTION_CALL.STR, args);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,14 @@ public void exitDefaultGraphClause(SparqlParser.DefaultGraphClauseContext ctx) {
163163
public void exitNamedGraphClause(SparqlParser.NamedGraphClauseContext ctx) {
164164
for (var d : delegates) d.exitNamedGraphClause(ctx);
165165
}
166+
167+
@Override
168+
public void enterExistsFunc(SparqlParser.ExistsFuncContext ctx) {
169+
for (var d : delegates) d.enterExistsFunc(ctx);
170+
}
171+
172+
@Override
173+
public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) {
174+
for (var d : delegates) d.enterNotExistsFunc(ctx);
175+
}
166176
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,14 @@ public void exitFilter_(SparqlParser.Filter_Context ctx) {
3030

3131
this.builder().addFilter(filter);
3232
}
33+
34+
@Override
35+
public void enterExistsFunc(SparqlParser.ExistsFuncContext ctx) {
36+
builder().enterExistsFunc();
37+
}
38+
39+
@Override
40+
public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) {
41+
builder().enterNotExistsFunc();
42+
}
3343
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fr.inria.corese.core.next.query.impl.sparql.ast.constraint;
2+
3+
import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;
4+
import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst;
5+
6+
/**
7+
* Operator {@code EXISTS { ... }} in SPARQL 1.1 FILTER
8+
*/
9+
public record ExistsAst(GroupGraphPatternAst pattern) implements ConstraintAst {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fr.inria.corese.core.next.query.impl.sparql.ast.constraint;
2+
3+
import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;
4+
import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst;
5+
6+
/**
7+
* Operator {@code NOT EXISTS { ... }} in SPARQL 1.1 FILTER
8+
*/
9+
public record NotExistsAst(GroupGraphPatternAst pattern) implements ConstraintAst {
10+
}

src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserFilterTest.java

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package fr.inria.corese.core.next.query.impl.parser;
22

3-
import fr.inria.corese.core.next.data.impl.common.literal.XSD;
43
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
54
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*;
65
import org.junit.jupiter.api.Test;
@@ -10,8 +9,6 @@
109
import java.util.List;
1110

1211
import static org.junit.jupiter.api.Assertions.*;
13-
import static org.junit.jupiter.api.Assertions.assertEquals;
14-
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1512

1613
public class SparqlParserFilterTest extends AbstractSparqlParserFeatureTest {
1714

@@ -1194,4 +1191,98 @@ void shouldParseMixedDivideThenMultiplyLeftAssociatively() {
11941191
assertInstanceOf(LiteralAst.class, divideAst.getRightArgument());
11951192
assertEquals("2", ((LiteralAst) divideAst.getRightArgument()).lexical());
11961193
}
1194+
1195+
@Test
1196+
void shouldParseExistsFilter() {
1197+
SparqlParser parser = newParserDefault();
1198+
1199+
QueryAst ast = parser.parse("""
1200+
SELECT * WHERE {
1201+
?s ?p ?o .
1202+
FILTER EXISTS { ?s ex:email ?e }
1203+
}
1204+
""");
1205+
1206+
assertNotNull(ast);
1207+
GroupGraphPatternAst where = ast.whereClause();
1208+
assertEquals(2, where.patterns().size());
1209+
1210+
PatternAst last = where.patterns().getLast();
1211+
assertInstanceOf(FilterAst.class, last);
1212+
1213+
FilterAst filterAst = (FilterAst) last;
1214+
assertInstanceOf(ExistsAst.class, filterAst.operator());
1215+
1216+
ExistsAst existsAst = (ExistsAst) filterAst.operator();
1217+
assertNotNull(existsAst.pattern());
1218+
assertFalse(existsAst.pattern().patterns().isEmpty());
1219+
}
1220+
1221+
@Test
1222+
void shouldParseNotExistsFilter() {
1223+
SparqlParser parser = newParserDefault();
1224+
1225+
QueryAst ast = parser.parse("""
1226+
SELECT * WHERE {
1227+
?s ?p ?o .
1228+
FILTER NOT EXISTS { ?s ex:email ?e }
1229+
}
1230+
""");
1231+
1232+
assertNotNull(ast);
1233+
GroupGraphPatternAst where = ast.whereClause();
1234+
assertEquals(2, where.patterns().size());
1235+
1236+
PatternAst last = where.patterns().getLast();
1237+
assertInstanceOf(FilterAst.class, last);
1238+
1239+
FilterAst filterAst = (FilterAst) last;
1240+
assertInstanceOf(NotExistsAst.class, filterAst.operator());
1241+
1242+
NotExistsAst notExistsAst = (NotExistsAst) filterAst.operator();
1243+
assertNotNull(notExistsAst.pattern());
1244+
assertFalse(notExistsAst.pattern().patterns().isEmpty());
1245+
}
1246+
1247+
@Test
1248+
void shouldParseExistsWithMultipleTriples() {
1249+
SparqlParser parser = newParserDefault();
1250+
1251+
QueryAst ast = parser.parse("""
1252+
SELECT * WHERE {
1253+
?s ?p ?o .
1254+
FILTER EXISTS {
1255+
?s ex:email ?e .
1256+
?s ex:name ?n
1257+
}
1258+
}
1259+
""");
1260+
1261+
assertNotNull(ast);
1262+
FilterAst filterAst = (FilterAst) ast.whereClause().patterns().getLast();
1263+
assertInstanceOf(ExistsAst.class, filterAst.operator());
1264+
1265+
ExistsAst existsAst = (ExistsAst) filterAst.operator();
1266+
assertNotNull(existsAst.pattern());
1267+
}
1268+
1269+
@Test
1270+
void shouldParseNotExistsEmpty() {
1271+
SparqlParser parser = newParserDefault();
1272+
1273+
QueryAst ast = parser.parse("""
1274+
SELECT * WHERE {
1275+
?s ?p ?o .
1276+
FILTER NOT EXISTS { }
1277+
}
1278+
""");
1279+
1280+
assertNotNull(ast);
1281+
FilterAst filterAst = (FilterAst) ast.whereClause().patterns().getLast();
1282+
assertInstanceOf(NotExistsAst.class, filterAst.operator());
1283+
1284+
NotExistsAst notExistsAst = (NotExistsAst) filterAst.operator();
1285+
assertNotNull(notExistsAst.pattern());
1286+
assertTrue(notExistsAst.pattern().patterns().isEmpty());
1287+
}
11971288
}

0 commit comments

Comments
 (0)