From f3cc438f108af0ce12385e013561604862705e49 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Tue, 2 Jun 2026 11:21:12 +0200 Subject: [PATCH 1/6] path ast --- .../impl/sparql/ast/TriplePatternAst.java | 4 + .../sparql/ast/path/AlternativePathAst.java | 9 +++ .../impl/sparql/ast/path/InversePathAst.java | 7 ++ .../ast/path/NegatedPropertySetPathAst.java | 9 +++ .../sparql/ast/path/OneOrMorePathAst.java | 7 ++ .../impl/sparql/ast/path/OptionalPathAst.java | 7 ++ .../query/impl/sparql/ast/path/PathAst.java | 12 +++ .../sparql/ast/path/PredicatePathAst.java | 9 +++ .../impl/sparql/ast/path/SequencePathAst.java | 9 +++ .../sparql/ast/path/ZeroOrMorePathAst.java | 7 ++ .../impl/parser/SparqlParserSequenceTest.java | 73 +++++++++++++++++++ 11 files changed, 153 insertions(+) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java index 38c8dc327..d11a8e799 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java @@ -13,6 +13,10 @@ public record TriplePatternAst(TermAst subject, TermAst predicate, TermAst objec } } + public TriplePatternAst(TermAst subject, TermAst predicate, TermAst object) { + this(subject, new PredicatePathAst(predicate), object); + } + @Override public void accept(AstVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java new file mode 100644 index 000000000..20ac29e4e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java @@ -0,0 +1,9 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record AlternativePathAst(PathAst left, PathAst right) implements PathAst { + public AlternativePathAst { + if (left == null || right == null) { + throw new IllegalArgumentException("alternative operands are null"); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java new file mode 100644 index 000000000..fa4a880f1 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java @@ -0,0 +1,7 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record InversePathAst(PathAst pathAst) implements PathAst { + public InversePathAst { + if (pathAst == null) throw new IllegalArgumentException("path is null"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java new file mode 100644 index 000000000..96705d191 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java @@ -0,0 +1,9 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +import java.util.List; + +public record NegatedPropertySetPathAst(List excluded) implements PathAst { + public NegatedPropertySetPathAst { + excluded = excluded != null ? List.copyOf(excluded) : List.of(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java new file mode 100644 index 000000000..029a8a52e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java @@ -0,0 +1,7 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record OneOrMorePathAst(PathAst pathAst) implements PathAst { + public OneOrMorePathAst { + if (pathAst == null) throw new IllegalArgumentException("path is null"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java new file mode 100644 index 000000000..27c31bd39 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java @@ -0,0 +1,7 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record OptionalPathAst(PathAst pathAst) implements PathAst { + public OptionalPathAst { + if (pathAst == null) throw new IllegalArgumentException("path is null"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java new file mode 100644 index 000000000..e6b907475 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -0,0 +1,12 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public sealed interface PathAst + permits PredicatePathAst, + SequencePathAst, + AlternativePathAst, + ZeroOrMorePathAst, + OneOrMorePathAst, + OptionalPathAst, + InversePathAst, + NegatedPropertySetPathAst { +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java new file mode 100644 index 000000000..785a16e94 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java @@ -0,0 +1,9 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; + +public record PredicatePathAst(TermAst predicate) implements PathAst { + public PredicatePathAst { + if (predicate == null) throw new IllegalArgumentException("predicate is null"); + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java new file mode 100644 index 000000000..987ef990e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java @@ -0,0 +1,9 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record SequencePathAst(PathAst left, PathAst right) implements PathAst { + public SequencePathAst { + if (left == null || right == null) { + throw new IllegalArgumentException("sequence operands are null"); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java new file mode 100644 index 000000000..229ad033b --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java @@ -0,0 +1,7 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +public record ZeroOrMorePathAst(PathAst pathAst) implements PathAst { + public ZeroOrMorePathAst { + if (pathAst == null) throw new IllegalArgumentException("path is null"); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java new file mode 100644 index 000000000..a78b96fa7 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java @@ -0,0 +1,73 @@ +package fr.inria.corese.core.next.query.impl.parser; + +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.TriplePatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.AlternativePathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.OptionalPathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.SequencePathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.ZeroOrMorePathAst; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class SparqlParserSequenceTest extends AbstractSparqlParserFeatureTest { + + @Test + void shouldParseSequencePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ex:p1/ex:p2 ?o . + } + """); + + TriplePatternAst t = null; + assertInstanceOf(SequencePathAst.class, t.predicate()); + } + + @Test + void shouldParseAlternativePath() { + SparqlParser parser = newParserDefault(); + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ex:p1|ex:p2 ?o . + } + """); + + TriplePatternAst t = null; + assertInstanceOf(AlternativePathAst.class, t.predicate()); + } + + @Test + void shouldParseZeroOrMorePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ex:p* ?o . + } + """); + + TriplePatternAst t = null; + assertInstanceOf(ZeroOrMorePathAst.class, t.predicate()); + } + + @Test + void shouldParseOptionalPath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ex:p? ?o . + } + """); + + TriplePatternAst t = null; + assertInstanceOf(OptionalPathAst.class, t.predicate()); + } + + + +} From 6b9b3804918d36c38b07d0d50640f12c144ad36c Mon Sep 17 00:00:00 2001 From: pierrerene Date: Tue, 16 Jun 2026 15:47:47 +0200 Subject: [PATCH 2/6] fix(next/parser): TriplePatternAst PathAst predicate + addTriple overloads Use PathAst as the predicate type in triple patterns, with TriplePatternAst.of for simple terms and addTriple overloads for path and term predicates. Extend VariableScopeAnalyzer and parser tests for PredicatePathAst wrapping. Co-authored-by: Cursor --- .../query/impl/parser/SparqlAstBuilder.java | 15 ++++-- .../impl/parser/SparqlQueryAstBuilder.java | 2 +- .../support/VariableScopeAnalyzer.java | 27 +++++++++- .../impl/sparql/ast/TriplePatternAst.java | 9 +++- .../query/api/sparql/ast/SparqlAstTest.java | 35 ++++++------ .../AbstractSparqlParserFeatureTest.java | 8 +++ .../query/impl/parser/SparqlListenerTest.java | 3 +- .../impl/parser/SparqlParserAskQueryTest.java | 32 +++++------ .../impl/parser/SparqlParserBgpTest.java | 30 +++++------ .../SparqlParserConstructQueryTest.java | 14 ++--- .../impl/parser/SparqlParserDescribeTest.java | 2 +- .../parser/SparqlParserSelectQueryTest.java | 54 +++++++++---------- .../impl/parser/SparqlParserServiceTest.java | 2 +- .../parser/SparqlQueryAstBuilderTest.java | 10 ++-- .../query/impl/sparql/ast/SparqlAstTest.java | 33 ++++++------ .../ast/TriplePatternAstTestSupport.java | 26 +++++++++ 16 files changed, 187 insertions(+), 115 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.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 67f0c8e3f..542871d00 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 @@ -9,6 +9,8 @@ import fr.inria.corese.core.next.query.impl.parser.semantic.support.VariableScopeAnalyzer; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; @@ -363,19 +365,24 @@ protected SelectFrame getCurrentSelectFrame() { } /** - * Add a triple pattern (?s ?p ?o) to the current BGP (TriplesBlock). + * Add a triple pattern to the current BGP (TriplesBlock). * This must be called while inside a TriplesBlock. */ - public void addTriple(TermAst s, TermAst p, TermAst o) { + public void addTriple(TermAst s, PathAst p, TermAst o) { if (bgpStack.isEmpty()) { - // This should not happen if listener wiring is correct, but we fail loudly - // because BGP boundaries matter (TriplesBlock). throw new IllegalStateException("addTriple() called outside of TriplesBlock (BGP). " + "Ensure you call enterBgp() on enterTriplesBlock."); } bgpStack.peek().add(new TriplePatternAst(s, p, o)); } + /** + * Add a triple whose predicate is a single term (IRI, variable, etc.). + */ + public void addTriple(TermAst s, TermAst p, TermAst o) { + addTriple(s, new PredicatePathAst(p), o); + } + // --- Filters --- /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilder.java index 597eea73b..16ca86567 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilder.java @@ -174,7 +174,7 @@ public void exitConstructTemplate() { * Adds a triple to the CONSTRUCT template (inside {@code ConstructTriples}, not WHERE). */ public void addConstructTriple(TermAst s, TermAst p, TermAst o) { - constructTriples.add(new TriplePatternAst(s, p, o)); + constructTriples.add(TriplePatternAst.of(s, p, o)); } /** 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 e3867664c..d50617cac 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,6 +2,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; import java.util.*; @@ -169,10 +170,9 @@ private void collectVisibleVariables(PatternAst pattern, Set visibleVari switch (pattern) { case BgpAst(List triples) -> { - // Variables come from triple terms. for (TriplePatternAst triple : triples) { addIfVariable(triple.subject(), visibleVariables); - addIfVariable(triple.predicate(), visibleVariables); + collectVisibleVariablesFromPath(triple.predicate(), visibleVariables); addIfVariable(triple.object(), visibleVariables); } } @@ -429,4 +429,27 @@ private void addIfVariable(TermAst term, Set variables) { variables.add(name); } } + + private void collectVisibleVariablesFromPath(PathAst path, Set visibleVariables) { + switch (path) { + case PredicatePathAst(TermAst predicate) -> addIfVariable(predicate, visibleVariables); + case SequencePathAst(PathAst left, PathAst right) -> { + collectVisibleVariablesFromPath(left, visibleVariables); + collectVisibleVariablesFromPath(right, visibleVariables); + } + case AlternativePathAst(PathAst left, PathAst right) -> { + collectVisibleVariablesFromPath(left, visibleVariables); + collectVisibleVariablesFromPath(right, visibleVariables); + } + case ZeroOrMorePathAst(PathAst inner) -> collectVisibleVariablesFromPath(inner, visibleVariables); + case OneOrMorePathAst(PathAst inner) -> collectVisibleVariablesFromPath(inner, visibleVariables); + case OptionalPathAst(PathAst inner) -> collectVisibleVariablesFromPath(inner, visibleVariables); + case InversePathAst(PathAst inner) -> collectVisibleVariablesFromPath(inner, visibleVariables); + case NegatedPropertySetPathAst(List excluded) -> { + for (PathAst excludedPath : excluded) { + collectVisibleVariablesFromPath(excludedPath, visibleVariables); + } + } + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java index d11a8e799..403292fa4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java @@ -2,6 +2,8 @@ import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; import fr.inria.corese.core.next.query.impl.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; /** * A single triple pattern (s p o) in a BGP. @@ -13,8 +15,11 @@ public record TriplePatternAst(TermAst subject, TermAst predicate, TermAst objec } } - public TriplePatternAst(TermAst subject, TermAst predicate, TermAst object) { - this(subject, new PredicatePathAst(predicate), object); + /** + * Builds a triple whose predicate is a single term (IRI, variable, etc.). + */ + public static TriplePatternAst of(TermAst subject, TermAst predicate, TermAst object) { + return new TriplePatternAst(subject, new PredicatePathAst(predicate), object); } @Override diff --git a/src/test/java/fr/inria/corese/core/next/query/api/sparql/ast/SparqlAstTest.java b/src/test/java/fr/inria/corese/core/next/query/api/sparql/ast/SparqlAstTest.java index 558083c50..1bc30f9f9 100644 --- a/src/test/java/fr/inria/corese/core/next/query/api/sparql/ast/SparqlAstTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/api/sparql/ast/SparqlAstTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import static fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport.simplePredicateTerm; import static org.junit.jupiter.api.Assertions.*; /** @@ -211,9 +212,9 @@ class TriplePatternAstTest { @Test @DisplayName("creates with three terms") void valid() { - TriplePatternAst t = new TriplePatternAst(s, p, o); + TriplePatternAst t = TriplePatternAst.of(s, p, o); assertSame(s, t.subject()); - assertSame(p, t.predicate()); + assertSame(p, simplePredicateTerm(t)); assertSame(o, t.object()); } @@ -222,35 +223,35 @@ void valid() { void mixedTerms() { IriAst pred = new IriAst("a"); LiteralAst obj = new LiteralAst("lit", null, null); - TriplePatternAst t = new TriplePatternAst(s, pred, obj); + TriplePatternAst t = TriplePatternAst.of(s, pred, obj); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(IriAst.class, t.predicate()); + assertInstanceOf(IriAst.class, simplePredicateTerm(t)); assertInstanceOf(LiteralAst.class, t.object()); } @Test @DisplayName("throws when subject is null") void nullSubject() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(null, p, o)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(null, p, o)); } @Test @DisplayName("throws when predicate is null") void nullPredicate() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(s, null, o)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(s, null, o)); } @Test @DisplayName("throws when object is null") void nullObject() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(s, p, null)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(s, p, null)); } @Test @DisplayName("equals and hashCode for same triple") void equality() { - TriplePatternAst a = new TriplePatternAst(s, p, o); - TriplePatternAst b = new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")); + TriplePatternAst a = TriplePatternAst.of(s, p, o); + TriplePatternAst b = TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o")); assertEquals(a, b); assertEquals(a.hashCode(), b.hashCode()); } @@ -258,8 +259,8 @@ void equality() { @Test @DisplayName("not equal when component differs") void inequality() { - TriplePatternAst t = new TriplePatternAst(s, p, o); - assertNotEquals(t, new TriplePatternAst(new VarAst("x"), p, o)); + TriplePatternAst t = TriplePatternAst.of(s, p, o); + assertNotEquals(t, TriplePatternAst.of(new VarAst("x"), p, o)); } } @@ -269,7 +270,7 @@ void inequality() { @DisplayName("BgpAst") class BgpAstTest { - private final TriplePatternAst triple = new TriplePatternAst( + private final TriplePatternAst triple = TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")); @Test @@ -328,7 +329,7 @@ void implementsPatternAst() { class GroupGraphPatternAstTest { private final BgpAst bgp = new BgpAst(List.of( - new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); + TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o")))); @Test @DisplayName("creates with empty list") @@ -383,7 +384,7 @@ class QuerySelectAstTest { @DisplayName("SelectQueryAst stores and returns whereClause via whereClause()") void whereClauseAccessor() { GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( - new BgpAst(List.of(new TriplePatternAst( + new BgpAst(List.of(TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")))))); SparqlQueryAst q = new SelectQueryAst(where); assertSame(where, q.whereClause()); @@ -424,7 +425,7 @@ class QueryAskAstTest { @DisplayName("AskQueryAst stores and returns whereClause via whereClause()") void whereClauseAccessor() { GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( - new BgpAst(List.of(new TriplePatternAst( + new BgpAst(List.of(TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")))))); SparqlQueryAst q = new AskQueryAst(DatasetClauseAst.none(), where); assertSame(where, q.whereClause()); @@ -491,7 +492,7 @@ void varAst() { @Test @DisplayName("TriplePatternAst toString is non-empty") void triplePatternAst() { - TriplePatternAst t = new TriplePatternAst( + TriplePatternAst t = TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")); assertNotNull(t.toString()); assertFalse(t.toString().isEmpty()); @@ -501,7 +502,7 @@ void triplePatternAst() { @DisplayName("SelectQueryAst whereClause() used in composition") void queryAstWithNestedStructures() { BgpAst bgp = new BgpAst(List.of( - new TriplePatternAst(new VarAst("s"), new IriAst("a"), new VarAst("o")))); + TriplePatternAst.of(new VarAst("s"), new IriAst("a"), new VarAst("o")))); GroupGraphPatternAst where = new GroupGraphPatternAst(List.of(bgp)); SparqlQueryAst q = new SelectQueryAst(where); assertEquals(1, q.whereClause().patterns().size()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java index 57f675054..8dff667d6 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java @@ -2,6 +2,10 @@ import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport; + +import static fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport.simplePredicateTerm; public class AbstractSparqlParserFeatureTest { @@ -32,4 +36,8 @@ protected SparqlParser newParser(boolean failFast, boolean collectErrors) { .collectErrors(collectErrors) .build()); } + + protected static fr.inria.corese.core.next.query.impl.sparql.ast.TermAst simplePredicateTerm(TriplePatternAst triple) { + return TriplePatternAstTestSupport.simplePredicateTerm(triple); + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerTest.java index 213e5e39e..f7dad31e5 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerTest.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.List; +import static fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport.simplePredicateTerm; import static org.junit.jupiter.api.Assertions.*; /** @@ -67,7 +68,7 @@ void withSingleBgpDelegateWalkProducesAst() { assertEquals(1, bgp.triples().size()); TriplePatternAst t = bgp.triples().getFirst(); assertEquals("s", ((VarAst) t.subject()).name()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserAskQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserAskQueryTest.java index 2da88fd9c..1ad541d30 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserAskQueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserAskQueryTest.java @@ -37,11 +37,11 @@ void shouldParseBasicAskQueryTest() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("s", ((VarAst) t.subject()).name()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); } @@ -72,11 +72,11 @@ void shouldParseShortAskQueryTest() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(IriAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("", ((IriAst)t.subject()).raw()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); } @@ -109,16 +109,16 @@ void shouldIgnoreCommentBeforeQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); } @@ -152,16 +152,16 @@ void shouldIgnoreCommentAfterQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); } @@ -195,16 +195,16 @@ void shouldIgnoreCommentInTheMiddleOfQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBgpTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBgpTest.java index 31855f628..e1cbaed58 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBgpTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserBgpTest.java @@ -42,11 +42,11 @@ void shouldParseSingleTriplePatternInBgp() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("s", ((VarAst) t.subject()).name()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); } @@ -75,18 +75,18 @@ void shouldParsePropertyListSemicolonAndCommaIntoMultipleTriples() { TriplePatternAst t0 = bgp.triples().getFirst(); assertEquals("s", ((VarAst) t0.subject()).name()); - assertInstanceOf(IriAst.class, t0.predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) t0.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(t0)); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) simplePredicateTerm(t0)).raw()); TriplePatternAst t1 = bgp.triples().get(1); assertEquals("s", ((VarAst) t1.subject()).name()); - assertInstanceOf(IriAst.class, t1.predicate()); - assertEquals("foaf:name", ((IriAst) t1.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(t1)); + assertEquals("foaf:name", ((IriAst) simplePredicateTerm(t1)).raw()); assertEquals("n", ((VarAst) t1.object()).name()); TriplePatternAst t2 = bgp.triples().get(2); assertEquals("s", ((VarAst) t2.subject()).name()); - assertEquals("foaf:name", ((IriAst) t2.predicate()).raw()); + assertEquals("foaf:name", ((IriAst) simplePredicateTerm(t2)).raw()); assertEquals("n2", ((VarAst) t2.object()).name()); } @@ -148,8 +148,8 @@ void shouldParsePrefixedNameWithDigitOnlyLocalPart() { BgpAst bgp = (BgpAst) ast.whereClause().patterns().getFirst(); TriplePatternAst triple = bgp.triples().getFirst(); - assertInstanceOf(IriAst.class, triple.predicate()); - assertEquals("ex:1", ((IriAst) triple.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(triple)); + assertEquals("ex:1", ((IriAst) simplePredicateTerm(triple)).raw()); } @Test @@ -166,8 +166,8 @@ void shouldParsePrefixedNameWithHyphenInLocalPart() { BgpAst bgp = (BgpAst) ast.whereClause().patterns().getFirst(); TriplePatternAst triple = bgp.triples().getFirst(); - assertInstanceOf(IriAst.class, triple.predicate()); - assertEquals("ex:abc-def", ((IriAst) triple.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(triple)); + assertEquals("ex:abc-def", ((IriAst) simplePredicateTerm(triple)).raw()); } @Test @@ -184,8 +184,8 @@ void shouldParsePrefixedNameWithEscapedReservedCharacterInLocalPart() { BgpAst bgp = (BgpAst) ast.whereClause().patterns().getFirst(); TriplePatternAst triple = bgp.triples().getFirst(); - assertInstanceOf(IriAst.class, triple.predicate()); - assertEquals("ns:id\\=123", ((IriAst) triple.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(triple)); + assertEquals("ns:id\\=123", ((IriAst) simplePredicateTerm(triple)).raw()); } @Test @@ -202,8 +202,8 @@ void shouldParsePrefixedNameWithDefaultPrefix() { BgpAst bgp = (BgpAst) ast.whereClause().patterns().getFirst(); TriplePatternAst triple = bgp.triples().getFirst(); - assertInstanceOf(IriAst.class, triple.predicate()); - assertEquals(":foo", ((IriAst) triple.predicate()).raw()); + assertInstanceOf(IriAst.class, simplePredicateTerm(triple)); + assertEquals(":foo", ((IriAst) simplePredicateTerm(triple)).raw()); } @Test diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserConstructQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserConstructQueryTest.java index 90f810fe8..ab945ec89 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserConstructQueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserConstructQueryTest.java @@ -33,7 +33,7 @@ void shouldParseBasicConstructQuery() { TriplePatternAst templateTriple = construct.constructTemplate().triplePatternAsts().getFirst(); assertInstanceOf(VarAst.class, templateTriple.subject()); - assertInstanceOf(VarAst.class, templateTriple.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(templateTriple)); assertInstanceOf(VarAst.class, templateTriple.object()); GroupGraphPatternAst where = construct.whereClause(); @@ -106,26 +106,26 @@ private void assertTemplateWithBlankNodes(ConstructTemplateAst template) { TriplePatternAst firstTriple = template.triplePatternAsts().get(0); assertInstanceOf(VarAst.class, firstTriple.subject()); - assertInstanceOf(IriAst.class, firstTriple.predicate()); + assertInstanceOf(IriAst.class, simplePredicateTerm(firstTriple)); assertInstanceOf(IriAst.class, firstTriple.object()); assertEquals("x", ((VarAst) firstTriple.subject()).name()); - assertEquals("vcard:N", ((IriAst) firstTriple.predicate()).raw()); + assertEquals("vcard:N", ((IriAst) simplePredicateTerm(firstTriple)).raw()); assertTrue(((IriAst) firstTriple.object()).raw().startsWith("_:"), "object should be a blank node"); TriplePatternAst secondTriple = template.triplePatternAsts().get(1); assertInstanceOf(IriAst.class, secondTriple.subject()); - assertInstanceOf(IriAst.class, secondTriple.predicate()); + assertInstanceOf(IriAst.class, simplePredicateTerm(secondTriple)); assertInstanceOf(VarAst.class, secondTriple.object()); assertTrue(((IriAst) secondTriple.subject()).raw().startsWith("_:"), "subject should be a blank node"); - assertEquals("vcard:givenName", ((IriAst) secondTriple.predicate()).raw()); + assertEquals("vcard:givenName", ((IriAst) simplePredicateTerm(secondTriple)).raw()); assertEquals("gname", ((VarAst) secondTriple.object()).name()); TriplePatternAst thirdTriple = template.triplePatternAsts().get(2); assertInstanceOf(IriAst.class, thirdTriple.subject()); - assertInstanceOf(IriAst.class, thirdTriple.predicate()); + assertInstanceOf(IriAst.class, simplePredicateTerm(thirdTriple)); assertInstanceOf(VarAst.class, thirdTriple.object()); assertTrue(((IriAst) thirdTriple.subject()).raw().startsWith("_:"), "subject should be a blank node"); - assertEquals("vcard:familyName", ((IriAst) thirdTriple.predicate()).raw()); + assertEquals("vcard:familyName", ((IriAst) simplePredicateTerm(thirdTriple)).raw()); assertEquals("fname", ((VarAst) thirdTriple.object()).name()); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDescribeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDescribeTest.java index 4cc2e48e5..899dbe567 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDescribeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDescribeTest.java @@ -74,7 +74,7 @@ void bgpContainsOneTriple() { TriplePatternAst triple = bgp.triples().getFirst(); assertEquals("s", ((VarAst) triple.subject()).name()); - assertEquals("p", ((VarAst) triple.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(triple)).name()); assertEquals("o", ((VarAst) triple.object()).name()); } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSelectQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSelectQueryTest.java index c0369f9a1..bb337c506 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSelectQueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSelectQueryTest.java @@ -69,11 +69,11 @@ void shouldParseBasicSelectQueryTest() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("s", ((VarAst) t.subject()).name()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); // --- PROJECTION --- @@ -126,11 +126,11 @@ void shouldParseBasicSelectAllQueryTest() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("s", ((VarAst) t.subject()).name()); - assertEquals("p", ((VarAst) t.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(t)).name()); assertEquals("o", ((VarAst) t.object()).name()); // --- PROJECTION --- @@ -175,7 +175,7 @@ void shouldParseBasicSelectShortQueryTest() { TriplePatternAst t = bgp.triples().getFirst(); assertInstanceOf(VarAst.class, t.subject()); - assertInstanceOf(VarAst.class, t.predicate()); + assertInstanceOf(VarAst.class, simplePredicateTerm(t)); assertInstanceOf(VarAst.class, t.object()); assertEquals("s", ((VarAst) t.subject()).name()); @@ -260,7 +260,7 @@ void shouldParseDistinctWithMultipleVariablesAndGroupBy() { assertEquals(1, bgp.triples().size()); TriplePatternAst triple = bgp.triples().getFirst(); assertEquals("s", ((VarAst) triple.subject()).name()); - assertEquals("p", ((VarAst) triple.predicate()).name()); + assertEquals("p", ((VarAst) simplePredicateTerm(triple)).name()); assertEquals("o", ((VarAst) triple.object()).name()); SolutionModifierAst mod = select.solutionModifier(); @@ -846,10 +846,10 @@ void shouldParseSelectDistinctWithUnionQueryTest() { assertEquals(1, bgp.triples().size(), "BGP should contain 1 triple"); TriplePatternAst t0 = bgp.triples().getFirst(); assertInstanceOf(IriAst.class, t0.subject()); - assertInstanceOf(IriAst.class, t0.predicate()); + assertInstanceOf(IriAst.class, simplePredicateTerm(t0)); assertInstanceOf(VarAst.class, t0.object()); assertEquals("wd:Q458", ((IriAst) t0.subject()).raw()); - assertEquals("wdt:P150", ((IriAst) t0.predicate()).raw()); + assertEquals("wdt:P150", ((IriAst) simplePredicateTerm(t0)).raw()); assertEquals("country", ((VarAst) t0.object()).name()); // Second pattern: UNION of two branches @@ -869,7 +869,7 @@ void shouldParseSelectDistinctWithUnionQueryTest() { assertEquals(1, leftBgp.triples().size()); TriplePatternAst leftTriple = leftBgp.triples().getFirst(); assertEquals("country", ((VarAst) leftTriple.subject()).name()); - assertEquals("wdt:P36", ((IriAst) leftTriple.predicate()).raw()); + assertEquals("wdt:P36", ((IriAst) simplePredicateTerm(leftTriple)).raw()); assertEquals("city", ((VarAst) leftTriple.object()).name()); // Right branch: { ?city wdt:P17 ?country. ?city wdt:P31 wd:Q1637706. } → two TriplesBlocks → two BGPs @@ -887,12 +887,12 @@ void shouldParseSelectDistinctWithUnionQueryTest() { ); assertTrue(rightTriples.stream().anyMatch(t -> "city".equals(((VarAst) t.subject()).name()) - && "wdt:P17".equals(((IriAst) t.predicate()).raw()) + && "wdt:P17".equals(((IriAst) simplePredicateTerm(t)).raw()) && "country".equals(((VarAst) t.object()).name())), "Right branch should contain triple ?city wdt:P17 ?country"); assertTrue(rightTriples.stream().anyMatch(t -> "city".equals(((VarAst) t.subject()).name()) - && "wdt:P31".equals(((IriAst) t.predicate()).raw()) + && "wdt:P31".equals(((IriAst) simplePredicateTerm(t)).raw()) && "wd:Q1637706".equals(((IriAst) t.object()).raw())), "Right branch should contain triple ?city wdt:P31 wd:Q1637706"); @@ -941,16 +941,16 @@ void shouldIgnoreCommentBeforeQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); assertNotNull(selectQueryAst.solutionModifier()); @@ -994,16 +994,16 @@ void shouldIgnoreCommentAfterQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); assertNotNull(selectQueryAst.solutionModifier()); @@ -1047,16 +1047,16 @@ void shouldIgnoreCommentInTheMiddleOfQuery() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(VarAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("p", ((VarAst)bgpAst.triples().getLast().predicate()).name()); + assertInstanceOf(VarAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("p", ((VarAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).name()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); assertNotNull(selectQueryAst.solutionModifier()); @@ -1100,16 +1100,16 @@ void shouldIgnoreCommentInTheMiddleOfQueryWithIRI() { assertNotNull(bgpAst.triples().getFirst().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getFirst().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getFirst().predicate()); - assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) bgpAst.triples().getFirst().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())); + assertEquals(expectedRdfTypeIriAst().raw(), ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getFirst())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getFirst().object()); assertEquals("c", ((VarAst)bgpAst.triples().getFirst().object()).name()); assertNotNull(bgpAst.triples().getLast()); assertNotNull(bgpAst.triples().getLast().subject()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().subject()); assertEquals("s", ((VarAst)bgpAst.triples().getLast().subject()).name()); - assertInstanceOf(IriAst.class, bgpAst.triples().getLast().predicate()); - assertEquals("", ((IriAst)bgpAst.triples().getLast().predicate()).raw()); + assertInstanceOf(IriAst.class, TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())); + assertEquals("", ((IriAst) TriplePatternAstTestSupport.simplePredicateTerm(bgpAst.triples().getLast())).raw()); assertInstanceOf(VarAst.class, bgpAst.triples().getLast().object()); assertEquals("o", ((VarAst)bgpAst.triples().getLast().object()).name()); assertNotNull(selectQueryAst.solutionModifier()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserServiceTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserServiceTest.java index 12de0c450..bee38b6cd 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserServiceTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserServiceTest.java @@ -69,7 +69,7 @@ void serviceBodyShouldContainBgp() { BgpAst bgp = (BgpAst) body.patterns().getFirst(); assertEquals(1, bgp.triples().size()); assertEquals(new VarAst("s"), bgp.triples().getFirst().subject()); - assertEquals(expectedRdfTypeIriAst(), bgp.triples().getFirst().predicate()); + assertEquals(expectedRdfTypeIriAst(), TriplePatternAstTestSupport.simplePredicateTerm(bgp.triples().getFirst())); assertEquals(new IriAst(""), bgp.triples().getFirst().object()); } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilderTest.java index 7e759a7f2..dc0c5a6ee 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAstBuilderTest.java @@ -60,7 +60,7 @@ void shouldBuildSingleBgpWithOneTriple() { QueryAst expected = new SelectQueryAst( new GroupGraphPatternAst(List.of( new BgpAst(List.of( - new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")) + TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o")) )) )) ); @@ -85,8 +85,8 @@ void shouldBuildSingleBgpWithMultipleTriples() { BgpAst bgp = singleBgp(ast); assertEquals(2, bgp.triples().size()); - assertEquals(new TriplePatternAst(new VarAst("s"), expectedRdfTypeIriAst(), new IriAst("foaf:Person")), bgp.triples().get(0)); - assertEquals(new TriplePatternAst(new VarAst("s"), new IriAst("foaf:name"), new VarAst("n")), bgp.triples().get(1)); + assertEquals(TriplePatternAst.of(new VarAst("s"), expectedRdfTypeIriAst(), new IriAst("foaf:Person")), bgp.triples().get(0)); + assertEquals(TriplePatternAst.of(new VarAst("s"), new IriAst("foaf:name"), new VarAst("n")), bgp.triples().get(1)); } @Test @@ -284,7 +284,7 @@ void askConstructDescribeMustNotStartInsideSelectSubqueryFrame() { @Test void bgpAstShouldDefensivelyCopyTriplesList() { List triples = new ArrayList<>(); - triples.add(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o"))); + triples.add(TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o"))); BgpAst bgp = new BgpAst(triples); triples.clear(); @@ -295,7 +295,7 @@ void bgpAstShouldDefensivelyCopyTriplesList() { @Test void groupGraphPatternAstShouldDefensivelyCopyPatternsList() { List patterns = new ArrayList<>(); - patterns.add(new BgpAst(List.of(new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o"))))); + patterns.add(new BgpAst(List.of(TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o"))))); GroupGraphPatternAst group = new GroupGraphPatternAst(patterns); patterns.clear(); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java index 70963b491..c59ecd284 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import static fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport.simplePredicateTerm; import static org.junit.jupiter.api.Assertions.*; /** @@ -210,9 +211,9 @@ class TriplePatternAstTest { @Test @DisplayName("creates with three terms") void valid() { - TriplePatternAst t = new TriplePatternAst(s, p, o); + TriplePatternAst t = TriplePatternAst.of(s, p, o); assertSame(s, t.subject()); - assertSame(p, t.predicate()); + assertSame(p, simplePredicateTerm(t)); assertSame(o, t.object()); } @@ -221,35 +222,35 @@ void valid() { void mixedTerms() { IriAst pred = new IriAst("a"); LiteralAst obj = new LiteralAst("lit", null, null); - TriplePatternAst t = new TriplePatternAst(s, pred, obj); + TriplePatternAst t = TriplePatternAst.of(s, pred, obj); assertTrue(t.subject() instanceof VarAst); - assertTrue(t.predicate() instanceof IriAst); + assertInstanceOf(IriAst.class, simplePredicateTerm(t)); assertTrue(t.object() instanceof LiteralAst); } @Test @DisplayName("throws when subject is null") void nullSubject() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(null, p, o)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(null, p, o)); } @Test @DisplayName("throws when predicate is null") void nullPredicate() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(s, null, o)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(s, null, o)); } @Test @DisplayName("throws when object is null") void nullObject() { - assertThrows(IllegalArgumentException.class, () -> new TriplePatternAst(s, p, null)); + assertThrows(IllegalArgumentException.class, () -> TriplePatternAst.of(s, p, null)); } @Test @DisplayName("equals and hashCode for same triple") void equality() { - TriplePatternAst a = new TriplePatternAst(s, p, o); - TriplePatternAst b = new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")); + TriplePatternAst a = TriplePatternAst.of(s, p, o); + TriplePatternAst b = TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o")); assertEquals(a, b); assertEquals(a.hashCode(), b.hashCode()); } @@ -257,8 +258,8 @@ void equality() { @Test @DisplayName("not equal when component differs") void inequality() { - TriplePatternAst t = new TriplePatternAst(s, p, o); - assertNotEquals(t, new TriplePatternAst(new VarAst("x"), p, o)); + TriplePatternAst t = TriplePatternAst.of(s, p, o); + assertNotEquals(t, TriplePatternAst.of(new VarAst("x"), p, o)); } } @@ -268,7 +269,7 @@ void inequality() { @DisplayName("BgpAst") class BgpAstTest { - private final TriplePatternAst triple = new TriplePatternAst( + private final TriplePatternAst triple = TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")); @Test @@ -327,7 +328,7 @@ void implementsPatternAst() { class GroupGraphPatternAstTest { private final BgpAst bgp = new BgpAst(List.of( - new TriplePatternAst(new VarAst("s"), new VarAst("p"), new VarAst("o")))); + TriplePatternAst.of(new VarAst("s"), new VarAst("p"), new VarAst("o")))); @Test @DisplayName("creates with empty list") @@ -382,7 +383,7 @@ class QueryAstTest { @DisplayName("SelectQueryAst stores and returns whereClause via whereClause()") void whereClauseAccessor() { GroupGraphPatternAst where = new GroupGraphPatternAst(List.of( - new BgpAst(List.of(new TriplePatternAst( + new BgpAst(List.of(TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")))))); SparqlQueryAst q = new SelectQueryAst(where); assertSame(where, q.whereClause()); @@ -449,7 +450,7 @@ void varAst() { @Test @DisplayName("TriplePatternAst toString is non-empty") void triplePatternAst() { - TriplePatternAst t = new TriplePatternAst( + TriplePatternAst t = TriplePatternAst.of( new VarAst("s"), new VarAst("p"), new VarAst("o")); assertNotNull(t.toString()); assertFalse(t.toString().isEmpty()); @@ -459,7 +460,7 @@ void triplePatternAst() { @DisplayName("SelectQueryAst whereClause() used in composition") void queryAstWithNestedStructures() { BgpAst bgp = new BgpAst(List.of( - new TriplePatternAst(new VarAst("s"), new IriAst("a"), new VarAst("o")))); + TriplePatternAst.of(new VarAst("s"), new IriAst("a"), new VarAst("o")))); GroupGraphPatternAst where = new GroupGraphPatternAst(List.of(bgp)); SparqlQueryAst q = new SelectQueryAst(where); assertEquals(1, q.whereClause().patterns().size()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java new file mode 100644 index 000000000..070014642 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java @@ -0,0 +1,26 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Test helpers for {@link TriplePatternAst} predicates wrapped in {@link PredicatePathAst}. + */ +public final class TriplePatternAstTestSupport { + + private TriplePatternAstTestSupport() { + } + + public static TermAst simplePredicateTerm(TriplePatternAst triple) { + PathAst predicate = triple.predicate(); + assertInstanceOf(PredicatePathAst.class, predicate, + "expected a simple predicate wrapped in PredicatePathAst"); + return ((PredicatePathAst) predicate).predicate(); + } + + public static PredicatePathAst predicatePath(TermAst term) { + return new PredicatePathAst(term); + } +} From 1d5bed5a50cc34cf1e77004527e12a769808cc90 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Tue, 16 Jun 2026 15:51:24 +0200 Subject: [PATCH 3/6] feat(next/parser): map SPARQL property paths to PathAst in SparqlAstBuilder Walk path/* grammar rules into PathAst nodes and wire BgpFeature to addTriple with composed predicates. Add parser tests for sequence, alternative, modifiers, inverse, and negated property sets. Co-authored-by: Cursor --- .../query/impl/parser/SparqlAstBuilder.java | 109 +++++++++++++++- .../impl/parser/listener/BgpFeature.java | 10 +- .../AbstractSparqlParserFeatureTest.java | 11 ++ .../impl/parser/SparqlParserSequenceTest.java | 120 +++++++++++++----- 4 files changed, 209 insertions(+), 41 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 542871d00..6dd3b85df 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 @@ -9,8 +9,7 @@ import fr.inria.corese.core.next.query.impl.parser.semantic.support.VariableScopeAnalyzer; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; @@ -1285,12 +1284,108 @@ public TermAst termFromReplace(SparqlParser.StrReplaceExpressionContext ctx) { } /** - * Predicate as a property path. - * For simple triples without a composed path, this is just an iriRef or 'a'. - * Composed property paths will be expanded in a later phase if needed. + * Predicate as a SPARQL 1.1 property path. */ - public TermAst termFromVerbPath(SparqlParser.VerbPathContext ctx) { - return this.iri(ctx.getText()); + public PathAst pathFromVerbPath(SparqlParser.VerbPathContext ctx) { + return pathFromPath(ctx.path()); + } + + public PathAst pathFromPath(SparqlParser.PathContext ctx) { + return pathFromPathAlternative(ctx.pathAlternative()); + } + + private PathAst pathFromPathAlternative(SparqlParser.PathAlternativeContext ctx) { + return foldAlternatives(ctx.pathSequence().stream().map(this::pathFromPathSequence).toList()); + } + + private PathAst pathFromPathSequence(SparqlParser.PathSequenceContext ctx) { + return foldSequences(ctx.pathEltOrInverse().stream().map(this::pathFromPathEltOrInverse).toList()); + } + + private PathAst pathFromPathEltOrInverse(SparqlParser.PathEltOrInverseContext ctx) { + if (ctx.CARET() != null) { + return new InversePathAst(pathFromPathElt(ctx.pathElt())); + } + return pathFromPathElt(ctx.pathElt()); + } + + private PathAst pathFromPathElt(SparqlParser.PathEltContext ctx) { + PathAst primary = pathFromPathPrimary(ctx.pathPrimary()); + SparqlParser.PathModContext mod = ctx.pathMod(); + if (mod == null) { + return primary; + } + if (mod.QUESTION() != null) { + return new OptionalPathAst(primary); + } + if (mod.STAR() != null) { + return new ZeroOrMorePathAst(primary); + } + if (mod.PLUS() != null) { + return new OneOrMorePathAst(primary); + } + throw new QueryEvaluationException("Unexpected path modifier in " + ctx.getText()); + } + + private PathAst pathFromPathPrimary(SparqlParser.PathPrimaryContext ctx) { + if (ctx.iriRef() != null) { + return new PredicatePathAst(termFromIriRef(ctx.iriRef())); + } + if (ctx.A() != null) { + return new PredicatePathAst(iri("a")); + } + if (ctx.EXCLAMATION() != null) { + return pathFromPathNegatedPropertySet(ctx.pathNegatedPropertySet()); + } + if (ctx.path() != null) { + return pathFromPath(ctx.path()); + } + throw new QueryEvaluationException("Unexpected path primary in " + ctx.getText()); + } + + private PathAst pathFromPathNegatedPropertySet(SparqlParser.PathNegatedPropertySetContext ctx) { + List excluded; + if (ctx.L_PAREN() != null) { + excluded = ctx.pathOneInPropertySet().stream().map(this::pathFromPathOneInPropertySet).toList(); + } else { + excluded = List.of(pathFromPathOneInPropertySet(ctx.pathOneInPropertySet().getFirst())); + } + return new NegatedPropertySetPathAst(excluded); + } + + private PathAst pathFromPathOneInPropertySet(SparqlParser.PathOneInPropertySetContext ctx) { + if (ctx.CARET() != null) { + if (ctx.iriRef() != null) { + return new InversePathAst(new PredicatePathAst(termFromIriRef(ctx.iriRef()))); + } + return new InversePathAst(new PredicatePathAst(iri("a"))); + } + if (ctx.iriRef() != null) { + return new PredicatePathAst(termFromIriRef(ctx.iriRef())); + } + return new PredicatePathAst(iri("a")); + } + + private PathAst foldAlternatives(List parts) { + if (parts.isEmpty()) { + throw new QueryEvaluationException("Empty property path alternative"); + } + PathAst result = parts.getFirst(); + for (int i = 1; i < parts.size(); i++) { + result = new AlternativePathAst(result, parts.get(i)); + } + return result; + } + + private PathAst foldSequences(List parts) { + if (parts.isEmpty()) { + throw new QueryEvaluationException("Empty property path sequence"); + } + PathAst result = parts.getFirst(); + for (int i = 1; i < parts.size(); i++) { + result = new SequencePathAst(result, parts.get(i)); + } + return result; } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java index 1d7a69321..88ded2eb6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java @@ -3,6 +3,8 @@ 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.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import java.util.List; @@ -96,7 +98,7 @@ public void exitTriplesSameSubjectPath(SparqlParser.TriplesSameSubjectPathContex int verbSimpleIdx = 0; for (SparqlParser.ObjectListPathContext objList : objLists) { - TermAst p; + PathAst predicate; boolean useVerbPath = verbPathIdx < verbPaths.size() && ( verbSimpleIdx >= verbSimples.size() || @@ -105,14 +107,14 @@ public void exitTriplesSameSubjectPath(SparqlParser.TriplesSameSubjectPathContex ); if (useVerbPath) { - p = builder().termFromVerbPath(verbPaths.get(verbPathIdx++)); + predicate = builder().pathFromVerbPath(verbPaths.get(verbPathIdx++)); } else { - p = builder().termFromVerbSimple(verbSimples.get(verbSimpleIdx++)); + predicate = new PredicatePathAst(builder().termFromVerbSimple(verbSimples.get(verbSimpleIdx++))); } List objects = builder().termListFromObjectListPath(objList); for (TermAst o : objects) { - builder().addTriple(s, p, o); + builder().addTriple(s, predicate, o); } } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java index 8dff667d6..7d2e418c5 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AbstractSparqlParserFeatureTest.java @@ -1,11 +1,16 @@ package fr.inria.corese.core.next.query.impl.parser; import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst; +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.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport; +import fr.inria.corese.core.next.query.impl.sparql.ast.WhereClauseQueryAst; import static fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport.simplePredicateTerm; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; public class AbstractSparqlParserFeatureTest { @@ -40,4 +45,10 @@ protected SparqlParser newParser(boolean failFast, boolean collectErrors) { protected static fr.inria.corese.core.next.query.impl.sparql.ast.TermAst simplePredicateTerm(TriplePatternAst triple) { return TriplePatternAstTestSupport.simplePredicateTerm(triple); } + + protected static TriplePatternAst firstWhereTriple(QueryAst ast) { + WhereClauseQueryAst query = (WhereClauseQueryAst) ast; + BgpAst bgp = (BgpAst) query.whereClause().patterns().getFirst(); + return bgp.triples().getFirst(); + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java index a78b96fa7..7f2534c8d 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java @@ -1,10 +1,13 @@ package fr.inria.corese.core.next.query.impl.parser; 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.TriplePatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.AlternativePathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.InversePathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.NegatedPropertySetPathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.OneOrMorePathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.OptionalPathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.SequencePathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.ZeroOrMorePathAst; import org.junit.jupiter.api.Test; @@ -13,61 +16,118 @@ public class SparqlParserSequenceTest extends AbstractSparqlParserFeatureTest { + private static final String PREFIX = """ + PREFIX ex: + """; + @Test void shouldParseSequencePath() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(""" - SELECT * WHERE { - ?s ex:p1/ex:p2 ?o . - } - """); + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/ex:p2 ?o . + } + """); - TriplePatternAst t = null; - assertInstanceOf(SequencePathAst.class, t.predicate()); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(SequencePathAst.class, triple.predicate()); } @Test void shouldParseAlternativePath() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(""" - SELECT * WHERE { - ?s ex:p1|ex:p2 ?o . - } - """); - - TriplePatternAst t = null; - assertInstanceOf(AlternativePathAst.class, t.predicate()); + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1|ex:p2 ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(AlternativePathAst.class, triple.predicate()); } @Test void shouldParseZeroOrMorePath() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(""" - SELECT * WHERE { - ?s ex:p* ?o . - } - """); + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p* ?o . + } + """); - TriplePatternAst t = null; - assertInstanceOf(ZeroOrMorePathAst.class, t.predicate()); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(ZeroOrMorePathAst.class, triple.predicate()); } @Test void shouldParseOptionalPath() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(""" - SELECT * WHERE { - ?s ex:p? ?o . - } - """); + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p? ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(OptionalPathAst.class, triple.predicate()); + } + + @Test + void shouldParseOneOrMorePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p+ ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(OneOrMorePathAst.class, triple.predicate()); + } - TriplePatternAst t = null; - assertInstanceOf(OptionalPathAst.class, t.predicate()); + @Test + void shouldParseInversePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ^ex:p ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(InversePathAst.class, triple.predicate()); + } + + @Test + void shouldParseNegatedPropertySetPath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s !(ex:p1|ex:p2) ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(NegatedPropertySetPathAst.class, triple.predicate()); } + @Test + void shouldParseSimplePredicatePathFromVerbSimple() { + SparqlParser parser = newParserDefault(); + QueryAst ast = parser.parse(""" + SELECT * WHERE { + ?s ?p ?o . + } + """); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(PredicatePathAst.class, triple.predicate()); + } } From 1a9e17eb83c83e4a0be423d15530ba4324008253 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Wed, 17 Jun 2026 11:35:30 +0200 Subject: [PATCH 4/6] path ast parser --- .../query/impl/parser/SparqlAstBuilder.java | 139 ++++++++++++++++-- .../impl/parser/listener/BgpFeature.java | 46 ++---- .../impl/sparql/ast/TriplePatternAst.java | 3 +- .../query/impl/sparql/ast/path/PathAst.java | 9 ++ .../impl/parser/SparqlParserSequenceTest.java | 78 ++++++++++ .../ast/TriplePatternAstTestSupport.java | 4 +- 6 files changed, 231 insertions(+), 48 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 6dd3b85df..2e192b71a 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 @@ -57,6 +57,11 @@ public abstract class SparqlAstBuilder { */ protected final Deque> bgpStack = new ArrayDeque<>(); + /** + * Counter for anonymous blank nodes created while expanding {@code [ ... ]} and {@code ( ... )} subjects. + */ + private int anonymousBlankNodeCounter; + /** * Stack of currently open SELECT operations (top-level SELECT and nested SELECT subqueries). */ @@ -379,7 +384,7 @@ public void addTriple(TermAst s, PathAst p, TermAst o) { * Add a triple whose predicate is a single term (IRI, variable, etc.). */ public void addTriple(TermAst s, TermAst p, TermAst o) { - addTriple(s, new PredicatePathAst(p), o); + addTriple(s, PathAst.from(p), o); } // --- Filters --- @@ -1329,10 +1334,10 @@ private PathAst pathFromPathElt(SparqlParser.PathEltContext ctx) { private PathAst pathFromPathPrimary(SparqlParser.PathPrimaryContext ctx) { if (ctx.iriRef() != null) { - return new PredicatePathAst(termFromIriRef(ctx.iriRef())); + return PathAst.from(termFromIriRef(ctx.iriRef())); } if (ctx.A() != null) { - return new PredicatePathAst(iri("a")); + return PathAst.from(iri("a")); } if (ctx.EXCLAMATION() != null) { return pathFromPathNegatedPropertySet(ctx.pathNegatedPropertySet()); @@ -1356,14 +1361,14 @@ private PathAst pathFromPathNegatedPropertySet(SparqlParser.PathNegatedPropertyS private PathAst pathFromPathOneInPropertySet(SparqlParser.PathOneInPropertySetContext ctx) { if (ctx.CARET() != null) { if (ctx.iriRef() != null) { - return new InversePathAst(new PredicatePathAst(termFromIriRef(ctx.iriRef()))); + return new InversePathAst(PathAst.from(termFromIriRef(ctx.iriRef()))); } - return new InversePathAst(new PredicatePathAst(iri("a"))); + return new InversePathAst(PathAst.from(iri("a"))); } if (ctx.iriRef() != null) { - return new PredicatePathAst(termFromIriRef(ctx.iriRef())); + return PathAst.from(termFromIriRef(ctx.iriRef())); } - return new PredicatePathAst(iri("a")); + return PathAst.from(iri("a")); } private PathAst foldAlternatives(List parts) { @@ -1388,6 +1393,119 @@ private PathAst foldSequences(List parts) { return result; } + /** + * Creates a fresh anonymous blank node label for expanded BGP triples. + */ + public TermAst newAnonymousBlankNode() { + return iri("_:b" + anonymousBlankNodeCounter++); + } + + /** + * Expands a {@code propertyListPathNotEmpty} into triple patterns for the given subject. + */ + public void addTriplesFromPropertyListPath( + TermAst subject, + SparqlParser.PropertyListPathNotEmptyContext propertyList) { + var verbPaths = propertyList.verbPath(); + var verbSimples = propertyList.verbSimple(); + var objectLists = propertyList.objectListPath(); + + int verbPathIdx = 0; + int verbSimpleIdx = 0; + + for (SparqlParser.ObjectListPathContext objectList : objectLists) { + PathAst predicate = predicateFromPropertyListPath( + verbPaths, verbSimples, verbPathIdx, verbSimpleIdx); + if (verbPathIdx < verbPaths.size() + && (verbSimpleIdx >= verbSimples.size() + || verbPaths.get(verbPathIdx).getStart().getTokenIndex() + < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex())) { + verbPathIdx++; + } else { + verbSimpleIdx++; + } + + for (TermAst object : termListFromObjectListPath(objectList)) { + addTriple(subject, predicate, object); + } + } + } + + private PathAst predicateFromPropertyListPath( + List verbPaths, + List verbSimples, + int verbPathIdx, + int verbSimpleIdx) { + boolean useVerbPath = verbPathIdx < verbPaths.size() + && (verbSimpleIdx >= verbSimples.size() + || verbPaths.get(verbPathIdx).getStart().getTokenIndex() + < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex()); + if (useVerbPath) { + return pathFromVerbPath(verbPaths.get(verbPathIdx)); + } + return PathAst.from(termFromVerbSimple(verbSimples.get(verbSimpleIdx))); + } + + /** + * Builds triple patterns for a {@code triplesNodePath} subject and returns its head term. + */ + public TermAst subjectFromTriplesNodePath(SparqlParser.TriplesNodePathContext ctx) { + if (ctx.blankNodePropertyListPath() != null) { + return subjectFromBlankNodePropertyListPath(ctx.blankNodePropertyListPath()); + } + if (ctx.collectionPath() != null) { + return subjectFromCollectionPath(ctx.collectionPath()); + } + throw new QueryEvaluationException("Unexpected triples node path: " + ctx.getText()); + } + + private TermAst subjectFromBlankNodePropertyListPath( + SparqlParser.BlankNodePropertyListPathContext ctx) { + TermAst blankNode = newAnonymousBlankNode(); + if (ctx.propertyListPathNotEmpty() != null) { + addTriplesFromPropertyListPath(blankNode, ctx.propertyListPathNotEmpty()); + } + return blankNode; + } + + private TermAst subjectFromCollectionPath(SparqlParser.CollectionPathContext ctx) { + List nodes = ctx.graphNodePath(); + if (nodes.isEmpty()) { + throw new QueryEvaluationException("Empty RDF collection in triple pattern"); + } + + TermAst head = newAnonymousBlankNode(); + TermAst current = head; + for (int i = 0; i < nodes.size(); i++) { + addTriple(current, rdfTerm(RDF.first), termFromGraphNodePath(nodes.get(i))); + if (i == nodes.size() - 1) { + addTriple(current, rdfTerm(RDF.rest), rdfTerm(RDF.nil)); + } else { + TermAst next = newAnonymousBlankNode(); + addTriple(current, rdfTerm(RDF.rest), next); + current = next; + } + } + return head; + } + + /** + * Graph node inside a property-path triple (variable/term, blank node property list, or collection). + */ + public TermAst termFromGraphNodePath(SparqlParser.GraphNodePathContext ctx) { + if (ctx.varOrTerm() != null) { + return termFromVarOrTerm(ctx.varOrTerm()); + } + if (ctx.triplesNodePath() != null) { + return subjectFromTriplesNodePath(ctx.triplesNodePath()); + } + throw new QueryEvaluationException("Unexpected graph node path: " + ctx.getText()); + } + + private TermAst rdfTerm(RDF term) { + return new IriAst("<" + term.getIRI().stringValue() + ">"); + } + /** * Predicate as a simple variable (e.g. ?p used as a predicate). */ @@ -1402,12 +1520,7 @@ public TermAst termFromVerbSimple(SparqlParser.VerbSimpleContext ctx) { public List termListFromObjectListPath(SparqlParser.ObjectListPathContext ctx) { List out = new ArrayList<>(); for (var objPath : ctx.objectPath()) { - var graphNodePath = objPath.graphNodePath(); - if (graphNodePath.varOrTerm() != null) { - out.add(termFromVarOrTerm(graphNodePath.varOrTerm())); - } else { - out.add(this.iri(graphNodePath.getText())); - } + out.add(termFromGraphNodePath(objPath.graphNodePath())); } return out; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java index 88ded2eb6..72bd28d31 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java @@ -3,8 +3,6 @@ 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.TermAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import java.util.List; @@ -86,36 +84,22 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) { @Override public void exitTriplesSameSubjectPath(SparqlParser.TriplesSameSubjectPathContext ctx) { - TermAst s = builder().termFromVarOrTerm(ctx.varOrTerm()); - var pl = ctx.propertyListPathNotEmpty(); - if (pl == null) return; - - var verbPaths = pl.verbPath(); - var verbSimples = pl.verbSimple(); - var objLists = pl.objectListPath(); - - int verbPathIdx = 0; - int verbSimpleIdx = 0; - - for (SparqlParser.ObjectListPathContext objList : objLists) { - PathAst predicate; - - boolean useVerbPath = verbPathIdx < verbPaths.size() && ( - verbSimpleIdx >= verbSimples.size() || - verbPaths.get(verbPathIdx).getStart().getTokenIndex() - < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex() - ); - - if (useVerbPath) { - predicate = builder().pathFromVerbPath(verbPaths.get(verbPathIdx++)); - } else { - predicate = new PredicatePathAst(builder().termFromVerbSimple(verbSimples.get(verbSimpleIdx++))); - } - - List objects = builder().termListFromObjectListPath(objList); - for (TermAst o : objects) { - builder().addTriple(s, predicate, o); + SparqlAstBuilder b = builder(); + if (ctx.varOrTerm() != null) { + var propertyList = ctx.propertyListPathNotEmpty(); + if (propertyList == null) { + return; } + b.addTriplesFromPropertyListPath(b.termFromVarOrTerm(ctx.varOrTerm()), propertyList); + return; + } + if (ctx.triplesNodePath() == null) { + return; + } + TermAst subject = b.subjectFromTriplesNodePath(ctx.triplesNodePath()); + SparqlParser.PropertyListPathContext propertyListPath = ctx.propertyListPath(); + if (propertyListPath != null && propertyListPath.propertyListPathNotEmpty() != null) { + b.addTriplesFromPropertyListPath(subject, propertyListPath.propertyListPathNotEmpty()); } } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java index 403292fa4..91448aaa3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java @@ -3,7 +3,6 @@ import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; import fr.inria.corese.core.next.query.impl.parser.semantic.support.VisitableAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; /** * A single triple pattern (s p o) in a BGP. @@ -19,7 +18,7 @@ public record TriplePatternAst(TermAst subject, TermAst predicate, TermAst objec * Builds a triple whose predicate is a single term (IRI, variable, etc.). */ public static TriplePatternAst of(TermAst subject, TermAst predicate, TermAst object) { - return new TriplePatternAst(subject, new PredicatePathAst(predicate), object); + return new TriplePatternAst(subject, PathAst.from(predicate), object); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java index e6b907475..c543d9cc1 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; + public sealed interface PathAst permits PredicatePathAst, SequencePathAst, @@ -9,4 +11,11 @@ public sealed interface PathAst OptionalPathAst, InversePathAst, NegatedPropertySetPathAst { + + /** + * Wraps a single term (IRI, variable, etc.) as a property path. + */ + static PathAst from(TermAst term) { + return new PredicatePathAst(term); + } } \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java index 7f2534c8d..da05064a3 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java @@ -1,7 +1,11 @@ package fr.inria.corese.core.next.query.impl.parser; +import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.WhereClauseQueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.AlternativePathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.InversePathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.NegatedPropertySetPathAst; @@ -12,7 +16,9 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.path.ZeroOrMorePathAst; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SparqlParserSequenceTest extends AbstractSparqlParserFeatureTest { @@ -130,4 +136,76 @@ void shouldParseSimplePredicatePathFromVerbSimple() { TriplePatternAst triple = firstWhereTriple(ast); assertInstanceOf(PredicatePathAst.class, triple.predicate()); } + + @Test + void shouldParseBlankNodePropertyListPathAsSubject() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + [ ex:p ?o ] . + } + """); + + BgpAst bgp = whereBgp(ast); + assertEquals(1, bgp.triples().size()); + + TriplePatternAst triple = bgp.triples().getFirst(); + assertTrue(((IriAst) triple.subject()).raw().startsWith("_:b")); + assertEquals("ex:p", predicateIri(triple)); + assertEquals("o", ((VarAst) triple.object()).name()); + } + + @Test + void shouldParseBlankNodePropertyListPathWithExternalProperties() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + [ ex:p ?o ] ex:q ?x . + } + """); + + BgpAst bgp = whereBgp(ast); + assertEquals(2, bgp.triples().size()); + + TriplePatternAst internal = bgp.triples().getFirst(); + TriplePatternAst external = bgp.triples().get(1); + assertEquals(internal.subject(), external.subject()); + assertEquals("ex:p", predicateIri(internal)); + assertEquals("o", ((VarAst) internal.object()).name()); + assertEquals("ex:q", predicateIri(external)); + assertEquals("x", ((VarAst) external.object()).name()); + } + + @Test + void shouldParseBlankNodePropertyListPathAsObject() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p [ ex:q ?x ] . + } + """); + + BgpAst bgp = whereBgp(ast); + assertEquals(2, bgp.triples().size()); + + TriplePatternAst inner = bgp.triples().getFirst(); + TriplePatternAst outer = bgp.triples().get(1); + assertEquals("s", ((VarAst) outer.subject()).name()); + assertEquals("ex:p", predicateIri(outer)); + assertEquals(outer.object(), inner.subject()); + assertEquals("ex:q", predicateIri(inner)); + assertEquals("x", ((VarAst) inner.object()).name()); + } + + private static BgpAst whereBgp(QueryAst ast) { + WhereClauseQueryAst query = (WhereClauseQueryAst) ast; + return (BgpAst) query.whereClause().patterns().getFirst(); + } + + private static String predicateIri(TriplePatternAst triple) { + return ((IriAst) ((PredicatePathAst) triple.predicate()).predicate()).raw(); + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java index 070014642..f2d5afdbb 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAstTestSupport.java @@ -20,7 +20,7 @@ public static TermAst simplePredicateTerm(TriplePatternAst triple) { return ((PredicatePathAst) predicate).predicate(); } - public static PredicatePathAst predicatePath(TermAst term) { - return new PredicatePathAst(term); + public static PathAst predicatePath(TermAst term) { + return PathAst.from(term); } } From 631ee4a8c9d6011b4f9cea6595d38867b3580663 Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Mon, 22 Jun 2026 13:33:23 +0200 Subject: [PATCH 5/6] ast visitor after rebase --- .../parser/semantic/support/AbstractAstVisitor.java | 6 ++++++ .../impl/parser/semantic/support/AstVisitor.java | 2 ++ .../query/impl/sparql/ast/TriplePatternAst.java | 2 +- .../impl/sparql/ast/path/AlternativePathAst.java | 13 +++++++++++++ .../query/impl/sparql/ast/path/InversePathAst.java | 10 ++++++++++ .../sparql/ast/path/NegatedPropertySetPathAst.java | 12 ++++++++++++ .../impl/sparql/ast/path/OneOrMorePathAst.java | 10 ++++++++++ .../query/impl/sparql/ast/path/OptionalPathAst.java | 10 ++++++++++ .../next/query/impl/sparql/ast/path/PathAst.java | 5 ++++- .../impl/sparql/ast/path/PredicatePathAst.java | 9 +++++++++ .../query/impl/sparql/ast/path/SequencePathAst.java | 13 +++++++++++++ .../impl/sparql/ast/path/ZeroOrMorePathAst.java | 10 ++++++++++ 12 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AbstractAstVisitor.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AbstractAstVisitor.java index fd5aff3c3..b70036c43 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AbstractAstVisitor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AbstractAstVisitor.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.parser.semantic.support; import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; /** * Default implementation for AstVisitor @@ -101,4 +102,9 @@ public void visit(ServiceAst ast) { public void visit(GraphRefAst ast) { } + + @Override + public void visit(PathAst ast) { + + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AstVisitor.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AstVisitor.java index c36f05433..5f4859d46 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AstVisitor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/AstVisitor.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.parser.semantic.support; import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; /** * Visitor interface for the AST hierarchy @@ -25,4 +26,5 @@ public interface AstVisitor { void visit(PrefixDeclarationAst ast); void visit(ServiceAst ast); void visit(GraphRefAst ast); + void visit(PathAst ast); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java index 91448aaa3..a9e0a30a5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java @@ -7,7 +7,7 @@ /** * A single triple pattern (s p o) in a BGP. */ -public record TriplePatternAst(TermAst subject, TermAst predicate, TermAst object) implements VisitableAst { +public record TriplePatternAst(TermAst subject, PathAst predicate, TermAst object) implements VisitableAst { public TriplePatternAst { if (subject == null || predicate == null || object == null) { throw new IllegalArgumentException("subject, predicate and object must be non-null"); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java index 20ac29e4e..76531cd93 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java @@ -1,9 +1,22 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record AlternativePathAst(PathAst left, PathAst right) implements PathAst { public AlternativePathAst { if (left == null || right == null) { throw new IllegalArgumentException("alternative operands are null"); } } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.left != null) { + this.left.accept(visitor); + } + if(this.right != null) { + this.right.accept(visitor); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java index fa4a880f1..d40dd2449 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java @@ -1,7 +1,17 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record InversePathAst(PathAst pathAst) implements PathAst { public InversePathAst { if (pathAst == null) throw new IllegalArgumentException("path is null"); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.pathAst != null) { + this.pathAst.accept(visitor); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java index 96705d191..03c525fa2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java @@ -1,9 +1,21 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + import java.util.List; public record NegatedPropertySetPathAst(List excluded) implements PathAst { public NegatedPropertySetPathAst { excluded = excluded != null ? List.copyOf(excluded) : List.of(); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.excluded != null) { + this.excluded.forEach(excludedPath -> { + excludedPath.accept(visitor); + }); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java index 029a8a52e..a8020820e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java @@ -1,7 +1,17 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record OneOrMorePathAst(PathAst pathAst) implements PathAst { public OneOrMorePathAst { if (pathAst == null) throw new IllegalArgumentException("path is null"); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.pathAst != null) { + this.pathAst.accept(visitor); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java index 27c31bd39..a15d88052 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java @@ -1,7 +1,17 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record OptionalPathAst(PathAst pathAst) implements PathAst { public OptionalPathAst { if (pathAst == null) throw new IllegalArgumentException("path is null"); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.pathAst != null) { + this.pathAst.accept(visitor); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java index c543d9cc1..72b1583b8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -1,8 +1,11 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.VisitableAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; public sealed interface PathAst + extends VisitableAst permits PredicatePathAst, SequencePathAst, AlternativePathAst, @@ -10,7 +13,7 @@ public sealed interface PathAst OneOrMorePathAst, OptionalPathAst, InversePathAst, - NegatedPropertySetPathAst { + NegatedPropertySetPathAst{ /** * Wraps a single term (IRI, variable, etc.) as a property path. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java index 785a16e94..3382b0c2d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java @@ -1,9 +1,18 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; public record PredicatePathAst(TermAst predicate) implements PathAst { public PredicatePathAst { if (predicate == null) throw new IllegalArgumentException("predicate is null"); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.predicate != null) { + this.predicate.accept(visitor); + } + } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java index 987ef990e..394ab23d8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java @@ -1,9 +1,22 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record SequencePathAst(PathAst left, PathAst right) implements PathAst { public SequencePathAst { if (left == null || right == null) { throw new IllegalArgumentException("sequence operands are null"); } } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.left != null) { + this.left.accept(visitor); + } + if(this.right != null) { + this.right.accept(visitor); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java index 229ad033b..b5a24bbe5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java @@ -1,7 +1,17 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + public record ZeroOrMorePathAst(PathAst pathAst) implements PathAst { public ZeroOrMorePathAst { if (pathAst == null) throw new IllegalArgumentException("path is null"); } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + if(this.pathAst != null) { + this.pathAst.accept(visitor); + } + } } From 523b18008eddb3a57583eb959f4cd5853d5e36e6 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Wed, 17 Jun 2026 11:42:15 +0200 Subject: [PATCH 6/6] bump gradle 9.1 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 517434985..abf3adcc0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -308,7 +308,7 @@ tasks.named("compileJava") { // Ensure sources JAR includes generated sources and depends on code generation tasks.named("sourcesJar") { - dependsOn("generateGrammarSource", "javaccSparqlCorese") + dependsOn("generateGrammarSource", "generateTestGrammarSource", "javaccSparqlCorese") from(javaccGeneratedDir) from(antlrPackageDir) includeEmptyDirs = false