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 c000ca901..55057535a 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 @@ -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 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 variableNames, List expressionBoundNames) { if (variableNames == null || variableNames.isEmpty()) { setProjectionAll(); return; @@ -330,9 +341,15 @@ public void setProjectionVariables(List variableNames) { .map(VarAst::new) .toList(); + Set 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; @@ -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 args = ctx.expression().stream().map(this::termFromExpression).toList(); return this.createConstraint(ASTConstants.FUNCTION_CALL.CONCAT, args); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/SelectQueryFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/SelectQueryFeature.java index bd3ad14a5..61d714f8b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/SelectQueryFeature.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/SelectQueryFeature.java @@ -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 vars = new ArrayList<>(); + List allVars = new ArrayList<>(); + List 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); } } \ No newline at end of file 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 a549d57b1..7426d3119 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 @@ -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")); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java index f6c64bcf3..1c867598d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java @@ -1,6 +1,7 @@ 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) @@ -8,22 +9,10 @@ *

* Use {@link ProjectionAsts#selectAll()} and {@link ProjectionAsts#of(List)} to create instances. */ - - -public record ProjectionAst(boolean selectAll, List 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 variables, Set 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"); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAsts.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAsts.java index bd909543b..6eb89afc3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAsts.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAsts.java @@ -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}. @@ -13,7 +14,7 @@ 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. */ @@ -21,6 +22,18 @@ public static ProjectionAst of(List 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 variables, Set expressionBoundVariables) { + if (variables == null || variables.isEmpty()) { + return selectAll(); + } + return new ProjectionAst(false, List.copyOf(variables), expressionBoundVariables != null ? expressionBoundVariables : Set.of()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java new file mode 100644 index 000000000..1fe2e5644 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java @@ -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 {} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserUuidTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserUuidTest.java new file mode 100644 index 000000000..5565b5921 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserUuidTest.java @@ -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()); + } +} \ No newline at end of file