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..f1aa39fcf 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 @@ -1,5 +1,6 @@ package fr.inria.corese.core.next.query.impl.parser; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; @@ -9,6 +10,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; @@ -20,6 +22,8 @@ */ public abstract class SparqlAstBuilder { + private static final String GENERATED_BLANK_NODE_PREFIX = IOConstants.BLANK_NODE_PREFIX + "b"; + /** * Parser options (e.g. for future use: strict mode, base IRI). */ @@ -56,6 +60,16 @@ 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; + + /** + * Blank node labels already present in the query or generated by this builder. + */ + private final Set blankNodeLabels = new HashSet<>(); + /** * Stack of currently open SELECT operations (top-level SELECT and nested SELECT subqueries). */ @@ -140,6 +154,10 @@ protected SparqlAstBuilder(SparqlParserOptions options) { this.baseUri = options.getBaseIRI(); } + public void reserveBlankNodeLabels(Collection labels) { + blankNodeLabels.addAll(Objects.requireNonNull(labels, "labels")); + } + // --- Construction entry points (called by listener) --- public void setBaseUri(String uri) { @@ -363,19 +381,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 --- /** @@ -869,14 +892,15 @@ public TermAst termFromNumericLiteralUnsigned(SparqlParser.NumericLiteralUnsigne } public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) { - return this.iri(ctx.getText()); + String label = ctx.getText(); + blankNodeLabels.add(label); + return this.iri(label); } /** * One {@code groupCondition} : {@code builtInCall | functionCall | '(' expression ('AS' var_)? ')' | var_}. - *

La forme {@code (expr AS ?v)} est résolue comme l’expression de regroupement. - * L’alias éventuel est capturé par {@link fr.inria.corese.core.next.query.impl.parser.listener.SolutionModifierFeature} - * puis porté par {@link GroupByAst}. + *

La forme {@code (expr AS ?v)} est résolue comme l’expression de regroupement (le nom optionnel rejoint la + * spec en étant porté par la projection SELECT ; l’AST minimal ne le duplique pas ici). */ public TermAst termFromGroupCondition(SparqlParser.GroupConditionContext ctx) { if (ctx.expression() != null) { @@ -1278,12 +1302,225 @@ 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; + } + + /** + * Creates a fresh internal blank node label for expanded BGP triples. + */ + public TermAst newAnonymousBlankNode() { + String label; + do { + label = GENERATED_BLANK_NODE_PREFIX + anonymousBlankNodeCounter++; + } while (!blankNodeLabels.add(label)); + return iri(label); + } + + /** + * 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 new PredicatePathAst(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() + ">"); } /** @@ -1295,17 +1532,11 @@ public TermAst termFromVerbSimple(SparqlParser.VerbSimpleContext ctx) { /** * List of objects inside a property path triple. - * Blank node property lists and collections are not yet fully supported (raw text fallback). */ 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/SparqlQueryAnalyzer.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java index 12f4fcee2..98ccb7fe0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlQueryAnalyzer.java @@ -16,13 +16,17 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.DefaultErrorStrategy; import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import java.io.IOException; import java.io.Reader; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * Performs SPARQL syntax, AST, and semantic analysis and returns the @@ -40,7 +44,7 @@ AnalysisResult analyze(Reader reader, SparqlParserOptions options) throws IOExce } try { - QueryAst ast = buildAst(syntaxAnalysis.parseTree(), options); + QueryAst ast = buildAst(syntaxAnalysis.parseTree(), options, syntaxAnalysis.blankNodeLabels()); QueryValidationResult semanticValidation = semanticValidator.validate(ast); return new AnalysisResult(ast, semanticValidation); } catch (QuerySyntaxException e) { @@ -85,13 +89,15 @@ private SyntaxAnalysis analyzeSyntax(Reader reader, SparqlParserOptions options) } ParseTree tree = parser.query(); + Set blankNodeLabels = blankNodeLabels(tokens); if (errorListener.hasErrors()) { return new SyntaxAnalysis( null, - new QueryValidationResult(mapSyntaxDiagnostics(errorListener.getDiagnostics()))); + new QueryValidationResult(mapSyntaxDiagnostics(errorListener.getDiagnostics())), + blankNodeLabels); } - return new SyntaxAnalysis(tree, new QueryValidationResult(List.of())); + return new SyntaxAnalysis(tree, new QueryValidationResult(List.of()), blankNodeLabels); } catch (RecognitionException e) { return new SyntaxAnalysis(null, new QueryValidationResult(List.of( new QueryDiagnostic( @@ -101,21 +107,32 @@ private SyntaxAnalysis analyzeSyntax(Reader reader, SparqlParserOptions options) -1, -1, null, - e.getClass().getSimpleName())))); + e.getClass().getSimpleName()))), Set.of()); } catch (ParseCancellationException e) { QuerySyntaxException syntaxException = SparqlParser.toQuerySyntaxException(e, errorListener); - return new SyntaxAnalysis(null, new QueryValidationResult(List.of(toSyntaxDiagnostic(syntaxException)))); + return new SyntaxAnalysis( + null, + new QueryValidationResult(List.of(toSyntaxDiagnostic(syntaxException))), + Set.of()); } catch (QuerySyntaxException e) { - return new SyntaxAnalysis(null, new QueryValidationResult(List.of(toSyntaxDiagnostic(e)))); + return new SyntaxAnalysis( + null, + new QueryValidationResult(List.of(toSyntaxDiagnostic(e))), + Set.of()); } } - private QueryAst buildAst(ParseTree tree, SparqlParserOptions options) { - fr.inria.corese.core.next.impl.parser.antlr.SparqlParser.QueryContext root = (fr.inria.corese.core.next.impl.parser.antlr.SparqlParser.QueryContext) tree; + private QueryAst buildAst( + ParseTree tree, + SparqlParserOptions options, + Collection blankNodeLabels) { + fr.inria.corese.core.next.impl.parser.antlr.SparqlParser.QueryContext root = + (fr.inria.corese.core.next.impl.parser.antlr.SparqlParser.QueryContext) tree; ParseTreeWalker walker = new ParseTreeWalker(); if (root.queryUnit() != null) { SparqlQueryAstBuilder queryBuilder = new SparqlQueryAstBuilder(options); + queryBuilder.reserveBlankNodeLabels(blankNodeLabels); SparqlListener queryListener = new SparqlListener(List.of( new AskQueryFeature(queryBuilder), new ConstructQueryFeature(queryBuilder), @@ -139,6 +156,7 @@ private QueryAst buildAst(ParseTree tree, SparqlParserOptions options) { if (root.updateUnit() != null) { SparqlUpdateAstBuilder updateBuilder = new SparqlUpdateAstBuilder(options); + updateBuilder.reserveBlankNodeLabels(blankNodeLabels); SparqlListener updateListener = new SparqlListener(List.of( new ClearRequestFeature(updateBuilder), new CreateRequestFeature(updateBuilder), @@ -155,7 +173,19 @@ private QueryAst buildAst(ParseTree tree, SparqlParserOptions options) { return updateBuilder.getResult(); } - throw new QueryEvaluationException("Unknown parse root, could not determine if this is an update request or a query."); + throw new QueryEvaluationException( + "Unknown parse root, could not determine if this is an update request or a query."); + } + + private Set blankNodeLabels(CommonTokenStream tokens) { + tokens.fill(); + Set labels = new LinkedHashSet<>(); + for (Token token : tokens.getTokens()) { + if (token.getType() == SparqlLexer.BLANK_NODE_LABEL) { + labels.add(token.getText()); + } + } + return labels; } private List mapSyntaxDiagnostics(List diagnostics) { @@ -198,7 +228,10 @@ private QueryDiagnostic toSyntaxDiagnostic(QuerySyntaxException exception) { exception.getClass().getSimpleName()); } - private record SyntaxAnalysis(ParseTree parseTree, QueryValidationResult validationResult) { + private record SyntaxAnalysis( + ParseTree parseTree, + QueryValidationResult validationResult, + Set blankNodeLabels) { } record AnalysisResult(QueryAst ast, QueryValidationResult validationResult) { 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..f2bcf21a9 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,22 +2,35 @@ 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. */ -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"); } } + 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 public void accept(AstVisitor visitor) { visitor.visit(this); - this.subject.accept(visitor); - this.predicate.accept(visitor); - this.object.accept(visitor); + subject.accept(visitor); + predicate.accept(visitor); + object.accept(visitor); } } 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..64b1d91ac --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.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; + +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); + left.accept(visitor); + 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..99ed2632e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java @@ -0,0 +1,15 @@ +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); + 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..31ac056c3 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.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; + +import java.util.List; +import java.util.Objects; + +public record NegatedPropertySetPathAst(List excluded) implements PathAst { + public NegatedPropertySetPathAst { + excluded = excluded == null + ? List.of() + : excluded.stream() + .map(path -> Objects.requireNonNull(path, "excluded path is null")) + .toList(); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + excluded.forEach(path -> path.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..18d9fcaf2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java @@ -0,0 +1,15 @@ +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); + 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..890e2bf17 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java @@ -0,0 +1,15 @@ +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); + 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..a4f56cf16 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -0,0 +1,23 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.path; + +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); + } +} 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..f3e9d9e5e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java @@ -0,0 +1,16 @@ +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); + predicate.accept(visitor); + } +} 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..36781ccb8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.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; + +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); + left.accept(visitor); + 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..68566fd43 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java @@ -0,0 +1,15 @@ +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); + pathAst.accept(visitor); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index 8e0ac6508..994c0d058 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -1,6 +1,8 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; import 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 fr.inria.corese.core.next.query.kgram.api.core.ExpType.Type; import fr.inria.corese.core.next.query.kgram.api.core.Filter; import fr.inria.corese.core.next.query.kgram.api.core.Node; @@ -185,6 +187,15 @@ static Node toNode(TermAst term) { + term.getClass().getSimpleName()); } + static TermAst simplePredicate(PathAst path) { + if (path instanceof PredicatePathAst(TermAst predicate)) { + return predicate; + } + throw new UnsupportedOperationException( + "Property path bridge compilation is not supported yet for: " + + path.getClass().getSimpleName()); + } + private static void rejectUnsupportedAskClauses(AskQueryAst askQueryAst) { if (!askQueryAst.valuesClause().mappings().isEmpty()) { throw new UnsupportedOperationException( @@ -461,7 +472,7 @@ private Exp compileConstructTemplate(Query query, ConstructTemplateAst template) Exp bgp = Exp.create(Type.BGP); for (TriplePatternAst triple : template.triplePatternAsts()) { Node subject = constructNode(query, triple.subject()); - Node predicate = constructNode(query, triple.predicate()); + Node predicate = constructNode(query, simplePredicate(triple.predicate())); Node object = constructNode(query, triple.object()); bgp.add(new AstBackedEdge(subject, predicate, object)); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java index 7bb436a7a..d5ba13f19 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java @@ -99,7 +99,7 @@ private Exp compileBgp(BgpAst bgp) { private Edge toEdge(TriplePatternAst triple) { Node subject = CoreseAstQueryBuilder.toNode(triple.subject()); - Node predicate = CoreseAstQueryBuilder.toNode(triple.predicate()); + Node predicate = CoreseAstQueryBuilder.toNode(CoreseAstQueryBuilder.simplePredicate(triple.predicate())); Node object = CoreseAstQueryBuilder.toNode(triple.object()); return new AstBackedEdge(subject, predicate, object); } 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..77b3f4b4a 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,14 @@ 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.TermAst; +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; public class AbstractSparqlParserFeatureTest { @@ -32,4 +39,15 @@ protected SparqlParser newParser(boolean failFast, boolean collectErrors) { .collectErrors(collectErrors) .build()); } + + protected static TermAst simplePredicateTerm(TriplePatternAst triple) { + return TriplePatternAstTestSupport.simplePredicateTerm(triple); + } + + protected static TriplePatternAst firstWhereTriple(QueryAst ast) { + WhereClauseQueryAst query = (WhereClauseQueryAst) ast; + GroupGraphPatternAst whereClause = query.whereClause(); + BgpAst bgp = (BgpAst) 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..45afdbc57 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserSequenceTest.java @@ -0,0 +1,305 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; +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.PathAst; +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 java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class SparqlParserSequenceTest extends AbstractSparqlParserFeatureTest { + + private static final String PREFIX = """ + PREFIX ex: + """; + private static final String USER_BLANK_NODE_LABEL = IOConstants.BLANK_NODE_PREFIX + "b0"; + + @Test + void shouldParseSequencePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/ex:p2 ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + SequencePathAst sequence = assertInstanceOf(SequencePathAst.class, triple.predicate()); + assertPredicate("ex:p1", sequence.left()); + assertPredicate("ex:p2", sequence.right()); + } + + @Test + void shouldParseAlternativePath() { + SparqlParser parser = newParserDefault(); + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1|ex:p2 ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + AlternativePathAst alternative = assertInstanceOf(AlternativePathAst.class, triple.predicate()); + assertPredicate("ex:p1", alternative.left()); + assertPredicate("ex:p2", alternative.right()); + } + + @Test + void shouldGiveSequenceHigherPrecedenceThanAlternativePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/ex:p2|ex:p3 ?o . + } + """); + + AlternativePathAst alternative = assertInstanceOf( + AlternativePathAst.class, + firstWhereTriple(ast).predicate()); + SequencePathAst sequence = assertInstanceOf(SequencePathAst.class, alternative.left()); + assertPredicate("ex:p1", sequence.left()); + assertPredicate("ex:p2", sequence.right()); + assertPredicate("ex:p3", alternative.right()); + } + + @Test + void shouldKeepParenthesizedAlternativeInsideSequencePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/(ex:p2|ex:p3) ?o . + } + """); + + SequencePathAst sequence = assertInstanceOf( + SequencePathAst.class, + firstWhereTriple(ast).predicate()); + assertPredicate("ex:p1", sequence.left()); + AlternativePathAst alternative = assertInstanceOf(AlternativePathAst.class, sequence.right()); + assertPredicate("ex:p2", alternative.left()); + assertPredicate("ex:p3", alternative.right()); + } + + @Test + void shouldParseZeroOrMorePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p* ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + ZeroOrMorePathAst path = assertInstanceOf(ZeroOrMorePathAst.class, triple.predicate()); + assertPredicate("ex:p", path.pathAst()); + } + + @Test + void shouldParseOptionalPath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p? ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + OptionalPathAst path = assertInstanceOf(OptionalPathAst.class, triple.predicate()); + assertPredicate("ex:p", path.pathAst()); + } + + @Test + void shouldParseOneOrMorePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p+ ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + OneOrMorePathAst path = assertInstanceOf(OneOrMorePathAst.class, triple.predicate()); + assertPredicate("ex:p", path.pathAst()); + } + + @Test + void shouldParseInversePath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ^ex:p ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + InversePathAst path = assertInstanceOf(InversePathAst.class, triple.predicate()); + assertPredicate("ex:p", path.pathAst()); + } + + @Test + void shouldApplyInverseToModifiedPathElt() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ^ex:p+ ?o . + } + """); + + InversePathAst inverse = assertInstanceOf( + InversePathAst.class, + firstWhereTriple(ast).predicate()); + OneOrMorePathAst plus = assertInstanceOf(OneOrMorePathAst.class, inverse.pathAst()); + assertPredicate("ex:p", plus.pathAst()); + } + + @Test + void shouldParseNegatedPropertySetPath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s !(ex:p1|ex:p2) ?o . + } + """); + + TriplePatternAst triple = firstWhereTriple(ast); + NegatedPropertySetPathAst negated = assertInstanceOf(NegatedPropertySetPathAst.class, triple.predicate()); + assertEquals(2, negated.excluded().size()); + assertPredicate("ex:p1", negated.excluded().get(0)); + assertPredicate("ex:p2", negated.excluded().get(1)); + } + + @Test + void shouldParseInverseInsideNegatedPropertySetPath() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s !(^ex:p|ex:q) ?o . + } + """); + + NegatedPropertySetPathAst negated = assertInstanceOf( + NegatedPropertySetPathAst.class, + firstWhereTriple(ast).predicate()); + assertEquals(2, negated.excluded().size()); + InversePathAst inverse = assertInstanceOf(InversePathAst.class, negated.excluded().getFirst()); + assertPredicate("ex:p", inverse.pathAst()); + assertPredicate("ex:q", negated.excluded().get(1)); + } + + @Test + void shouldKeepMixedPropertyListPathPredicatesInSourceOrder() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:p1/ex:p2 ?o ; + ?p ?x ; + ex:p3|ex:p4 ?y . + } + """); + + BgpAst bgp = whereBgp(ast); + assertEquals(3, bgp.triples().size()); + assertInstanceOf(SequencePathAst.class, bgp.triples().get(0).predicate()); + assertPredicateVariable("p", bgp.triples().get(1).predicate()); + assertInstanceOf(AlternativePathAst.class, bgp.triples().get(2).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 generatedBlankNodeShouldNotCollideWithUserBlankNodeLabel() { + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(PREFIX + """ + SELECT * WHERE { + ?s ex:link [ ex:p ?o ] . + %s ex:p ?o . + } + """.formatted(USER_BLANK_NODE_LABEL)); + + List triples = ((WhereClauseQueryAst) ast).whereClause().patterns().stream() + .filter(BgpAst.class::isInstance) + .map(BgpAst.class::cast) + .flatMap(bgp -> bgp.triples().stream()) + .toList(); + assertEquals(3, triples.size()); + + IriAst userBlankNode = triples.stream() + .map(TriplePatternAst::subject) + .filter(IriAst.class::isInstance) + .map(IriAst.class::cast) + .filter(subject -> USER_BLANK_NODE_LABEL.equals(subject.raw())) + .findFirst() + .orElseThrow(); + + List generatedBlankNodes = triples.stream() + .flatMap(triple -> Stream.of(triple.subject(), triple.object())) + .filter(IriAst.class::isInstance) + .map(IriAst.class::cast) + .filter(term -> term.raw().startsWith(IOConstants.BLANK_NODE_PREFIX)) + .filter(term -> !USER_BLANK_NODE_LABEL.equals(term.raw())) + .toList(); + + assertEquals(2, generatedBlankNodes.size()); + assertEquals(1, generatedBlankNodes.stream().distinct().count()); + assertNotEquals(userBlankNode, generatedBlankNodes.getFirst()); + } + + private static void assertPredicate(String expectedRaw, PathAst path) { + PredicatePathAst predicatePath = assertInstanceOf(PredicatePathAst.class, path); + IriAst predicate = assertInstanceOf(IriAst.class, predicatePath.predicate()); + assertEquals(expectedRaw, predicate.raw()); + } + + private static void assertPredicateVariable(String expectedName, PathAst path) { + PredicatePathAst predicatePath = assertInstanceOf(PredicatePathAst.class, path); + VarAst predicate = assertInstanceOf(VarAst.class, predicatePath.predicate()); + assertEquals(expectedName, predicate.name()); + } + + private static BgpAst whereBgp(QueryAst ast) { + WhereClauseQueryAst query = (WhereClauseQueryAst) ast; + return (BgpAst) query.whereClause().patterns().getFirst(); + } +} 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..81e8a680b 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 @@ -1,5 +1,8 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.NegatedPropertySetPathAst; +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.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -7,6 +10,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 +214,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 +225,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 +261,39 @@ 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)); + } + } + + // ---------- PathAst ---------- + + @Nested + @DisplayName("PathAst") + class PathAstTest { + + @Test + @DisplayName("negated property set rejects null excluded paths") + void negatedPropertySetRejectsNullExcludedPath() { + List paths = new ArrayList<>(); + paths.add(new PredicatePathAst(new IriAst("ex:p"))); + paths.add(null); + + assertThrows( + NullPointerException.class, + () -> new NegatedPropertySetPathAst(paths)); + } + + @Test + @DisplayName("negated property set defensively copies excluded paths") + void negatedPropertySetDefensivelyCopiesExcludedPaths() { + PathAst excluded = new PredicatePathAst(new IriAst("ex:p")); + List paths = new ArrayList<>(List.of(excluded)); + + NegatedPropertySetPathAst negated = new NegatedPropertySetPathAst(paths); + paths.clear(); + + assertEquals(List.of(excluded), negated.excluded()); } } @@ -268,7 +303,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 @@ -301,7 +336,8 @@ void defensiveCopy() { @DisplayName("returned list is unmodifiable") void unmodifiable() { BgpAst bgp = new BgpAst(List.of(triple)); - assertThrows(UnsupportedOperationException.class, () -> bgp.triples().add(triple)); + List triples = bgp.triples(); + assertThrows(UnsupportedOperationException.class, () -> triples.add(triple)); } @Test @@ -327,7 +363,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") @@ -359,7 +395,8 @@ void defensiveCopy() { @DisplayName("returned list is unmodifiable") void unmodifiable() { GroupGraphPatternAst g = new GroupGraphPatternAst(List.of(bgp)); - assertThrows(UnsupportedOperationException.class, () -> g.patterns().add(bgp)); + List patterns = g.patterns(); + assertThrows(UnsupportedOperationException.class, () -> patterns.add(bgp)); } @Test @@ -382,7 +419,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 +486,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 +496,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); + } +}