Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package fr.inria.corese.core.next.query.api.exception;

/**
* Thrown when a query is syntactically valid but violates a static validation rule.
*/
@SuppressWarnings("java:S110")
public class QueryValidationException extends QueryException {

/**
* Constructs a QueryValidationException with a detail message.
*
* @param message the detail message explaining why the query is invalid
*/
public QueryValidationException(String message) {
super(message);
}

/**
* Constructs a QueryValidationException with a detail message and cause.
*
* @param message the detail message explaining why the query is invalid
* @param cause the underlying cause of the validation failure
*/
public QueryValidationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser;
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
import fr.inria.corese.core.next.query.api.exception.QueryValidationException;
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;


import java.util.*;

/**
Expand Down Expand Up @@ -112,6 +112,11 @@ public final class SparqlAstBuilder {
*/
private ConstructTemplateAst constructTemplate;

/**
* Helper used to compute visible and referenced variables.
*/
private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer();

public SparqlAstBuilder(SparqlParserOptions options) {
this.options = options;
}
Expand Down Expand Up @@ -328,14 +333,10 @@ public QueryAst getResult() {
}
DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs);
return switch (this.queryType) {
case DESCRIBE -> new DescribeQueryAst(datasetClauseAst, describeResources, whereClause);
case CONSTRUCT -> new ConstructQueryAst(
constructTemplate != null ? constructTemplate : new ConstructTemplateAst(List.of()),
datasetClauseAst,
whereClause,
buildSolutionModifier());
case ASK -> new AskQueryAst(datasetClauseAst, whereClause);
case SELECT -> new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier());
case ASK -> buildAskQueryAst(datasetClauseAst);
case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst);
case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst);
case SELECT -> buildSelectQueryAst(datasetClauseAst);
case UNDEFINED -> throw new QueryEvaluationException("Could not determine the type of query during parsing");
};
}
Expand Down Expand Up @@ -366,6 +367,116 @@ private void ensureNoOpenBgp() {
}
}

/**
* Builds the AST for ASK queries.
*/
private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst) {
return new AskQueryAst(datasetClauseAst, whereClause);
}

/**
* Builds the AST for SELECT queries.
*/
private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst) {
validateSelectQueryScope();
return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier());
}

/**
* Builds the AST for DESCRIBE queries.
*/
private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst) {
// TODO #306: validate variable scope for DESCRIBE modifiers when DescribeQueryAst carries them.
return new DescribeQueryAst(datasetClauseAst, describeResources, whereClause);
}

/**
* Builds the AST for CONSTRUCT queries.
*/
private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst) {
// TODO #306: validate variable scope for CONSTRUCT modifiers when ConstructQueryAst carries them.
return new ConstructQueryAst(
constructTemplate != null ? constructTemplate : new ConstructTemplateAst(List.of()),
datasetClauseAst,
whereClause,
buildSolutionModifier());
}

/**
* Validates SELECT projection and ORDER BY variables against the WHERE clause scope.
*/
private void validateSelectQueryScope() {
// TODO #306: extend this validation to GROUP BY when it is supported by the next parser.
Set<String> visibleVariables = variableScopeAnalyzer.collectVisibleVariables(whereClause);

// SELECT * still needs ORDER BY validation.
if (!projection.selectAll()) {
validateProjectionVariables(visibleVariables);
}

validateOrderVariables(collectOrderByAvailableVariables(visibleVariables));
}

/**
* Validates explicit projection variables against the WHERE clause scope.
*
* @param visibleVariables variable names visible from the WHERE clause
*/
private void validateProjectionVariables(Set<String> visibleVariables) {
for (VarAst projectedVar : projection.variables()) {
if (!visibleVariables.contains(projectedVar.name())) {
throw new QueryValidationException(buildOutOfScopeVariableMessage(
projectedVar.name(),
"SELECT projection"));
}
}
}

/**
* Collects variables that may be referenced from ORDER BY.
*
* <p>SPARQL applies ORDER BY before the final projection step. In the current next parser,
* explicit projection variables are already validated against the WHERE clause, so adding
* them here mainly keeps the availability rule explicit while staying within the current
* SPARQL 1.0 feature set.
*
* @param visibleVariables variable names visible from the WHERE clause
* @return variable names available to ORDER BY validation
*/
private Set<String> collectOrderByAvailableVariables(Set<String> visibleVariables) {
Set<String> availableVariables = new LinkedHashSet<>(visibleVariables);
if (!projection.selectAll()) {
for (VarAst projectedVar : projection.variables()) {
availableVariables.add(projectedVar.name());
}
}
return availableVariables;
}

/**
* Validates ORDER BY variables against the variables available at ORDER BY time.
*
* @param availableOrderVariables variable names available to ORDER BY
*/
private void validateOrderVariables(Set<String> availableOrderVariables) {
for (OrderConditionAst orderCondition : orderConditions) {
Set<String> referencedVariables = variableScopeAnalyzer
.collectReferencedVariables(orderCondition.expression());

for (String variableName : referencedVariables) {
if (!availableOrderVariables.contains(variableName)) {
throw new QueryValidationException(buildOutOfScopeVariableMessage(
variableName,
"ORDER BY"));
}
}
}
}

