Skip to content

Commit f33cd4c

Browse files
authored
Merge pull request #379 from corese-stack/feature/306-query-modifier-variable-validation
feat(next/parser): validate SELECT projection and ORDER BY variable visibility
2 parents 063d41d + 303b227 commit f33cd4c

10 files changed

Lines changed: 755 additions & 108 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fr.inria.corese.core.next.query.api.exception;
2+
3+
/**
4+
* Thrown when a query is syntactically valid but violates a static validation rule.
5+
*/
6+
@SuppressWarnings("java:S110")
7+
public class QueryValidationException extends QueryException {
8+
9+
/**
10+
* Constructs a QueryValidationException with a detail message.
11+
*
12+
* @param message the detail message explaining why the query is invalid
13+
*/
14+
public QueryValidationException(String message) {
15+
super(message);
16+
}
17+
18+
/**
19+
* Constructs a QueryValidationException with a detail message and cause.
20+
*
21+
* @param message the detail message explaining why the query is invalid
22+
* @param cause the underlying cause of the validation failure
23+
*/
24+
public QueryValidationException(String message, Throwable cause) {
25+
super(message, cause);
26+
}
27+
}

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

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser;
55
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
66
import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
7+
import fr.inria.corese.core.next.query.api.exception.QueryValidationException;
78
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
89
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*;
910
import org.antlr.v4.runtime.tree.ParseTree;
1011
import org.antlr.v4.runtime.tree.TerminalNode;
1112

12-
1313
import java.util.*;
1414

1515
/**
@@ -112,6 +112,11 @@ public final class SparqlAstBuilder {
112112
*/
113113
private ConstructTemplateAst constructTemplate;
114114

115+
/**
116+
* Helper used to compute visible and referenced variables.
117+
*/
118+
private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer();
119+
115120
public SparqlAstBuilder(SparqlParserOptions options) {
116121
this.options = options;
117122
}
@@ -328,14 +333,10 @@ public QueryAst getResult() {
328333
}
329334
DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs);
330335
return switch (this.queryType) {
331-
case DESCRIBE -> new DescribeQueryAst(datasetClauseAst, describeResources, whereClause);
332-
case CONSTRUCT -> new ConstructQueryAst(
333-
constructTemplate != null ? constructTemplate : new ConstructTemplateAst(List.of()),
334-
datasetClauseAst,
335-
whereClause,
336-
buildSolutionModifier());
337-
case ASK -> new AskQueryAst(datasetClauseAst, whereClause);
338-
case SELECT -> new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier());
336+
case ASK -> buildAskQueryAst(datasetClauseAst);
337+
case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst);
338+
case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst);
339+
case SELECT -> buildSelectQueryAst(datasetClauseAst);
339340
case UNDEFINED -> throw new QueryEvaluationException("Could not determine the type of query during parsing");
340341
};
341342
}
@@ -366,6 +367,116 @@ private void ensureNoOpenBgp() {
366367
}
367368
}
368369

