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 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..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 @@ -9,6 +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.*; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; @@ -56,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). */ @@ -363,19 +369,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, PathAst.from(p), o); + } + // --- Filters --- /** @@ -1278,12 +1289,221 @@ 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 PathAst.from(termFromIriRef(ctx.iriRef())); + } + if (ctx.A() != null) { + return PathAst.from(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(PathAst.from(termFromIriRef(ctx.iriRef()))); + } + return new InversePathAst(PathAst.from(iri("a"))); + } + if (ctx.iriRef() != null) { + return PathAst.from(termFromIriRef(ctx.iriRef())); + } + return PathAst.from(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; + } + + /** + * 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() + ">"); } /** @@ -1300,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/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/listener/BgpFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpFeature.java index 1d7a69321..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 @@ -84,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) { - TermAst p; - - boolean useVerbPath = verbPathIdx < verbPaths.size() && ( - verbSimpleIdx >= verbSimples.size() || - verbPaths.get(verbPathIdx).getStart().getTokenIndex() - < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex() - ); - - if (useVerbPath) { - p = builder().termFromVerbPath(verbPaths.get(verbPathIdx++)); - } else { - p = builder().termFromVerbSimple(verbSimples.get(verbSimpleIdx++)); - } - - List objects = builder().termListFromObjectListPath(objList); - for (TermAst o : objects) { - builder().addTriple(s, p, 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/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/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 38c8dc327..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 @@ -2,17 +2,25 @@ 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; /** * 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"); } } + /** + * 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, PathAst.from(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..76531cd93 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..d40dd2449 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..03c525fa2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..a8020820e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..a15d88052 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..72b1583b8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -0,0 +1,24 @@ +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, + ZeroOrMorePathAst, + OneOrMorePathAst, + 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/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..3382b0c2d --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..394ab23d8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java @@ -0,0 +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 new file mode 100644 index 000000000..b5a24bbe5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java @@ -0,0 +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); + } + } +} 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..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,7 +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 { @@ -32,4 +41,14 @@ 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); + } + + 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/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/SparqlParserSequenceTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java new file mode 100644 index 000000000..da05064a3 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java @@ -0,0 +1,211 @@ +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; +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; + +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 { + + private static final String PREFIX = """ + PREFIX ex: + """; + + @Test + void shouldParseSequencePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/ex:p2 ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(SequencePathAst.class, triple.predicate()); + } + + @Test + void shouldParseAlternativePath() { + SparqlParser parser = newParserDefault(); + 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(PREFIX + """ + SELECT * WHERE { + ?s ex:p* ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(ZeroOrMorePathAst.class, triple.predicate()); + } + + @Test + void shouldParseOptionalPath() { + SparqlParser parser = newParserDefault(); + + 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()); + } + + @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()); + } + + @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/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..f2d5afdbb --- /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 PathAst predicatePath(TermAst term) { + return PathAst.from(term); + } +}