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 b325eeb14..001247639 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 @@ -322,4 +322,14 @@ public void enterAdd(SparqlParser.AddContext ctx) { public void exitAdd(SparqlParser.AddContext ctx) { for (var d : delegates) d.exitAdd(ctx); } + + @Override + public void enterCopy(SparqlParser.CopyContext ctx) { + for (var d : delegates) d.enterCopy(ctx); + } + + @Override + public void exitCopy(SparqlParser.CopyContext ctx) { + for (var d : delegates) d.exitCopy(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 99b6a1fff..5b66858ea 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 @@ -163,6 +163,7 @@ private QueryAst buildAst( new DropUpdateAstListener(updateBuilder), new LoadUpdateAstListener(updateBuilder), new AddUpdateAstListener(updateBuilder), + new CopyUpdateAstListener(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 f3476dfa2..27c881c37 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 @@ -90,4 +90,11 @@ public AddRequestAst addToAst(SparqlParser.AddContext ctx) { graphRefFromGraphOrDefault(ctx.graphOrDefault(1)), ctx.SILENT() != null); } + + public CopyRequestAst copyToAst(SparqlParser.CopyContext ctx) { + return new CopyRequestAst( + 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/CopyUpdateAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/CopyUpdateAstListener.java new file mode 100644 index 000000000..96febff99 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/CopyUpdateAstListener.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.CopyRequestAst; + +/** + * AST feature listener for COPY SPARQL update query. + */ +public class CopyUpdateAstListener extends AbstractSparqlUpdateAstListener { + public CopyUpdateAstListener(SparqlUpdateAstBuilder builder) { + super(builder); + } + + @Override + public void enterCopy(SparqlParser.CopyContext ctx) { + CopyRequestAst ast = this.updateBuilder().copyToAst(ctx); + this.updateBuilder().addRequest(ast); + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java new file mode 100644 index 000000000..f781f601b --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.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 COPY 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 CopyRequestAst(GraphRefAst source, GraphRefAst destination, boolean silent) implements UpdateRequestUnitAst { + /** + * Construct a COPY query with a silent flag to false. + * @param source source graph + * @param destination destination graph + */ + public CopyRequestAst(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); + } +} \ No newline at end of file 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 770afd6a7..afa56b702 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, AddRequestAst { +public sealed interface UpdateRequestUnitAst extends VisitableAst permits LoadRequestAst, ClearRequestAst, CreateRequestAst, DropRequestAst, AddRequestAst, CopyRequestAst { } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserCopyQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserCopyQueryTest.java new file mode 100644 index 000000000..fb6ea9738 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserCopyQueryTest.java @@ -0,0 +1,117 @@ +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.CopyRequestAst; +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 SparqlParserCopyQueryTest 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("COPY GRAPH TO GRAPH : source and destination named graphs, not silent") + void copyGraphToGraph() { + CopyRequestAst copy = assertInstanceOf(CopyRequestAst.class, + parseUnit("COPY GRAPH TO GRAPH ")); + assertFalse(copy.silent()); + assertEquals("", copy.source().graph().raw()); + assertEquals("", copy.destination().graph().raw()); + } + + @Test + @DisplayName("COPY SILENT DEFAULT TO GRAPH : silent, source is DEFAULT") + void copySilentDefaultToGraph() { + CopyRequestAst copy = assertInstanceOf(CopyRequestAst.class, + parseUnit("COPY SILENT DEFAULT TO GRAPH ")); + assertTrue(copy.silent()); + assertTrue(copy.source().defaultGraph()); + assertEquals("", copy.destination().graph().raw()); + } + + @Test + @DisplayName("COPY GRAPH TO DEFAULT: destination is DEFAULT") + void copyGraphToDefault() { + CopyRequestAst copy = assertInstanceOf(CopyRequestAst.class, + parseUnit("COPY GRAPH TO DEFAULT")); + assertFalse(copy.silent()); + assertEquals("", copy.source().graph().raw()); + assertTrue(copy.destination().defaultGraph()); + } + + @Test + @DisplayName("COPY DEFAULT TO DEFAULT: both source and destination are DEFAULT") + void copyDefaultToDefault() { + CopyRequestAst copy = assertInstanceOf(CopyRequestAst.class, + parseUnit("COPY DEFAULT TO DEFAULT")); + assertFalse(copy.silent()); + assertTrue(copy.source().defaultGraph()); + assertTrue(copy.destination().defaultGraph()); + } + + @Test + @DisplayName("COPY SILENT DEFAULT TO DEFAULT: silent with both DEFAULT") + void copySilentDefaultToDefault() { + CopyRequestAst copy = assertInstanceOf(CopyRequestAst.class, + parseUnit("COPY SILENT DEFAULT TO DEFAULT")); + assertTrue(copy.silent()); + assertTrue(copy.source().defaultGraph()); + assertTrue(copy.destination().defaultGraph()); + } + + @Test + @DisplayName("chained COPY operations separated by ';' produce two update operations") + void chainedCopyProducesTwoOperations() { + QueryAst ast = newParserDefault().parse( + "COPY GRAPH TO GRAPH ; " + + "COPY SILENT DEFAULT TO GRAPH "); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(2, update.operations().size()); + + CopyRequestAst first = assertInstanceOf(CopyRequestAst.class, update.operations().get(0)); + assertEquals("", first.source().graph().raw()); + assertEquals("", first.destination().graph().raw()); + assertFalse(first.silent()); + + CopyRequestAst second = assertInstanceOf(CopyRequestAst.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 a COPY query") + void copyMustKeepItsPrologue() { + QueryAst ast = newParserDefault().parse( + "PREFIX ex: COPY GRAPH ex:s TO GRAPH ex:d"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size()); + assertInstanceOf(CopyRequestAst.class, update.operations().getFirst()); + assertNotNull(update.prologue()); + assertEquals(1, update.prologue().prefixDeclarations().size()); + } + + @Test + @DisplayName("COPY without TO keyword should throw a syntax exception") + void copyMissingToShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("COPY GRAPH ")); + } + + @Test + @DisplayName("COPY without source should throw a syntax exception") + void copyMissingSourceShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("COPY TO GRAPH ")); + } +}