From e823f224d4bcc990438ec09ca5d1e775a47c2dde Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 31 Mar 2026 09:06:06 +0200 Subject: [PATCH 1/5] #332 [SPARQL 1.1] - Parser and AST : BIND --- .../query/impl/parser/SparqlAstBuilder.java | 24 +++-- .../query/impl/parser/SparqlListener.java | 5 + .../next/query/impl/parser/SparqlParser.java | 3 +- .../impl/parser/listener/BindFeature.java | 24 +++++ .../next/query/impl/sparql/ast/BindAst.java | 6 ++ .../query/impl/sparql/ast/PatternAst.java | 4 +- .../impl/parser/SparqlParserBindTest.java | 91 +++++++++++++++++++ 7 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BindFeature.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java 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 78a10a5df..082251207 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 @@ -987,7 +987,14 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S return new ExistsAst(popCapturedExistsPattern()); } else if (ctx.notExistsFunc() != null) { return new NotExistsAst(popCapturedExistsPattern()); - } else if (ctx.expression() != null) { + } else if (ctx.regexExpression() != null) { + return termFromRegex(ctx.regexExpression()); + } else if (ctx.BOUND() != null) { + return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText()))); + } else if (ctx.CONCAT() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return new FunctionCallAst(new IriAst("CONCAT"), args); + } else if (ctx.expression() != null && !ctx.expression().isEmpty()) { List args = ctx.expression().stream().map(this::termFromExpression).toList(); if (ctx.STR() != null) { return this.createConstraint(ASTConstants.FUNCTION_CALL.STR, args); @@ -1005,12 +1012,8 @@ public TermAst termFromBuiltInCall(fr.inria.corese.core.next.impl.parser.antlr.S return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, args); } else if (ctx.IS_LITERAL() != null) { return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, args); - } else if (ctx.BOUND() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText()))); - } else if (ctx.regexExpression() != null) { - return termFromRegex(ctx.regexExpression()); } else { - throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText()); + throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText()); } } else { throw new QueryEvaluationException("Unable to resolve BuiltInCall for token " + ctx.getText()); @@ -1237,4 +1240,13 @@ public List termListFromObjectListPath(SparqlParser.ObjectListPathConte } return out; } + + /** + * Adds a BIND clause to the current group + */ + public void addBind(BindAst bind) { + if (this.hasCurrentGroup()) { + this.currentGroup().add(bind); + } + } } 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 f2a2c0b00..b3d42b3cb 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 @@ -189,4 +189,9 @@ public void enterExistsFunc(SparqlParser.ExistsFuncContext ctx) { public void enterNotExistsFunc(SparqlParser.NotExistsFuncContext ctx) { for (var d : delegates) d.enterNotExistsFunc(ctx); } + + @Override + public void exitBind(SparqlParser.BindContext ctx) { + for (var d : delegates) d.exitBind(ctx); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java index 097fd9b79..bf6972ffc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java @@ -118,7 +118,8 @@ public QueryAst parse(Reader reader, String baseIRI) { new UnionFeature(builder), new DescribeQueryFeature(builder), new DatasetClauseFeature(builder), - new PrologueFeature(builder) + new PrologueFeature(builder), + new BindFeature(builder) )); walker.walk(listener, tree); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BindFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BindFeature.java new file mode 100644 index 000000000..15d93cb70 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BindFeature.java @@ -0,0 +1,24 @@ +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.impl.parser.SparqlAstBuilder; +import fr.inria.corese.core.next.query.impl.sparql.ast.BindAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; + +/** + * SPARQL {@code BIND} feature + */ +public class BindFeature extends AbstractSparqlFeature { + + public BindFeature(SparqlAstBuilder builder) { + super(builder); + } + + @Override + public void exitBind(SparqlParser.BindContext ctx) { + TermAst expression = builder().termFromExpression(ctx.expression()); + VarAst variable = (VarAst) builder().var(ctx.var_().getText()); + builder().addBind(new BindAst(expression, variable)); + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java new file mode 100644 index 000000000..2fea14899 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java @@ -0,0 +1,6 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +/** + * BIND(expression AS ?var) clause in SPARQL 1.1 + */ +public record BindAst(TermAst expression, VarAst variable) implements PatternAst {} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java index 8ad564e25..fc70186f8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; /** - * Element of a group graph pattern (BGP, optional, union, etc.). + * Element of a group graph pattern (BGP, optional, union, Bind, etc.). */ -public sealed interface PatternAst permits BgpAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst { +public sealed interface PatternAst permits BgpAst, BindAst, FilterAst, GroupGraphPatternAst, OptionalAst, UnionAst { } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java new file mode 100644 index 000000000..b812b02ae --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java @@ -0,0 +1,91 @@ +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.FunctionCallAst; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("SPARQL 1.1 - Parser and AST : BIND") +public class SparqlParserBindTest extends AbstractSparqlParserFeatureTest { + + @Test + @DisplayName("BIND(?s AS ?x) — variable expression") + void shouldParseBindWithVariable() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND(?s AS ?x) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + PatternAst last = where.patterns().getLast(); + assertInstanceOf(BindAst.class, last); + + BindAst bindAst = (BindAst) last; + assertInstanceOf(VarAst.class, bindAst.expression()); + assertEquals("s", ((VarAst) bindAst.expression()).name()); + assertEquals("x", bindAst.variable().name()); + } + + @Test + @DisplayName("BIND(CONCAT(?a, ?b) AS ?c) — built-in function expression") + void shouldParseBindWithConcat() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND(CONCAT(?a, ?b) AS ?c) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + PatternAst last = where.patterns().getLast(); + assertInstanceOf(BindAst.class, last); + + BindAst bindAst = (BindAst) last; + assertInstanceOf(FunctionCallAst.class, bindAst.expression()); + assertEquals("c", bindAst.variable().name()); + + FunctionCallAst concatAst = (FunctionCallAst) bindAst.expression(); + assertEquals("CONCAT", ((IriAst) concatAst.functionName()).raw()); + assertEquals(2, concatAst.arguments().size()); + assertEquals("a", ((VarAst) concatAst.arguments().getFirst()).name()); + assertEquals("b", ((VarAst) concatAst.arguments().getLast()).name()); + } + + @Test + @DisplayName("BIND(?x + 1 AS ?y) — arithmetic expression") + void shouldParseBindWithArithmetic() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND(?x + 1 AS ?y) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + PatternAst last = where.patterns().getLast(); + assertInstanceOf(BindAst.class, last); + + BindAst bindAst = (BindAst) last; + assertEquals("y", bindAst.variable().name()); + assertNotNull(bindAst.expression()); + } +} \ No newline at end of file From ba580d5b8837f37ebb5fe14508deb832af6d513e Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 31 Mar 2026 15:25:32 +0200 Subject: [PATCH 2/5] #332 [SPARQL 1.1] - Parser and AST : BIND --- .../impl/parser/VariableScopeAnalyzer.java | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java index f3ab4b842..842db855e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java @@ -1,26 +1,15 @@ package fr.inria.corese.core.next.query.impl.parser; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.FilterAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.OptionalAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst; -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.UnionAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TrinaryRegexAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UnaryConstraintAst; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + /** * Collects visible and referenced variables from the next SPARQL AST. * A visible variable is introduced by a graph pattern in the WHERE clause and @@ -92,7 +81,9 @@ case UnionAst(GroupGraphPatternAst left, GroupGraphPatternAst right) -> { collectVisibleVariables(left, visibleVariables); collectVisibleVariables(right, visibleVariables); } - + case BindAst(TermAst expression, VarAst variable) -> { + visibleVariables.add(variable.name()); + } case FilterAst ignored -> { // FILTER does not make a variable visible by itself. } From cab6549ef77d5dbd0c1e6a30b6b544099b1f222e Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 31 Mar 2026 15:34:18 +0200 Subject: [PATCH 3/5] #332 [SPARQL 1.1] - Parser and AST : BIND correction java doc --- .../inria/corese/core/next/data/impl/common/util/IRIUtils.java | 2 +- .../corese/core/next/query/impl/sparql/ast/SelectQueryAst.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java index 00a245a59..0198a24e0 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java @@ -126,7 +126,7 @@ public static String guessLocalName(String iri) { /** * Detects if an IRI is absolute according to the REGEX given in the recommendation RFC3987 - * @param iri any uri (expecting to be the content between < and > + * @param iri any uri (expecting to be the content between < and >) * @return true if it is compliant with RFC3987. May accept the prefixed for of uri, as there is no way to * distinguish a prefix from a protocol */ 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 fbcf02164..947107f81 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 @@ -6,7 +6,7 @@ * Abstract Syntax Tree (AST) representation of a SPARQL {@code SELECT} query. * Holds the projection (SELECT * or SELECT ?v1 ?v2 ...) and the WHERE clause. *

