diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 10e3b1cb7..a7300d0eb 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -137,6 +137,11 @@ private record ServiceEntry(int groupDepth, TermAst endpoint, boolean silent) {} private final Set datasetDefaultGraphs = new LinkedHashSet<>(); private final Set datasetNamedGraphs = new LinkedHashSet<>(); + /** + * VALUES clause (agglomerate all VALUES declarations in the query) + */ + private final List values = new ArrayList<>(); + /** SELECT DISTINCT / REDUCED. Set by SelectQueryFeature when parsing SELECT (DISTINCT | REDUCED)? ... */ private boolean distinct; private boolean reduced; @@ -274,12 +279,15 @@ public void exitSelectQuery() { new IriAst(baseUri) ); + ValuesAst valuesClause = new ValuesAst(this.values); + SelectQueryAst selectQueryAst = new SelectQueryAst( frame.projection, datasetClauseAst, frame.whereClause, buildSolutionModifier(frame), - prologueAst + prologueAst, + valuesClause ); /* @@ -562,7 +570,7 @@ public void addTriple(TermAst s, TermAst p, TermAst o) { // --- Filters --- /** - * Exits a Filter, builds FilterAst and adds it to the current group + * Exits a Filter and adds it to the current group */ public void addFilter(FilterAst filter) { if (this.hasCurrentGroup()) { @@ -570,6 +578,10 @@ public void addFilter(FilterAst filter) { } } + public void addValues(List mappings) { + this.values.addAll(mappings); + } + /** * Adds {@code BIND(expression AS ?var)} to the current group. The variable must not already be * visible from earlier patterns in the same group (SPARQL 1.1 scoping). @@ -705,11 +717,12 @@ public QueryAst getResult() { } DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); QueryPrologueAst prologueAst = new QueryPrologueAst(List.copyOf(prefixDeclarations), new IriAst(baseUri)); + ValuesAst valuesClause = new ValuesAst(this.values); return switch (this.queryType) { - case ASK -> buildAskQueryAst(datasetClauseAst, prologueAst); - case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst, prologueAst); - case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst, prologueAst); - case SELECT -> buildSelectQueryAst(datasetClauseAst, prologueAst); + case ASK -> buildAskQueryAst(datasetClauseAst, prologueAst, valuesClause); + case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst, prologueAst, valuesClause); + case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst, prologueAst, valuesClause); + case SELECT -> buildSelectQueryAst(datasetClauseAst, prologueAst, valuesClause); case UNDEFINED -> throw new QueryEvaluationException("Could not determine the type of query during parsing"); }; } @@ -743,14 +756,14 @@ private void ensureNoOpenBgp() { /** * Builds the AST for ASK queries. */ - private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { - return new AskQueryAst(datasetClauseAst, whereClause, buildSolutionModifier(), prologue); + private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue, ValuesAst valuesClause) { + return new AskQueryAst(datasetClauseAst, whereClause, buildSolutionModifier(), prologue, valuesClause); } /** * Builds the AST for SELECT queries. */ - private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { + private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue, ValuesAst valuesClause) { if (hasCurrentSelect()) { SelectFrame frame = getCurrentSelectFrame(); return new SelectQueryAst( @@ -758,36 +771,39 @@ private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst, Qu datasetClauseAst, frame.whereClause, buildSolutionModifier(frame), - prologue + prologue, + valuesClause ); } - return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier(), prologue); + return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier(), prologue, valuesClause); } /** * Builds the AST for DESCRIBE queries. */ - private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { + private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue, ValuesAst valuesClause) { // TODO #306: validate variable scope for DESCRIBE modifiers when DescribeQueryAst carries them. return new DescribeQueryAst( datasetClauseAst, describeResources, whereClause, buildSolutionModifier(), - prologue); + prologue, + valuesClause); } /** * Builds the AST for CONSTRUCT queries. */ - private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { + private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue, ValuesAst valuesClause) { // 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(), - prologue); + prologue, + valuesClause); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java index d2b53cc72..e392f6828 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java @@ -242,4 +242,24 @@ public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) { public void exitBind(SparqlParser.BindContext ctx) { for (var d : delegates) d.exitBind(ctx); } + + @Override + public void enterValuesClause(SparqlParser.ValuesClauseContext ctx) { + for (var d : delegates) d.enterValuesClause(ctx); + } + + @Override + public void exitValuesClause(SparqlParser.ValuesClauseContext ctx) { + for (var d : delegates) d.exitValuesClause(ctx); + } + + @Override + public void enterInlineData(SparqlParser.InlineDataContext ctx) { + for (var d : delegates) d.enterInlineData(ctx); + } + + @Override + public void exitInlineData(SparqlParser.InlineDataContext ctx) { + for (var d : delegates) d.exitInlineData(ctx); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java index 06ed0ff03..3c5caf766 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java @@ -6,20 +6,7 @@ import fr.inria.corese.core.next.query.api.sparql.options.SparqlAstError; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; import fr.inria.corese.core.next.query.api.validation.QueryValidationResult; -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.BindFeature; -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.HavingFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.MinusFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.PrologueFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.SelectQueryFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.ServiceFeature; -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.parser.listener.*; import fr.inria.corese.core.next.query.impl.parser.semantic.validator.SparqlQuerySemanticValidator; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import org.antlr.v4.runtime.BailErrorStrategy; @@ -139,7 +126,8 @@ private QueryAst buildAst(ParseTree tree, SparqlParserOptions options) { new DatasetClauseFeature(builder), new PrologueFeature(builder), new BindFeature(builder), - new ServiceFeature(builder) + new ServiceFeature(builder), + new ValuesFeature(builder) )); ParseTreeWalker walker = new ParseTreeWalker(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/FilterFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/FilterFeature.java index fc3b182ed..9a9d2af16 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/FilterFeature.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/FilterFeature.java @@ -12,8 +12,6 @@ */ public class FilterFeature extends AbstractSparqlFeature { - private static final Logger logger = LoggerFactory.getLogger(FilterFeature.class); - public FilterFeature(SparqlAstBuilder builder) { super(builder); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/ValuesFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/ValuesFeature.java new file mode 100644 index 000000000..5fa769607 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/ValuesFeature.java @@ -0,0 +1,115 @@ +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.api.exception.QuerySyntaxException; +import fr.inria.corese.core.next.query.impl.parser.SparqlAstBuilder; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * Capture parsing of VALUES. VALUES can be declared both in the WHERE clause (through {@code inlineData}) and outside the query (through {@code valuesClause}). + */ +public class ValuesFeature extends AbstractSparqlFeature { + + public ValuesFeature(SparqlAstBuilder builder) { + super(builder); + } + + @Override + public void exitValuesClause(SparqlParser.ValuesClauseContext ctx) { + if(ctx.dataBlock() != null) { + processDataBlock(ctx.dataBlock()); + } + } + + @Override + public void exitInlineData(SparqlParser.InlineDataContext ctx) { + if(ctx.dataBlock() != null) { + processDataBlock(ctx.dataBlock()); + } + } + + private void processDataBlock(SparqlParser.DataBlockContext ctx) { + if(ctx.inlineDataOneVar() != null) { + processInlineDataOneVar(ctx.inlineDataOneVar()); + } else if (ctx.inlineDataFull() != null) { + processInlineDataFull(ctx.inlineDataFull()); + } + } + + private void processInlineDataOneVar(SparqlParser.InlineDataOneVarContext ctx) { + if(ctx.var_() != null) { + VarAst varAst = (VarAst) this.builder().termFromVar(ctx.var_()); + if(!Objects.equals(varAst.name(), "()")) { + List mappingAstList = new ArrayList<>(); + if(ctx.dataBlockValue() != null) { + ctx.dataBlockValue().forEach(dataBlockValueContext -> { + Map valueMap = termAstFromDataBlockValues(List.of(varAst), List.of(dataBlockValueContext)); + // Each dataBlockValue is a solution + mappingAstList.add(new ValueMappingAst(valueMap)); + }); + this.builder().addValues(mappingAstList); + } + } + } else if(ctx.var_() == null && ctx.dataBlockValue() != null) { + throw new QuerySyntaxException("Missing variable for solution mapping in VALUES clause"); + } + } + + /** + * + * @param dataBlockValueList + * @return A list of terms or null for UNDEF values + */ + private Map termAstFromDataBlockValues(List variables, List dataBlockValueList) { + if(variables.size() != dataBlockValueList.size()) { + throw new QuerySyntaxException("VALUE solutions should have a value for every variable and at least a variable for a solution."); + } + Map valuesList = new HashMap<>(); + for(int varNum = 0; varNum < variables.size(); varNum++) { + VarAst variable = variables.get(varNum); + SparqlParser.DataBlockValueContext dataBlockValueContext = dataBlockValueList.get(varNum); + if(dataBlockValueContext.iriRef() != null) { + valuesList.put(variable, this.builder().termFromIriRef(dataBlockValueContext.iriRef())); + } else if(dataBlockValueContext.rdfLiteral() != null) { + valuesList.put(variable, this.builder().termFromRdfLiteral(dataBlockValueContext.rdfLiteral())); + } else if(dataBlockValueContext.numericLiteral() != null) { + valuesList.put(variable, this.builder().termFromNumericLiteral(dataBlockValueContext.numericLiteral())); + } else if(dataBlockValueContext.booleanLiteral() != null) { + valuesList.put(variable, this.builder().termFromBooleanLiteral(dataBlockValueContext.booleanLiteral())); + } else if(dataBlockValueContext.UNDEF() != null) { + valuesList.put(variable, null); + } + } + dataBlockValueList.forEach(dataBlockValueContext -> { + }); + return valuesList; + } + + private void processInlineDataFull(SparqlParser.InlineDataFullContext ctx) { + List varList = new ArrayList<>(); + if(ctx.var_() != null) { + ctx.var_().forEach(varContext -> { + VarAst varAst = (VarAst) this.builder().termFromVar(varContext); + if(!Objects.equals(varAst.name(), "()")) { + varList.add(varAst); + } + }); + } + if(!varList.isEmpty()) { + List valuesList = new ArrayList<>(); + if(ctx.dataBlockValues() != null) { // Each dataBlockValues is a solution + ctx.dataBlockValues().forEach(dataBlockValuesContext -> { // Each dataBlockValue is a value for a variable in a solution + if(dataBlockValuesContext.dataBlockValue() != null) { + Map valueList = termAstFromDataBlockValues(varList, dataBlockValuesContext.dataBlockValue()); + valuesList.add(new ValueMappingAst(valueList)); + } + }); + } + this.builder().addValues(valuesList); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/AbstractSemanticValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/AbstractSemanticValidationRule.java new file mode 100644 index 000000000..096cccbb3 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/AbstractSemanticValidationRule.java @@ -0,0 +1,19 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.rule; + +import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; + +public abstract class AbstractSemanticValidationRule implements SemanticValidationRule { + + protected abstract String getDiagnosticSource(); + + protected QueryDiagnostic buildOutOfScopeDiagnostic(String variableName, String clause) { + return new QueryDiagnostic( + QueryDiagnostic.Kind.SEMANTIC_ERROR, + QueryDiagnostic.Severity.ERROR, + "Variable ?" + variableName + " used in " + clause + " is not visible in WHERE clause", + -1, + -1, + "?" + variableName, + getDiagnosticSource()); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OrderByScopeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OrderByScopeValidationRule.java index a322a387f..8f5e07d83 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OrderByScopeValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OrderByScopeValidationRule.java @@ -22,12 +22,17 @@ * Validates that variables referenced from ORDER BY are visible from the * WHERE clause scope. */ -public final class OrderByScopeValidationRule implements SemanticValidationRule { +public final class OrderByScopeValidationRule extends AbstractSemanticValidationRule { private static final String DIAGNOSTIC_SOURCE = "OrderByScopeValidationRule"; private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer(); + @Override + protected String getDiagnosticSource() { + return DIAGNOSTIC_SOURCE; + } + @Override public List validate(QueryAst queryAst) { return switch (queryAst) { @@ -44,7 +49,7 @@ public List validate(QueryAst queryAst) { } private List validateSelectQuery(SelectQueryAst queryAst) { - Set visibleVariables = variableScopeAnalyzer.collectVisibleVariables(queryAst.whereClause()); + Set visibleVariables = variableScopeAnalyzer.collectVisibleVariables(queryAst); List diagnostics = new ArrayList<>(); validateOrderVariables( collectOrderByAvailableVariables(queryAst.projection(), visibleVariables), @@ -99,15 +104,4 @@ private void validateOrderVariables( } } } - - private QueryDiagnostic buildOutOfScopeDiagnostic(String variableName, String clause) { - return new QueryDiagnostic( - QueryDiagnostic.Kind.SEMANTIC_ERROR, - QueryDiagnostic.Severity.ERROR, - "Variable ?" + variableName + " used in " + clause + " is not visible in WHERE clause", - -1, - -1, - "?" + variableName, - DIAGNOSTIC_SOURCE); - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/SelectProjectionScopeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/SelectProjectionScopeValidationRule.java index 7426d3119..f44a46afe 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/SelectProjectionScopeValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/SelectProjectionScopeValidationRule.java @@ -6,6 +6,8 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -15,12 +17,17 @@ * Validates that explicitly projected SELECT variables are visible from the * WHERE clause scope. */ -public final class SelectProjectionScopeValidationRule implements SemanticValidationRule { +public final class SelectProjectionScopeValidationRule extends AbstractSemanticValidationRule { private static final String DIAGNOSTIC_SOURCE = "SelectProjectionScopeValidationRule"; private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer(); + @Override + protected String getDiagnosticSource() { + return DIAGNOSTIC_SOURCE; + } + @Override public List validate(QueryAst queryAst) { if (!(queryAst instanceof SelectQueryAst selectQueryAst)) { @@ -30,7 +37,7 @@ public List validate(QueryAst queryAst) { return List.of(); } - Set visibleVariables = variableScopeAnalyzer.collectVisibleVariables(selectQueryAst.whereClause()); + Set visibleVariables = variableScopeAnalyzer.collectVisibleVariables(selectQueryAst); List diagnostics = new ArrayList<>(); validateProjectionVariables(selectQueryAst.projection(), visibleVariables, diagnostics); return List.copyOf(diagnostics); @@ -52,15 +59,4 @@ private void validateProjectionVariables( } } } - - private QueryDiagnostic buildOutOfScopeDiagnostic(String variableName, String clause) { - return new QueryDiagnostic( - QueryDiagnostic.Kind.SEMANTIC_ERROR, - QueryDiagnostic.Severity.ERROR, - "Variable ?" + variableName + " used in " + clause + " is not visible in WHERE clause", - -1, - -1, - "?" + variableName, - DIAGNOSTIC_SOURCE); - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/VariableScopeAnalyzer.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/VariableScopeAnalyzer.java index e76905ff7..3c8392146 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/VariableScopeAnalyzer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/VariableScopeAnalyzer.java @@ -2,7 +2,10 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -16,6 +19,44 @@ */ public final class VariableScopeAnalyzer { + /** + * Collects variables visible from a Query. + * + * @param query the query to inspect + * @return the set of visible variable names, without {@code ?} or {@code $} in the patterns used for the resolution of the query + */ + public Set collectVisibleVariables(QueryAst query) { + Set visibleVariables = collectVisibleVariables(query.whereClause()); + visibleVariables.addAll(collectVisibleVariables(query.valuesClause())); + return visibleVariables; + } + + /** + * Collects variables visible from a VALUES clause. + * + * @param valuesClause the WHERE clause to inspect + * @return the set of visible variable names, without {@code ?} or {@code $} + */ + public Set collectVisibleVariables(ValuesAst valuesClause) { + Set visibleVariables = new LinkedHashSet<>(); + + if(valuesClause == null) { + return visibleVariables; + } + + valuesClause.mappings().forEach(valueMappingAst -> { + Set varNameSet = new HashSet<>(); + valueMappingAst.values().keySet().forEach(varAst -> { + if(varAst != null) { + varNameSet.add(varAst.name()); + } + }); + visibleVariables.addAll(varNameSet); + }); + + return visibleVariables; + } + /** * Collects variables visible from a WHERE clause. * @@ -89,9 +130,11 @@ case BindAst(TermAst expression, VarAst variable) -> // FILTER does not make a variable visible by itself. } - case ServiceAst(TermAst endpoint, boolean silentFlag, GroupGraphPatternAst servicePattern) -> + case ServiceAst(TermAst endpoint, boolean silentFlag, GroupGraphPatternAst servicePattern) -> { // SERVICE exposes variables from its inner graph pattern. collectVisibleVariables(servicePattern, visibleVariables); + addIfVariable(endpoint, visibleVariables); + } case SubQueryAst(QueryAst query) -> { if (query instanceof SelectQueryAst select) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index 60f540039..d9a26a801 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -16,10 +16,10 @@ * } */ public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, - SolutionModifierAst solutionModifier, QueryPrologueAst prologue) implements QueryAst { + SolutionModifierAst solutionModifier, QueryPrologueAst prologue, ValuesAst valuesClause) implements QueryAst { public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) { - this(datasetClause, whereClause, null, null); + this(datasetClause, whereClause, null, null, null); } public AskQueryAst { @@ -35,5 +35,8 @@ public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereCla if (solutionModifier == null) { solutionModifier = SolutionModifierAst.empty(); } + if(valuesClause == null) { + valuesClause = ValuesAst.none(); + } } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index 9c30bca4d..86b9a6730 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -35,7 +35,8 @@ public record ConstructQueryAst( DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, - QueryPrologueAst prologue + QueryPrologueAst prologue, + ValuesAst valuesClause ) implements QueryAst { public ConstructQueryAst { if (constructTemplate == null) { @@ -53,19 +54,22 @@ public record ConstructQueryAst( if (prologue == null) { prologue = QueryPrologueAst.empty(); } + if(valuesClause == null) { + valuesClause = ValuesAst.none(); + } } /** * constructor with default prefix handler */ public ConstructQueryAst(ConstructTemplateAst template, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) { - this(template, datasetClause, whereClause, solutionModifier, null); + this(template, datasetClause, whereClause, solutionModifier, null, null); } /** * constructor with default prefix handler and default solution modifier */ public ConstructQueryAst(ConstructTemplateAst template, GroupGraphPatternAst whereClause) { - this(template, DatasetClauseAst.none(), whereClause, SolutionModifierAst.empty(), null); + this(template, DatasetClauseAst.none(), whereClause, SolutionModifierAst.empty(), null, null); } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index a689cffba..8389f3ae4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -26,7 +26,8 @@ public record DescribeQueryAst( List described, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, - QueryPrologueAst prologue + QueryPrologueAst prologue, + ValuesAst valuesClause ) implements QueryAst { public DescribeQueryAst { described = described != null ? List.copyOf(described) : List.of(); @@ -42,6 +43,9 @@ public record DescribeQueryAst( if(prologue == null) { prologue = QueryPrologueAst.empty(); } + if(valuesClause == null) { + valuesClause = ValuesAst.none(); + } } /** @@ -53,14 +57,14 @@ public DescribeQueryAst( GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier ) { - this(datasetClause, described, whereClause, solutionModifier, null); + this(datasetClause, described, whereClause, solutionModifier, null, null); } /** * constructor with default prefix handler and default solution modifier */ public DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause) { - this(datasetClause, described, whereClause, null, null); + this(datasetClause, described, whereClause, null, null, null); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java index 6df83c7bd..cae32d94f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java @@ -8,4 +8,5 @@ public sealed interface QueryAst permits AskQueryAst, ConstructQueryAst, Describ DatasetClauseAst datasetClause(); GroupGraphPatternAst whereClause(); QueryPrologueAst prologue(); + ValuesAst valuesClause(); } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index 947107f81..1d9de7f52 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -9,7 +9,7 @@ * {@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 { +public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, QueryPrologueAst prologue, ValuesAst valuesClause) implements QueryAst { /** Constructor with default projection SELECT *. */ public SelectQueryAst(GroupGraphPatternAst whereClause) { @@ -18,12 +18,12 @@ public SelectQueryAst(GroupGraphPatternAst whereClause) { /** Constructor with default solution modifier (no DISTINCT/REDUCED/ORDER BY/LIMIT/OFFSET) and default prologue. */ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) { - this(projection, datasetClause, whereClause, null, null); + this(projection, datasetClause, whereClause, null, null, null); } /** Constructor with default prologue */ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) { - this(projection, datasetClause, whereClause, solutionModifier, null); + this(projection, datasetClause, whereClause, solutionModifier, null, null); } public SelectQueryAst { @@ -42,5 +42,8 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, if (prologue == null) { prologue = QueryPrologueAst.empty(); } + if(valuesClause == null) { + valuesClause = ValuesAst.none(); + } } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java new file mode 100644 index 000000000..f1ffc1cd8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java @@ -0,0 +1,17 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represent one solution mapping for VALUES, in the order it is written. A set of values for variables with null standing for UNDEF. + * @param values + */ +public record ValueMappingAst(Map values) { + + public ValueMappingAst { + if(values == null) { + values = new HashMap<>(); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java new file mode 100644 index 000000000..323987aa2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java @@ -0,0 +1,20 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import java.util.ArrayList; +import java.util.List; + +/** + * VALUES clause. Cumulated mappings of all the VALUES clauses declared in a query + */ +public record ValuesAst(List mappings) { + + public ValuesAst { + if(mappings == null) { + mappings = new ArrayList<>(); + } + } + + public static ValuesAst none() { + return new ValuesAst(new ArrayList<>()); + } +} diff --git a/src/main/java/fr/inria/corese/core/sparql/triple/parser/Variable.java b/src/main/java/fr/inria/corese/core/sparql/triple/parser/Variable.java index a379f7acd..43ebe658f 100755 --- a/src/main/java/fr/inria/corese/core/sparql/triple/parser/Variable.java +++ b/src/main/java/fr/inria/corese/core/sparql/triple/parser/Variable.java @@ -131,7 +131,10 @@ void getVariables(List list) { } public String getSimpleName() { - return getName().substring(1); + if(getName().startsWith("?") || getName().startsWith("$")) { + return getName().substring(1); + } + return getName(); } public void setPath(boolean b) { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValuesTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValuesTest.java new file mode 100644 index 000000000..d158cafc4 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValuesTest.java @@ -0,0 +1,172 @@ +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.api.exception.QuerySyntaxException; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.*; + +public class SparqlParserValuesTest extends AbstractSparqlParserFeatureTest { + + private static final Logger logger = LoggerFactory.getLogger(SparqlParserValuesTest.class); + + @Test + public void inlineSyntaxTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var { + VALUES ?var { "test" } + } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(2, valuesAst.mappings().size()); + assertEquals(1, valuesAst.mappings().getFirst().values().keySet().size()); + assertEquals(1, valuesAst.mappings().getLast().values().keySet().size()); + VarAst varKey = new VarAst("var"); + assertTrue(valuesAst.mappings().getFirst().values().containsKey(varKey)); + assertInstanceOf(LiteralAst.class, valuesAst.mappings().getFirst().values().get(varKey)); + LiteralAst literalAst = (LiteralAst) valuesAst.mappings().getFirst().values().get(varKey); + assertEquals("\"test\"", literalAst.lexical()); + assertInstanceOf(IriAst.class, valuesAst.mappings().getLast().values().get(varKey)); + IriAst iriAst = (IriAst) valuesAst.mappings().getLast().values().get(varKey); + assertEquals("", iriAst.raw()); + } + + @Test + public void fullSyntaxTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var1 ?var2 { + VALUES (?var1 ?var2) { ("test1" ) ("test2" ) } + } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(2, valuesAst.mappings().size()); + assertEquals(2, valuesAst.mappings().getFirst().values().keySet().size()); + assertEquals(2, valuesAst.mappings().getLast().values().keySet().size()); + VarAst var1Key = new VarAst("var1"); + VarAst var2Key = new VarAst("var2"); + assertTrue(valuesAst.mappings().getFirst().values().containsKey(var1Key)); + assertTrue(valuesAst.mappings().getFirst().values().containsKey(var2Key)); + assertTrue(valuesAst.mappings().getLast().values().containsKey(var1Key)); + assertTrue(valuesAst.mappings().getLast().values().containsKey(var2Key)); + assertEquals(2, valuesAst.mappings().getFirst().values().size()); + assertEquals(2, valuesAst.mappings().getLast().values().size()); + + ValueMappingAst valueMappingAst1 = valuesAst.mappings().getFirst(); + assertInstanceOf(LiteralAst.class, valueMappingAst1.values().get(var1Key)); + LiteralAst literalAst1 = (LiteralAst) valueMappingAst1.values().get(var1Key); + assertEquals("\"test1\"", literalAst1.lexical()); + assertInstanceOf(IriAst.class, valueMappingAst1.values().get(var2Key)); + IriAst iriAst1 = (IriAst) valueMappingAst1.values().get(var2Key); + assertEquals("", iriAst1.raw()); + + ValueMappingAst valueMappingAst2 = valuesAst.mappings().getLast(); + assertInstanceOf(LiteralAst.class, valueMappingAst2.values().get(var1Key)); + LiteralAst literalAst2 = (LiteralAst) valueMappingAst2.values().get(var1Key); + assertEquals("\"test2\"", literalAst2.lexical()); + assertInstanceOf(IriAst.class, valueMappingAst2.values().get(var2Key)); + IriAst iriAst2 = (IriAst) valueMappingAst2.values().get(var2Key); + assertEquals("", iriAst2.raw()); + } + + @Test + public void multipleValuesTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var1 ?var2 { + VALUES ?var1 { "test1" } + } + VALUES (?var2 ?var3) { ("test2" ) } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(2, valuesAst.mappings().size()); + assertEquals(1, valuesAst.mappings().getFirst().values().keySet().size()); + assertEquals(2, valuesAst.mappings().getLast().values().keySet().size()); + VarAst var1Key = new VarAst("var1"); + VarAst var2Key = new VarAst("var2"); + VarAst var3Key = new VarAst("var3"); + assertTrue(valuesAst.mappings().getFirst().values().containsKey(var1Key)); + assertTrue(valuesAst.mappings().getLast().values().containsKey(var2Key)); + assertTrue(valuesAst.mappings().getLast().values().containsKey(var3Key)); + + ValueMappingAst valueMappingAst1 = valuesAst.mappings().getFirst(); + assertInstanceOf(LiteralAst.class, valueMappingAst1.values().get(var1Key)); + LiteralAst literalAst1 = (LiteralAst) valueMappingAst1.values().get(var1Key); + assertEquals("\"test1\"", literalAst1.lexical()); + + ValueMappingAst valueMappingAst2 = valuesAst.mappings().getLast(); + assertInstanceOf(LiteralAst.class, valueMappingAst2.values().get(var2Key)); + LiteralAst literalAst2 = (LiteralAst) valueMappingAst2.values().get(var2Key); + assertEquals("\"test2\"", literalAst2.lexical()); + assertInstanceOf(IriAst.class, valueMappingAst2.values().get(var3Key)); + IriAst iriAst2 = (IriAst) valueMappingAst2.values().get(var3Key); + assertEquals("", iriAst2.raw()); + } + + @Test + public void nilValueSyntaxTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var { + VALUES ?var { "test" UNDEF } + } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(2, valuesAst.mappings().size()); + assertEquals(1, valuesAst.mappings().getFirst().values().keySet().size()); + assertEquals(1, valuesAst.mappings().getLast().values().keySet().size()); + VarAst varKey = new VarAst("var"); + assertTrue(valuesAst.mappings().getFirst().values().containsKey(varKey)); + assertInstanceOf(LiteralAst.class, valuesAst.mappings().getFirst().values().get(varKey)); + LiteralAst literalAst = (LiteralAst) valuesAst.mappings().getFirst().values().get(varKey); + assertEquals("\"test\"", literalAst.lexical()); + assertNull(valuesAst.mappings().getLast().values().get(varKey)); + } + + @Test + public void nilVarNilValueSyntaxTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var { + ?var ?var ?var . + VALUES () { () } + } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(0, valuesAst.mappings().size()); + } + + @Test + public void nilVarSomeValueSyntaxExceptionTest() { + SparqlParser parser = newParserDefault(); + String inlineValueTest = """ + SELECT ?var { + ?var ?var ?var . + VALUES () { ( "test" ) } + } + """; + QueryAst ast = parser.parse(inlineValueTest); + assertNotNull(ast); + ValuesAst valuesAst = ast.valuesClause(); + assertNotNull(valuesAst); + assertEquals(0, valuesAst.mappings().size()); + } +}