private String buildOutOfScopeVariableMessage(String variableName, String clause) {
return "Variable ?" + variableName + " used in " + clause + " is not visible in WHERE clause";
}

/**
* Signals the start of a {@code GroupOrUnionGraphPattern}.
*/
Expand Down Expand Up @@ -1021,7 +1132,6 @@ public TermAst termFromRegex(SparqlParser.RegexExpressionContext ctx) {
throw new QueryEvaluationException("Unexpected arguments for REGEX call");
}
}

/**
* Predicate as a property path.
* For simple triples without a composed path, this is just an iriRef or 'a'.
Expand Down Expand Up @@ -1054,4 +1164,4 @@ public List<TermAst> termListFromObjectListPath(SparqlParser.ObjectListPathConte
}
return out;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
package fr.inria.corese.core.next.query.impl.parser;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DefaultErrorStrategy;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.misc.ParseCancellationException;

import fr.inria.corese.core.next.data.impl.io.common.IOConstants;
import fr.inria.corese.core.next.data.impl.io.parser.util.ParserConstants;
import fr.inria.corese.core.next.impl.parser.antlr.SparqlLexer;
import fr.inria.corese.core.next.query.api.base.io.AbstractQueryParser;
import fr.inria.corese.core.next.query.api.exception.QueryException;
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
import fr.inria.corese.core.next.query.api.exception.QueryValidationException;
import fr.inria.corese.core.next.query.api.io.parser.QueryOptions;
import fr.inria.corese.core.next.query.api.sparql.options.BaseIRIOptions;
import fr.inria.corese.core.next.query.impl.parser.listener.*;
import fr.inria.corese.core.next.query.impl.parser.listener.AskQueryFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.BgpFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.ConstructQueryFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.DatasetClauseFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.DescribeQueryFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.FilterFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.SelectQueryFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.SolutionModifierFeature;
import fr.inria.corese.core.next.query.impl.parser.listener.UnionFeature;
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class SparqlParser extends AbstractQueryParser {

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

try {
tree= parser.query();
tree = parser.query();
if (errorListener.hasErrors()) {
String errorMsg = errorListener.getErrorMessage();
if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = "Unknown syntax error detected";
}
throw new QueryException("Syntax error in Sparql query: " + errorMsg);
throw new QuerySyntaxException("Syntax error in SPARQL query: " + errorMsg);
}
} catch (RecognitionException e) {
throw new QueryException("Recognition error in Sparql query: " + e.getMessage(), e);
throw new QuerySyntaxException("Recognition error in SPARQL query: " + e.getMessage(), e);
} catch (ParseCancellationException e) {
throw toQuerySyntaxException(e, errorListener);
}


SparqlAstBuilder builder = new SparqlAstBuilder(sparqlParserOptions);

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

return builder.getResult();

}
catch (IOException e) {
throw new QueryException("Failed to parse SPARQL query: " + e.getMessage(), e);
}
catch (Exception e) {
} catch (QuerySyntaxException | QueryValidationException | QueryEvaluationException e) {
throw e;
Comment thread
remiceres marked this conversation as resolved.
} catch (IOException e) {
throw new QueryEvaluationException("Failed to parse SPARQL query: " + e.getMessage(), e);
} catch (Exception e) {
throw new QuerySyntaxException("Unexpected error during SPARQL parsing: " + e.getMessage(), e);
}
Comment on lines +136 to 140
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With BailErrorStrategy (used when failFast is true), ANTLR commonly throws ParseCancellationException (a RuntimeException) for syntax errors. That will currently fall into this generic catch (Exception e) and be rethrown as an "Unexpected error", which is misleading for callers. Consider catching ParseCancellationException explicitly and converting it into a clearer QuerySyntaxException (preserving the original cause/message).

Copilot uses AI. Check for mistakes.
}
Expand All @@ -139,6 +155,37 @@ private String getBaseIRIFromConfig() {
return opts.getBaseIRI();
}

/**
* Normalizes ANTLR fail-fast parse cancellations into a Corese syntax exception.
* Reuses an existing QuerySyntaxException when available, otherwise prefers
* diagnostics collected by the error listener before falling back to the
* cancellation or cause message.
*
* @param e the original ParseCancellationException thrown by ANTLR
* @param errorListener the error listener that may have collected syntax errors
* @return a QuerySyntaxException with an informative message and the original exception as cause
*/
static QuerySyntaxException toQuerySyntaxException(ParseCancellationException e, SparqlErrorListener errorListener) {
if (e.getCause() instanceof QuerySyntaxException querySyntaxException) {
return querySyntaxException;
}

String errorMsg = null;
if (errorListener != null && errorListener.hasErrors()) {
errorMsg = errorListener.getErrorMessage();
} else if (e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().trim().isEmpty()) {
errorMsg = e.getCause().getMessage();
} else if (e.getMessage() != null && !e.getMessage().trim().isEmpty()) {
errorMsg = e.getMessage();
}

if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = "Parsing cancelled due to a syntax error";
}

return new QuerySyntaxException(errorMsg, e);
}

/** Returns config as SparqlParserOptions, or default options if null or wrong type. */
private SparqlParserOptions getEffectiveConfig() {
QueryOptions opts = getConfig();
Expand Down
Loading
Loading