370+
/**
371+
* Builds the AST for ASK queries.
372+
*/
373+
private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst) {
374+
return new AskQueryAst(datasetClauseAst, whereClause);
375+
}
376+
377+
/**
378+
* Builds the AST for SELECT queries.
379+
*/
380+
private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst) {
381+
validateSelectQueryScope();
382+
return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier());
383+
}
384+
385+
/**
386+
* Builds the AST for DESCRIBE queries.
387+
*/
388+
private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst) {
389+
// TODO #306: validate variable scope for DESCRIBE modifiers when DescribeQueryAst carries them.
390+
return new DescribeQueryAst(datasetClauseAst, describeResources, whereClause);
391+
}
392+
393+
/**
394+
* Builds the AST for CONSTRUCT queries.
395+
*/
396+
private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst) {
397+
// TODO #306: validate variable scope for CONSTRUCT modifiers when ConstructQueryAst carries them.
398+
return new ConstructQueryAst(
399+
constructTemplate != null ? constructTemplate : new ConstructTemplateAst(List.of()),
400+
datasetClauseAst,
401+
whereClause,
402+
buildSolutionModifier());
403+
}
404+
405+
/**
406+
* Validates SELECT projection and ORDER BY variables against the WHERE clause scope.
407+
*/
408+
private void validateSelectQueryScope() {
409+
// TODO #306: extend this validation to GROUP BY when it is supported by the next parser.
410+
Set<String> visibleVariables = variableScopeAnalyzer.collectVisibleVariables(whereClause);
411+
412+
// SELECT * still needs ORDER BY validation.
413+
if (!projection.selectAll()) {
414+
validateProjectionVariables(visibleVariables);
415+
}
416+
417+
validateOrderVariables(collectOrderByAvailableVariables(visibleVariables));
418+
}
419+
420+
/**
421+
* Validates explicit projection variables against the WHERE clause scope.
422+
*
423+
* @param visibleVariables variable names visible from the WHERE clause
424+
*/
425+
private void validateProjectionVariables(Set<String> visibleVariables) {
426+
for (VarAst projectedVar : projection.variables()) {
427+
if (!visibleVariables.contains(projectedVar.name())) {
428+
throw new QueryValidationException(buildOutOfScopeVariableMessage(
429+
projectedVar.name(),
430+
"SELECT projection"));
431+
}
432+
}
433+
}
434+
435+
/**
436+
* Collects variables that may be referenced from ORDER BY.
437+
*
438+
* <p>SPARQL applies ORDER BY before the final projection step. In the current next parser,
439+
* explicit projection variables are already validated against the WHERE clause, so adding
440+
* them here mainly keeps the availability rule explicit while staying within the current
441+
* SPARQL 1.0 feature set.
442+
*
443+
* @param visibleVariables variable names visible from the WHERE clause
444+
* @return variable names available to ORDER BY validation
445+
*/
446+
private Set<String> collectOrderByAvailableVariables(Set<String> visibleVariables) {
447+
Set<String> availableVariables = new LinkedHashSet<>(visibleVariables);
448+
if (!projection.selectAll()) {
449+
for (VarAst projectedVar : projection.variables()) {
450+
availableVariables.add(projectedVar.name());
451+
}
452+
}
453+
return availableVariables;
454+
}
455+
456+
/**
457+
* Validates ORDER BY variables against the variables available at ORDER BY time.
458+
*
459+
* @param availableOrderVariables variable names available to ORDER BY
460+
*/
461+
private void validateOrderVariables(Set<String> availableOrderVariables) {
462+
for (OrderConditionAst orderCondition : orderConditions) {
463+
Set<String> referencedVariables = variableScopeAnalyzer
464+
.collectReferencedVariables(orderCondition.expression());
465+
466+
for (String variableName : referencedVariables) {
467+
if (!availableOrderVariables.contains(variableName)) {
468+
throw new QueryValidationException(buildOutOfScopeVariableMessage(
469+
variableName,
470+
"ORDER BY"));
471+
}
472+
}
473+
}
474+
}
475+
476+
private String buildOutOfScopeVariableMessage(String variableName, String clause) {
477+
return "Variable ?" + variableName + " used in " + clause + " is not visible in WHERE clause";
478+
}
479+
369480
/**
370481
* Signals the start of a {@code GroupOrUnionGraphPattern}.
371482
*/
@@ -1021,7 +1132,6 @@ public TermAst termFromRegex(SparqlParser.RegexExpressionContext ctx) {
10211132
throw new QueryEvaluationException("Unexpected arguments for REGEX call");
10221133
}
10231134
}
1024-
10251135
/**
10261136
* Predicate as a property path.
10271137
* For simple triples without a composed path, this is just an iriRef or 'a'.
@@ -1054,4 +1164,4 @@ public List<TermAst> termListFromObjectListPath(SparqlParser.ObjectListPathConte
10541164
}
10551165
return out;
10561166
}
1057-
}
1167+
}

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

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

3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.Reader;
6+
import java.io.StringReader;
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.List;
9+
10+
import org.antlr.v4.runtime.BailErrorStrategy;
11+
import org.antlr.v4.runtime.CharStream;
12+
import org.antlr.v4.runtime.CharStreams;
13+
import org.antlr.v4.runtime.CommonTokenStream;
14+
import org.antlr.v4.runtime.DefaultErrorStrategy;
15+
import org.antlr.v4.runtime.RecognitionException;
16+
import org.antlr.v4.runtime.tree.ParseTree;
17+
import org.antlr.v4.runtime.tree.ParseTreeWalker;
18+
import org.antlr.v4.runtime.misc.ParseCancellationException;
19+
320
import fr.inria.corese.core.next.data.impl.io.common.IOConstants;
421
import fr.inria.corese.core.next.data.impl.io.parser.util.ParserConstants;
522
import fr.inria.corese.core.next.impl.parser.antlr.SparqlLexer;
623
import fr.inria.corese.core.next.query.api.base.io.AbstractQueryParser;
7-
import fr.inria.corese.core.next.query.api.exception.QueryException;
24+
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
825
import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
26+
import fr.inria.corese.core.next.query.api.exception.QueryValidationException;
927
import fr.inria.corese.core.next.query.api.io.parser.QueryOptions;
1028
import fr.inria.corese.core.next.query.api.sparql.options.BaseIRIOptions;
11-
import fr.inria.corese.core.next.query.impl.parser.listener.*;
29+
import fr.inria.corese.core.next.query.impl.parser.listener.AskQueryFeature;
30+
import fr.inria.corese.core.next.query.impl.parser.listener.BgpFeature;
31+
import fr.inria.corese.core.next.query.impl.parser.listener.ConstructQueryFeature;
32+
import fr.inria.corese.core.next.query.impl.parser.listener.DatasetClauseFeature;
33+
import fr.inria.corese.core.next.query.impl.parser.listener.DescribeQueryFeature;
34+
import fr.inria.corese.core.next.query.impl.parser.listener.FilterFeature;
35+
import fr.inria.corese.core.next.query.impl.parser.listener.SelectQueryFeature;
36+
import fr.inria.corese.core.next.query.impl.parser.listener.SolutionModifierFeature;
37+
import fr.inria.corese.core.next.query.impl.parser.listener.UnionFeature;
1238
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
13-
import org.antlr.v4.runtime.*;
14-
import org.antlr.v4.runtime.tree.ParseTree;
15-
import org.antlr.v4.runtime.tree.ParseTreeWalker;
16-
17-
import java.io.IOException;
18-
import java.io.InputStream;
19-
import java.io.Reader;
20-
import java.io.StringReader;
21-
import java.nio.charset.StandardCharsets;
22-
import java.util.List;
2339

2440
public class SparqlParser extends AbstractQueryParser {
2541

@@ -84,19 +100,20 @@ public QueryAst parse(Reader reader, String baseIRI) {
84100
ParseTree tree;
85101

86102
try {
87-
tree= parser.query();
103+
tree = parser.query();
88104
if (errorListener.hasErrors()) {
89105
String errorMsg = errorListener.getErrorMessage();
90106
if (errorMsg == null || errorMsg.trim().isEmpty()) {
91107
errorMsg = "Unknown syntax error detected";
92108
}
93-
throw new QueryException("Syntax error in Sparql query: " + errorMsg);
109+
throw new QuerySyntaxException("Syntax error in SPARQL query: " + errorMsg);
94110
}
95111
} catch (RecognitionException e) {
96-
throw new QueryException("Recognition error in Sparql query: " + e.getMessage(), e);
112+
throw new QuerySyntaxException("Recognition error in SPARQL query: " + e.getMessage(), e);
113+
} catch (ParseCancellationException e) {
114+
throw toQuerySyntaxException(e, errorListener);
97115
}
98116

99-
100117
SparqlAstBuilder builder = new SparqlAstBuilder(sparqlParserOptions);
101118

102119
SparqlListener listener = new SparqlListener(List.of(
@@ -114,12 +131,11 @@ public QueryAst parse(Reader reader, String baseIRI) {
114131
walker.walk(listener, tree);
115132

116133
return builder.getResult();
117-
118-
}
119-
catch (IOException e) {
120-
throw new QueryException("Failed to parse SPARQL query: " + e.getMessage(), e);
121-
}
122-
catch (Exception e) {
134+
} catch (QuerySyntaxException | QueryValidationException | QueryEvaluationException e) {
135+
throw e;
136+
} catch (IOException e) {
137+
throw new QueryEvaluationException("Failed to parse SPARQL query: " + e.getMessage(), e);
138+
} catch (Exception e) {
123139
throw new QuerySyntaxException("Unexpected error during SPARQL parsing: " + e.getMessage(), e);
124140
}
125141
}
@@ -139,6 +155,37 @@ private String getBaseIRIFromConfig() {
139155
return opts.getBaseIRI();
140156
}
141157

158+
/**
159+
* Normalizes ANTLR fail-fast parse cancellations into a Corese syntax exception.
160+
* Reuses an existing QuerySyntaxException when available, otherwise prefers
161+
* diagnostics collected by the error listener before falling back to the
162+
* cancellation or cause message.
163+
*
164+
* @param e the original ParseCancellationException thrown by ANTLR
165+
* @param errorListener the error listener that may have collected syntax errors
166+
* @return a QuerySyntaxException with an informative message and the original exception as cause
167+
*/
168+
static QuerySyntaxException toQuerySyntaxException(ParseCancellationException e, SparqlErrorListener errorListener) {
169+
if (e.getCause() instanceof QuerySyntaxException querySyntaxException) {
170+
return querySyntaxException;
171+
}
172+
173+
String errorMsg = null;
174+
if (errorListener != null && errorListener.hasErrors()) {
175+
errorMsg = errorListener.getErrorMessage();
176+
} else if (e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().trim().isEmpty()) {
177+
errorMsg = e.getCause().getMessage();
178+
} else if (e.getMessage() != null && !e.getMessage().trim().isEmpty()) {
179+
errorMsg = e.getMessage();
180+
}
181+
182+
if (errorMsg == null || errorMsg.trim().isEmpty()) {
183+
errorMsg = "Parsing cancelled due to a syntax error";
184+
}
185+
186+
return new QuerySyntaxException(errorMsg, e);
187+
}
188+
142189
/** Returns config as SparqlParserOptions, or default options if null or wrong type. */
143190
private SparqlParserOptions getEffectiveConfig() {
144191
QueryOptions opts = getConfig();

0 commit comments

Comments
 (0)