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