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
Expand Up @@ -320,6 +320,17 @@ public void setProjectionAll() {
* Sets explicit SELECT variables (e.g. SELECT ?s ?p). Variable names may include ? or $ prefix.
*/
public void setProjectionVariables(List<String> variableNames) {
setProjectionVariables(variableNames, List.of());
}

/**
* Sets explicit SELECT variables where some may be introduced by {@code (expr AS ?var)}.
* Variable names may include ? or $ prefix.
*
* @param variableNames all projected variable names (plain + expression-bound), in order
* @param expressionBoundNames names of variables introduced by {@code (expr AS ?var)}
*/
public void setProjectionVariables(List<String> variableNames, List<String> expressionBoundNames) {
if (variableNames == null || variableNames.isEmpty()) {
setProjectionAll();
return;
Expand All @@ -330,9 +341,15 @@ public void setProjectionVariables(List<String> variableNames) {
.map(VarAst::new)
.toList();

Set<String> expressionBound = expressionBoundNames == null ? Set.of() :
expressionBoundNames.stream()
.map(s -> s == null ? "" : (s.startsWith("?") || s.startsWith("$") ? s.substring(1).trim() : s.trim()))
.filter(s -> !s.isBlank())
.collect(java.util.stream.Collectors.toUnmodifiableSet());

ProjectionAst newProjection = vars.isEmpty()
? ProjectionAsts.selectAll()
:ProjectionAsts.of(vars);
: ProjectionAsts.of(vars, expressionBound);

if (hasCurrentSelect()) {
getCurrentSelectFrame().projection = newProjection;
Expand Down Expand Up @@ -1242,6 +1259,8 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S
return new IfAst(args.get(0), args.get(1), args.get(2));
} else if (ctx.RAND() != null) {
return new RandAst();
} else if (ctx.UUID() != null) {
return new UuidAst();
} else if (ctx.CONCAT() != null) {
List<TermAst> args = ctx.expression().stream().map(this::termFromExpression).toList();
return this.createConstraint(ASTConstants.FUNCTION_CALL.CONCAT, args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,27 @@ public void exitSubSelect(SparqlParser.SubSelectContext ctx) {
}

/**
* Extracts SELECT * or SELECT ?v1 ?v2 ... from the parse context.
* Grammar: {@code SELECT (DISTINCT | REDUCED)? (var_+ | '*') ...}
* Extracts SELECT * or SELECT ?v1 ?v2 ... (expr AS ?v3) ... from the parse context.
* Grammar: {@code SELECT (DISTINCT | REDUCED)? (selectVar+ | '*')}
* where {@code selectVar ::= var_ | '(' expression AS var_ ')'}
*/
private void extractProjection(SparqlParser.SelectClauseContext ctx) {
if (ctx.STAR() != null) {
builder().setProjectionAll();
return;
}
List<String> vars = new ArrayList<>();
List<String> allVars = new ArrayList<>();
List<String> expressionBoundVars = new ArrayList<>();
for (SparqlParser.SelectVarContext selectVar : ctx.selectVar()) {
if (selectVar.var_() != null) {
vars.add(selectVar.var_().getText());
if (selectVar.expression() != null) {
// (expr AS ?var) — introduces a new variable, not projected from WHERE
String varName = selectVar.var_().getText();
allVars.add(varName);
expressionBoundVars.add(varName);
} else if (selectVar.var_() != null) {
allVars.add(selectVar.var_().getText());
}
}
builder().setProjectionVariables(vars);
builder().setProjectionVariables(allVars, expressionBoundVars);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ private void validateProjectionVariables(
) {
// TODO: handle SELECT (expr AS ?var) with SPARQL 1.1 support.
for (VarAst projectedVar : projection.variables()) {
if (projection.expressionBoundVariables().contains(projectedVar.name())) {
// Variable introduced by (expr AS ?var) — not required to be visible in WHERE.
continue;
}
if (!visibleVariables.contains(projectedVar.name())) {
diagnostics.add(buildOutOfScopeDiagnostic(projectedVar.name(), "SELECT projection"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
package fr.inria.corese.core.next.query.impl.sparql.ast;

import java.util.List;
import java.util.Set;

/**
* SPARQL SELECT projection: either {@code SELECT *} (all variables from the pattern)
* or an explicit list of variables {@code SELECT ?s ?p ?o}.
* <p>
* Use {@link ProjectionAsts#selectAll()} and {@link ProjectionAsts#of(List)} to create instances.
*/


public record ProjectionAst(boolean selectAll, List<VarAst> variables) {
/**
* Creates a SPARQL SELECT Projection
* @param selectAll {@code true} if the projection corresponds to {@code SELECT *},
* meaning all variables from the WHERE clause are projected;
* {@code false} if the projection explicitly lists variables.
* @param variables the variables explicitly projected by the SELECT clause.
* When {@code selectAll} is {@code true}, this list must be empty.
* @throws IllegalArgumentException if {@code selectAll} is {@code true} and
* {@code variables} is non-empty
* if {@code selectAll} is {@code false} and {@code variables} is empty
*/
public record ProjectionAst(boolean selectAll, List<VarAst> variables, Set<String> expressionBoundVariables) {
public ProjectionAst {
variables = variables != null ? List.copyOf(variables) : List.of();
expressionBoundVariables = expressionBoundVariables != null ? Set.copyOf(expressionBoundVariables) : Set.of();
if (selectAll && !variables.isEmpty()) {
throw new IllegalArgumentException("selectAll is true but variables is non-empty");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.inria.corese.core.next.query.impl.sparql.ast;

import java.util.List;
import java.util.Set;

/**
* Utility factory for {@link ProjectionAst}.
Expand All @@ -13,14 +14,26 @@ private ProjectionAsts() {

/** SELECT * : project all variables from the WHERE clause. */
public static ProjectionAst selectAll() {
return new ProjectionAst(true, List.of());
return new ProjectionAst(true, List.of(), Set.of());
}

/** SELECT ?v1 ?v2 ... : project only the given variables. */
public static ProjectionAst of(List<VarAst> variables) {
if (variables == null || variables.isEmpty()) {
return selectAll();
}
return new ProjectionAst(false, List.copyOf(variables));
return new ProjectionAst(false, List.copyOf(variables), Set.of());
}

/**
* SELECT ?v1 (expr AS ?v2) ... : project plain variables and expression-bound variables.
* Expression-bound variables are those introduced by {@code (expr AS ?var)} in the SELECT clause;
* they are included in {@code variables} but are not required to be visible in the WHERE clause.
*/
public static ProjectionAst of(List<VarAst> variables, Set<String> expressionBoundVariables) {
if (variables == null || variables.isEmpty()) {
return selectAll();
}
return new ProjectionAst(false, List.copyOf(variables), expressionBoundVariables != null ? expressionBoundVariables : Set.of());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.inria.corese.core.next.query.impl.sparql.ast.constraint;

import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;

/**
* Function {@code UUID()} in SPARQL 1.1
* Returns a fresh IRI from the UUID URN scheme.
*/
public record UuidAst() implements ConstraintAst {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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.impl.sparql.ast.constraint.EqualsAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UuidAst;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("SPARQL 1.1 - Parser and AST : UUID")
class SparqlParserUuidTest extends AbstractSparqlParserFeatureTest {

@Test
@DisplayName("BIND(UUID() AS ?id)")
void shouldParseUuid() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
BIND(UUID() AS ?id)
}
""");

assertNotNull(ast);
BindAst bind = assertInstanceOf(BindAst.class, ast.whereClause().patterns().getLast());
assertInstanceOf(UuidAst.class, bind.expression());
assertEquals("id", bind.variable().name());
}

@Test
@DisplayName("FILTER(UUID() = ?s)")
void shouldParseUuidInFilter() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT * WHERE {
?s ?p ?o .
FILTER(UUID() = ?s)
}
""");

assertNotNull(ast);
FilterAst filter = assertInstanceOf(FilterAst.class, ast.whereClause().patterns().getLast());
EqualsAst equals = assertInstanceOf(EqualsAst.class, filter.operator());
assertInstanceOf(UuidAst.class, equals.getLeftArgument());
assertEquals("s", assertInstanceOf(VarAst.class, equals.getRightArgument()).name());
}

@Test
@DisplayName("SELECT (UUID() AS ?id) ?s WHERE { ?s ?p ?o }")
void shouldParseUuidInProjectionBinding() {
SparqlParser parser = newParserDefault();

QueryAst ast = parser.parse("""
SELECT (UUID() AS ?id) ?s WHERE {
?s ?p ?o .
}
""");

assertNotNull(ast);
SelectQueryAst selectAst = assertInstanceOf(SelectQueryAst.class, ast);
assertFalse(selectAst.projection().selectAll());
assertEquals(2, selectAst.projection().variables().size());
assertEquals("id", selectAst.projection().variables().getFirst().name());
assertEquals("s", selectAst.projection().variables().getLast().name());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as https://github.com/corese-stack/corese-core/pull/406/changes#r3072101894
Please add a test for the usage of the function as a projection binding
Ex:

SELECT (UUID() AS ?id) ?s {
    ?s ?p ?o .
}

}
Loading