Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fr.inria.corese.core.next.query.impl.query;

import fr.inria.corese.core.next.data.spi.io.IOConstants;
import fr.inria.corese.core.next.data.api.term.IRI;
import fr.inria.corese.core.next.data.api.term.BNode;
import fr.inria.corese.core.next.data.api.term.Resource;
import fr.inria.corese.core.next.data.api.model.Statement;
import fr.inria.corese.core.next.data.api.term.Value;
Expand All @@ -15,15 +17,18 @@
import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.NamedGraphQuadsAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.QuadsAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestUnitAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst;
import fr.inria.corese.core.next.query.impl.sparql.bridge.SparqlAstToExpression;
import fr.inria.corese.core.next.storage.api.StorageManager;
import fr.inria.corese.core.next.storage.api.operations.MutationOperations;
import fr.inria.corese.core.next.common.text.RdfText;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
Expand Down Expand Up @@ -60,8 +65,8 @@ public void execute() throws QueryEvaluationException {

for (UpdateRequestUnitAst operation : request.operations()) {
switch (operation) {
case InsertDataRequestAst(QuadsAst data) -> applyQuads(data, mutations, factory, true);
case DeleteDataRequestAst(QuadsAst data) -> applyQuads(data, mutations, factory, false);
case InsertDataRequestAst(QuadsAst data) -> applyQuads(data, mutations, factory, request.prologue(), true);
case DeleteDataRequestAst(QuadsAst data) -> applyQuads(data, mutations, factory, request.prologue(), false);
default -> throw new UnsupportedQueryFeatureException(
"SPARQL UPDATE operation not yet supported: "
+ operation.getClass().getSimpleName());
Expand All @@ -74,19 +79,20 @@ public void execute() throws QueryEvaluationException {
// -------------------------------------------------------------------------

private void applyQuads(QuadsAst quads, MutationOperations mutations,
CoreseValueFactory factory, boolean insert) {
CoreseValueFactory factory, QueryPrologueAst prologue, boolean insert) {
Map<String, BNode> blankNodes = new HashMap<>();
for (TriplePatternAst triple : quads.defaultTriples()) {
Statement stmt = toStatement(triple, null, factory);
Statement stmt = toStatement(triple, null, factory, prologue, blankNodes, insert);
if (insert) {
mutations.add(stmt);
} else {
mutations.remove(stmt);
}
}
for (NamedGraphQuadsAst block : quads.namedGraphBlocks()) {
Resource context = (Resource) termToValue(block.graph(), factory);
Resource context = (Resource) termToValue(block.graph(), factory, prologue, blankNodes, insert);
for (TriplePatternAst triple : block.triples()) {
Statement stmt = toStatement(triple, context, factory);
Statement stmt = toStatement(triple, context, factory, prologue, blankNodes, insert);
if (insert) {
mutations.add(stmt);
} else {
Expand All @@ -96,16 +102,18 @@ private void applyQuads(QuadsAst quads, MutationOperations mutations,
}
}

private Statement toStatement(TriplePatternAst triple, Resource context, CoreseValueFactory factory) {
Value subject = termToValue(triple.subject(), factory);
Value object = termToValue(triple.object(), factory);
private Statement toStatement(TriplePatternAst triple, Resource context,
CoreseValueFactory factory, QueryPrologueAst prologue,
Map<String, BNode> blankNodes, boolean insert) {
Value subject = termToValue(triple.subject(), factory, prologue, blankNodes, insert);
Value object = termToValue(triple.object(), factory, prologue, blankNodes, insert);

// Resolve predicate — INSERT/DELETE DATA only allows simple predicate IRIs
if (!(triple.predicate() instanceof PredicatePathAst(TermAst pp))) {
throw new UnsupportedQueryFeatureException(
"Property paths are not allowed in INSERT/DELETE DATA");
}
Value predicate = termToValue(pp, factory);
Value predicate = termToValue(pp, factory, prologue, blankNodes, insert);

if (!(subject instanceof Resource s)) {
throw new QueryEvaluationException("UPDATE subject must be a Resource, got: " + subject);
Expand All @@ -119,17 +127,33 @@ private Statement toStatement(TriplePatternAst triple, Resource context, CoreseV
return factory.createStatement(s, p, object);
}

private Value termToValue(TermAst term, CoreseValueFactory factory) {
private Value termToValue(
TermAst term,
CoreseValueFactory factory,
QueryPrologueAst prologue,
Map<String, BNode> blankNodes,
boolean insert) {
return switch (term) {
case IriAst(String raw) -> factory.createIRI(RdfText.stripAngleBrackets(raw));
case LiteralAst(String lexical, String datatype, String lang) -> {
case IriAst(String raw) -> {
String resolved = SparqlAstToExpression.resolveIri(raw, prologue);
if (resolved != null && resolved.startsWith(IOConstants.BLANK_NODE_PREFIX)) {
if (!insert) {
throw new QueryEvaluationException("Blank nodes are not allowed in DELETE DATA");
}
yield blankNodes.computeIfAbsent(resolved, ignored -> factory.createBNode());
}
yield factory.createIRI(resolved);
}
case LiteralAst(String lexical, String lang, String datatype) -> {
String clean = SparqlAstToExpression.unquoteLexical(lexical);
if (lang != null && !lang.isBlank()) {
yield factory.createLiteral(lexical, lang);
yield factory.createLiteral(clean, lang);
}
if (datatype != null && !datatype.isBlank()) {
yield factory.createLiteral(lexical, factory.createIRI(datatype));
String resolvedDatatype = SparqlAstToExpression.resolveIri(datatype, prologue);
yield factory.createLiteral(clean, factory.createIRI(resolvedDatatype));
}
yield factory.createLiteral(lexical);
yield factory.createLiteral(clean);
}
default -> throw new UnsupportedQueryFeatureException(
"Variables are not allowed in INSERT/DELETE DATA: " + term);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,40 @@ private static Constant literalToConstant(String lexical, String lang, String da
return Constant.createString(unquoteLexical(lexical));
}

private static String unquoteLexical(String lexical) {
if (lexical.length() >= 2 && lexical.startsWith("\"")) {
if (lexical.endsWith("\"")) {
return lexical.substring(1, lexical.length() - 1);
public static String unquoteLexical(String lexical) {
if (lexical == null || lexical.length() < 2) {
return lexical;
}
String unquotedTriple = stripTripleQuotes(lexical);
if (unquotedTriple != null) {
return unquotedTriple;
}
return stripSingleQuotes(lexical);
}

private static String stripTripleQuotes(String lexical) {
if (lexical.length() >= 6) {
if (lexical.startsWith("\"\"\"") && lexical.endsWith("\"\"\"")) {
return lexical.substring(3, lexical.length() - 3);
}
int langIdx = lexical.lastIndexOf('"');
if (langIdx > 0) {
return lexical.substring(1, langIdx);
if (lexical.startsWith("'''") && lexical.endsWith("'''")) {
return lexical.substring(3, lexical.length() - 3);
}
}
return null;
}

private static String stripSingleQuotes(String lexical) {
char quote = lexical.charAt(0);
if (quote != '"' && quote != '\'') {
return lexical;
}
int endIdx = lexical.endsWith(String.valueOf(quote))
? lexical.length() - 1
: lexical.lastIndexOf(quote);
if (endIdx > 0) {
return lexical.substring(1, endIdx);
}
return lexical;
}

Expand Down Expand Up @@ -189,7 +213,7 @@ private static Constant iriToConstant(String rawIri) {
* <li>Prefixed names (prefix:local) are expanded using declared prefixes or Corese default namespaces.</li>
* </ul>
*/
static String resolveIri(String raw, QueryPrologueAst prologue) {
public static String resolveIri(String raw, QueryPrologueAst prologue) {
if (raw == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlAstBuilder;
import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlQueryAstBuilder;
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
import org.antlr.v4.runtime.RuleContext;

import java.util.List;

Expand All @@ -29,10 +30,20 @@ public SparqlQueryAstBuilder queryBuilder() {
@Override
public void enterConstructQuery(SparqlParser.ConstructQueryContext ctx) {
queryBuilder().enterConstructQuery();
if (ctx.constructTemplate() == null) {
queryBuilder().enterConstructTemplate();
queryBuilder().enterGroup();
queryBuilder().enterBgp();
}
}

@Override
public void exitConstructQuery(SparqlParser.ConstructQueryContext ctx) {
if (ctx.constructTemplate() == null) {
queryBuilder().exitBgp();
queryBuilder().exitGroup();
queryBuilder().exitConstructTemplate();
}
queryBuilder().exitConstructQuery();
}

Expand All @@ -47,11 +58,14 @@ public void exitConstructTemplate(SparqlParser.ConstructTemplateContext ctx) {
}

/**
* Only handles {@code triplesSameSubject} nodes inside the CONSTRUCT template (not the WHERE BGP).
* Handles {@code triplesSameSubject} nodes inside the CONSTRUCT template
* or inside the short-form {@code CONSTRUCT WHERE { triplesTemplate }}.
*/
@Override
public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) {
if (!(ctx.getParent() instanceof SparqlParser.ConstructTriplesContext)) {
boolean inConstructTriples = ctx.getParent() instanceof SparqlParser.ConstructTriplesContext;
boolean inConstructWhere = isConstructWhere(ctx);
if (!inConstructTriples && !inConstructWhere) {
return;
}
if (ctx.varOrTerm() == null || ctx.propertyListNotEmpty() == null) {
Expand All @@ -64,7 +78,18 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) {
List<TermAst> objects = queryBuilder().termListFromObjectList(propertyList.objectList(verbIndex));
for (TermAst object : objects) {
queryBuilder().addConstructTriple(subject, predicate, object);
if (inConstructWhere) {
queryBuilder().addTriple(subject, predicate, object);
}
}
}
}

private boolean isConstructWhere(SparqlParser.TriplesSameSubjectContext ctx) {
RuleContext parent = ctx.getParent();
while (parent instanceof SparqlParser.TriplesTemplateContext) {
parent = parent.getParent();
}
return parent instanceof SparqlParser.ConstructQueryContext;
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.rule;

import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic;
import fr.inria.corese.core.next.query.impl.sparql.ast.ProjectionAst;
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 java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* Validates that explicitly projected SELECT variables are visible from the
* WHERE clause scope.
* Validates aliases introduced by explicit SELECT expressions.
*
* <p>A projected variable, or a variable referenced by a SELECT expression, may be
* unbound. The expression then evaluates to an error for that solution. In contrast,
* the target variable of {@code (expr AS ?var)} must be new at the point where it is
* introduced.</p>
*/
public final class SelectProjectionScopeValidationRule extends AbstractSemanticValidationRule {

Expand All @@ -33,53 +34,31 @@ public List<QueryDiagnostic> validate(QueryAst queryAst) {
return List.of();
}

Set<String> visibleVariables = collectSelectAvailableVariables(selectQueryAst);
List<QueryDiagnostic> diagnostics = new ArrayList<>();
validateProjectionVariables(selectQueryAst.projection(), visibleVariables, diagnostics);
return List.copyOf(diagnostics);
Set<String> inScopeVariables = collectSelectAvailableVariables(selectQueryAst);
return selectQueryAst.projection().expressionBoundVariables().stream()
.filter(inScopeVariables::contains)
.map(this::buildAlreadyInScopeDiagnostic)
.toList();
}

/**
* SELECT projections can reuse aliases introduced by {@code GROUP BY (expr AS ?var)}.
* GROUP BY aliases are already in scope and cannot be reused as SELECT expression targets.
*/
private Set<String> collectSelectAvailableVariables(SelectQueryAst selectQueryAst) {
Set<String> visibleVariables = new LinkedHashSet<>(collectVisibleVariables(selectQueryAst));
visibleVariables.addAll(selectQueryAst.solutionModifier().groupBy().expressionBoundVariables());
return visibleVariables;
}

private void validateProjectionVariables(
ProjectionAst projection,
Set<String> visibleVariables,
List<QueryDiagnostic> diagnostics
) {
Set<String> availableVariables = new LinkedHashSet<>(visibleVariables);
for (VarAst projectedVar : projection.variables()) {
if (projection.expressionBoundVariables().contains(projectedVar.name())) {
validateProjectionExpression(projectedVar.name(), projection, availableVariables, diagnostics);
availableVariables.add(projectedVar.name());
continue;
}
if (!availableVariables.contains(projectedVar.name())) {
diagnostics.add(buildOutOfScopeDiagnostic(projectedVar.name(), ScopeClause.SELECT_PROJECTION));
}
}
private QueryDiagnostic buildAlreadyInScopeDiagnostic(String variableName) {
return new QueryDiagnostic(
QueryDiagnostic.Kind.SEMANTIC_ERROR,
QueryDiagnostic.Severity.ERROR,
"Variable ?" + variableName + " introduced by SELECT expression is already in scope",
-1,
-1,
"?" + variableName,
getDiagnosticSource());
}

/**
* SELECT expressions introduce the projected variable themselves, but the variables they reference
* must still be visible from the query scope.
*/
private void validateProjectionExpression(
String projectionVariableName,
ProjectionAst projection,
Set<String> visibleVariables,
List<QueryDiagnostic> diagnostics
) {
addOutOfScopeDiagnostics(
projection.expressionReferencedVariables().getOrDefault(projectionVariableName, Set.of()),
visibleVariables,
ScopeClause.SELECT_PROJECTION,
diagnostics);
}
}
Loading
Loading