diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerDispatcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerDispatcher.java index 8849fb3fb..b325eeb14 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerDispatcher.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListenerDispatcher.java @@ -312,4 +312,14 @@ public void enterDrop(SparqlParser.DropContext ctx) { public void exitDrop(SparqlParser.DropContext ctx) { for (var d : delegates) d.exitDrop(ctx); } + + @Override + public void enterAdd(SparqlParser.AddContext ctx) { + for (var d : delegates) d.enterAdd(ctx); + } + + @Override + public void exitAdd(SparqlParser.AddContext ctx) { + for (var d : delegates) d.exitAdd(ctx); + } } 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 a4c845f75..99b6a1fff 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 @@ -162,6 +162,7 @@ private QueryAst buildAst( new CreateUpdateAstListener(updateBuilder), new DropUpdateAstListener(updateBuilder), new LoadUpdateAstListener(updateBuilder), + new AddUpdateAstListener(updateBuilder), new BgpAstListener(updateBuilder), new BindAstListener(updateBuilder), new FilterAstListener(updateBuilder), diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlUpdateAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlUpdateAstBuilder.java index 4a77a2031..f3476dfa2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlUpdateAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlUpdateAstBuilder.java @@ -83,4 +83,11 @@ public CreateRequestAst createToAst(SparqlParser.CreateContext ctx) { public void addRequest(UpdateRequestUnitAst ast) { this.updateRequestAst.add(ast); } + + public AddRequestAst addToAst(SparqlParser.AddContext ctx) { + return new AddRequestAst( + graphRefFromGraphOrDefault(ctx.graphOrDefault(0)), + graphRefFromGraphOrDefault(ctx.graphOrDefault(1)), + ctx.SILENT() != null); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/AddUpdateAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/AddUpdateAstListener.java new file mode 100644 index 000000000..1fbe132e9 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/AddUpdateAstListener.java @@ -0,0 +1,20 @@ +package fr.inria.corese.core.next.query.impl.parser.listener; + +import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; +import fr.inria.corese.core.next.query.impl.parser.SparqlUpdateAstBuilder; +import fr.inria.corese.core.next.query.impl.sparql.ast.AddRequestAst; + +/** + * AST feature listener for ADD SPARQL update query. + */ +public class AddUpdateAstListener extends AbstractSparqlUpdateAstListener { + public AddUpdateAstListener(SparqlUpdateAstBuilder builder) { + super(builder); + } + + @Override + public void enterAdd(SparqlParser.AddContext ctx) { + AddRequestAst ast = this.updateBuilder().addToAst(ctx); + this.updateBuilder().addRequest(ast); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java new file mode 100644 index 000000000..8e794130e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java @@ -0,0 +1,27 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + +/** + * Represents the ADD operation as defined in the SPARQL 1.1 recommendation. + * @param source source graph ({@code GRAPH } or {@code DEFAULT}) + * @param destination destination graph ({@code GRAPH } or {@code DEFAULT}) + * @param silent Determine if the resolution of the query must be resolved silently or not. + */ +public record AddRequestAst(GraphRefAst source, GraphRefAst destination, boolean silent) implements UpdateRequestUnitAst { + /** + * Construct an ADD query with the silent flag set to false. + * @param source source graph + * @param destination destination graph + */ + public AddRequestAst(GraphRefAst source, GraphRefAst destination) { + this(source, destination, false); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + this.source.accept(visitor); + this.destination.accept(visitor); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java index 52dc6ca05..770afd6a7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java @@ -5,5 +5,5 @@ /** * Root interface for all operations related to the SPARQL Update operations listed in SPARQL 1.1 Update. */ -public sealed interface UpdateRequestUnitAst extends VisitableAst permits LoadRequestAst, ClearRequestAst, CreateRequestAst, DropRequestAst { +public sealed interface UpdateRequestUnitAst extends VisitableAst permits LoadRequestAst, ClearRequestAst, CreateRequestAst, DropRequestAst, AddRequestAst { } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/AddUpdateAstBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AddUpdateAstBuilderTest.java new file mode 100644 index 000000000..b962c8ebd --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/AddUpdateAstBuilderTest.java @@ -0,0 +1,126 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import fr.inria.corese.core.next.query.impl.sparql.ast.AddRequestAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestUnitAst; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AddUpdateAstBuilderTest extends AbstractSparqlParserFeatureTest { + + private UpdateRequestUnitAst parseUnit(String query) { + QueryAst ast = newParserDefault().parse(query); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size(), "one update operation"); + return update.operations().getFirst(); + } + + @Test + @DisplayName("ADD GRAPH TO GRAPH : source and destination named graphs, not silent") + void addGraphToGraph() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD GRAPH TO GRAPH ")); + assertFalse(add.silent()); + assertEquals("", add.source().graph().raw()); + assertEquals("", add.destination().graph().raw()); + } + + @Test + @DisplayName("ADD accepts the SPARQL shorthand graph IRI form without GRAPH") + void addAcceptsGraphIriShorthand() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD TO ")); + assertEquals("", add.source().graph().raw()); + assertEquals("", add.destination().graph().raw()); + } + + @Test + @DisplayName("ADD SILENT DEFAULT TO GRAPH : silent, source is DEFAULT") + void addSilentDefaultToGraph() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD SILENT DEFAULT TO GRAPH ")); + assertTrue(add.silent()); + assertTrue(add.source().defaultGraph()); + assertEquals("", add.destination().graph().raw()); + } + + @Test + @DisplayName("ADD GRAPH TO DEFAULT: destination is DEFAULT") + void addGraphToDefault() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD GRAPH TO DEFAULT")); + assertFalse(add.silent()); + assertEquals("", add.source().graph().raw()); + assertTrue(add.destination().defaultGraph()); + } + + @Test + @DisplayName("ADD DEFAULT TO DEFAULT: both source and destination are DEFAULT") + void addDefaultToDefault() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD DEFAULT TO DEFAULT")); + assertFalse(add.silent()); + assertTrue(add.source().defaultGraph()); + assertTrue(add.destination().defaultGraph()); + } + + @Test + @DisplayName("ADD SILENT DEFAULT TO DEFAULT: silent with both DEFAULT") + void addSilentDefaultToDefault() { + AddRequestAst add = assertInstanceOf(AddRequestAst.class, + parseUnit("ADD SILENT DEFAULT TO DEFAULT")); + assertTrue(add.silent()); + assertTrue(add.source().defaultGraph()); + assertTrue(add.destination().defaultGraph()); + } + + @Test + @DisplayName("chained ADD operations separated by ';' produce two update operations") + void chainedAddProducesTwoOperations() { + QueryAst ast = newParserDefault().parse( + "ADD GRAPH TO GRAPH ; " + + "ADD SILENT DEFAULT TO GRAPH "); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(2, update.operations().size()); + + AddRequestAst first = assertInstanceOf(AddRequestAst.class, update.operations().get(0)); + assertEquals("", first.source().graph().raw()); + assertEquals("", first.destination().graph().raw()); + assertFalse(first.silent()); + + AddRequestAst second = assertInstanceOf(AddRequestAst.class, update.operations().get(1)); + assertTrue(second.source().defaultGraph()); + assertEquals("", second.destination().graph().raw()); + assertTrue(second.silent()); + } + + @Test + @DisplayName("PREFIX declaration is preserved in the prologue of an ADD query") + void addMustKeepItsPrologue() { + QueryAst ast = newParserDefault().parse( + "PREFIX ex: ADD GRAPH ex:s TO GRAPH ex:d"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size()); + assertInstanceOf(AddRequestAst.class, update.operations().getFirst()); + assertNotNull(update.prologue()); + assertEquals(1, update.prologue().prefixDeclarations().size()); + } + + @Test + @DisplayName("ADD without TO keyword should throw a syntax exception") + void addMissingToShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("ADD GRAPH ")); + } + + @Test + @DisplayName("ADD without source should throw a syntax exception") + void addMissingSourceShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("ADD TO GRAPH ")); + } +}