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 "));
+ }
+}