- * {@link #prologue()} captures PREFIX/BASE for SELECT; {@link #prefixHandler()} is derived from it + * {@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 { From 0e1859655f43760d510100769cf7bf82cd49ef3c Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 1 Apr 2026 10:14:29 +0200 Subject: [PATCH 4/5] #332 [SPARQL 1.1] - Parser and AST : BIND --- .../query/impl/parser/SparqlAstBuilder.java | 19 ++- .../impl/parser/SparqlParserBindTest.java | 120 ++++++++++++++++++ .../parser/SparqlParserValidationTest.java | 38 ++++++ 3 files changed, 174 insertions(+), 3 deletions(-) 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 082251207..ece60a4d7 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 @@ -1242,11 +1242,24 @@ public List termListFromObjectListPath(SparqlParser.ObjectListPathConte } /** - * Adds a BIND clause to the current group + * Adds a BIND clause to the current group. + * + * @throws QueryValidationException if the variable introduced by BIND is already visible + * in the group graph pattern up to this point, as required + * by the SPARQL 1.1 specification. */ public void addBind(BindAst bind) { - if (this.hasCurrentGroup()) { - this.currentGroup().add(bind); + if (!this.hasCurrentGroup()) { + return; + } + List current = this.currentGroup(); + Set alreadyVisible = variableScopeAnalyzer + .collectVisibleVariables(new GroupGraphPatternAst(current)); + if (alreadyVisible.contains(bind.variable().name())) { + throw new QueryValidationException( + "Variable ?" + bind.variable().name() + + " used in BIND is already declared in the same group graph pattern"); } + current.add(bind); } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java index b812b02ae..90fd32f52 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java @@ -88,4 +88,124 @@ void shouldParseBindWithArithmetic() { assertEquals("y", bindAst.variable().name()); assertNotNull(bindAst.expression()); } + + @Test + @DisplayName("BIND(\"hello\" AS ?label) — string literal expression") + void shouldParseBindWithStringLiteral() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND("hello" AS ?label) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + PatternAst last = where.patterns().getLast(); + assertInstanceOf(BindAst.class, last); + + BindAst bindAst = (BindAst) last; + assertInstanceOf(LiteralAst.class, bindAst.expression()); + assertEquals("label", bindAst.variable().name()); + assertEquals("\"hello\"", ((LiteralAst) bindAst.expression()).lexical()); + } + + @Test + @DisplayName("BIND( AS ?type) — IRI expression") + void shouldParseBindWithIri() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND( AS ?type) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + PatternAst last = where.patterns().getLast(); + assertInstanceOf(BindAst.class, last); + + BindAst bindAst = (BindAst) last; + assertInstanceOf(IriAst.class, bindAst.expression()); + assertEquals("type", bindAst.variable().name()); + assertEquals("", ((IriAst) bindAst.expression()).raw()); + } + + @Test + @DisplayName("Multiple BIND clauses in the same group") + void shouldParseMultipleBinds() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND(?s AS ?x) + BIND(?p AS ?y) + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(3, where.patterns().size()); + + assertInstanceOf(BgpAst.class, where.patterns().get(0)); + + BindAst first = assertInstanceOf(BindAst.class, where.patterns().get(1)); + assertEquals("x", first.variable().name()); + + BindAst second = assertInstanceOf(BindAst.class, where.patterns().get(2)); + assertEquals("y", second.variable().name()); + } + + @Test + @DisplayName("BIND inside an OPTIONAL block") + void shouldParseBindInsideOptional() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + OPTIONAL { BIND(?s AS ?x) } + } + """); + + assertNotNull(ast); + GroupGraphPatternAst where = ast.whereClause(); + assertEquals(2, where.patterns().size()); + + assertInstanceOf(BgpAst.class, where.patterns().get(0)); + + OptionalAst optional = assertInstanceOf(OptionalAst.class, where.patterns().get(1)); + GroupGraphPatternAst optionalGroup = assertInstanceOf(GroupGraphPatternAst.class, optional.ast()); + + assertEquals(1, optionalGroup.patterns().size()); + BindAst bind = assertInstanceOf(BindAst.class, optionalGroup.patterns().getFirst()); + assertEquals("x", bind.variable().name()); + } + + @Test + @DisplayName("SELECT ?x with BIND(... AS ?x) — BIND variable is visible in projection") + void shouldAcceptBindVariableInSelectProjection() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT ?x WHERE { + ?s ?p ?o . + BIND(?s AS ?x) + } + """); + + assertNotNull(ast); + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, ast); + assertEquals(1, select.projection().variables().size()); + assertEquals("x", select.projection().variables().getFirst().name()); + } } \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java index 09531a94e..ac103fdb8 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java @@ -121,4 +121,42 @@ void shouldRejectInvalidProjection() { } + @Nested + class BindValidationTest { + + @Test + @DisplayName("Should reject BIND when variable is already introduced by a triple pattern") + void shouldRejectBindVariableAlreadyVisibleFromTriple() { + SparqlParser parser = newParserDefault(); + + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + BIND(?o AS ?x) + } + """)); + + assertEquals("Variable ?x used in BIND is already declared in the same group graph pattern", + exception.getMessage()); + } + + @Test + @DisplayName("Should reject BIND when variable is already introduced by a previous BIND") + void shouldRejectBindVariableAlreadyVisibleFromPreviousBind() { + SparqlParser parser = newParserDefault(); + + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + BIND(?s AS ?x) + BIND(?p AS ?x) + } + """)); + + assertEquals("Variable ?x used in BIND is already declared in the same group graph pattern", + exception.getMessage()); + } + + } + } From aa1fbfc246a622763774d63cfff8bbaa03ebe81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Wed, 1 Apr 2026 15:53:19 +0200 Subject: [PATCH 5/5] Clean up Sonar warnings in BIND parser --- .../core/next/query/impl/parser/VariableScopeAnalyzer.java | 4 ++-- .../core/next/query/impl/parser/SparqlParserBindTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java index 842db855e..443a8f4b7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/VariableScopeAnalyzer.java @@ -81,9 +81,9 @@ case UnionAst(GroupGraphPatternAst left, GroupGraphPatternAst right) -> { collectVisibleVariables(left, visibleVariables); collectVisibleVariables(right, visibleVariables); } - case BindAst(TermAst expression, VarAst variable) -> { + case BindAst(TermAst expression, VarAst variable) -> visibleVariables.add(variable.name()); - } + case FilterAst ignored -> { // FILTER does not make a variable visible by itself. } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java index 90fd32f52..e80bd4935 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBindTest.java @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.*; @DisplayName("SPARQL 1.1 - Parser and AST : BIND") -public class SparqlParserBindTest extends AbstractSparqlParserFeatureTest { +class SparqlParserBindTest extends AbstractSparqlParserFeatureTest { @Test @DisplayName("BIND(?s AS ?x) — variable expression")