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 07d05f88f..994e8f053 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 @@ -342,4 +342,34 @@ public void enterMove(SparqlParser.MoveContext ctx) { public void exitMove(SparqlParser.MoveContext ctx) { for (var d : delegates) d.exitMove(ctx); } + + @Override + public void enterInsertData(SparqlParser.InsertDataContext ctx) { + for (var d : delegates) d.enterInsertData(ctx); + } + + @Override + public void exitInsertData(SparqlParser.InsertDataContext ctx) { + for (var d : delegates) d.exitInsertData(ctx); + } + + @Override + public void enterDeleteData(SparqlParser.DeleteDataContext ctx) { + for (var d : delegates) d.enterDeleteData(ctx); + } + + @Override + public void exitDeleteData(SparqlParser.DeleteDataContext ctx) { + for (var d : delegates) d.exitDeleteData(ctx); + } + + @Override + public void enterDeleteWhere(SparqlParser.DeleteWhereContext ctx) { + for (var d : delegates) d.enterDeleteWhere(ctx); + } + + @Override + public void exitDeleteWhere(SparqlParser.DeleteWhereContext ctx) { + for (var d : delegates) d.exitDeleteWhere(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 878655ec8..45e1e65f2 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 @@ -165,6 +165,9 @@ private QueryAst buildAst( new AddUpdateAstListener(updateBuilder), new CopyUpdateAstListener(updateBuilder), new MoveUpdateAstListener(updateBuilder), + new InsertDataUpdateAstListener(updateBuilder), + new DeleteDataUpdateAstListener(updateBuilder), + new DeleteWhereUpdateAstListener(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 a1ddcbb1f..8f5237133 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 @@ -104,4 +104,57 @@ public MoveRequestAst moveToAst(SparqlParser.MoveContext ctx) { graphRefFromGraphOrDefault(ctx.graphOrDefault(1)), ctx.SILENT() != null); } + + public InsertDataRequestAst insertDataToAst(SparqlParser.InsertDataContext ctx) { + return new InsertDataRequestAst(quadsFromQuads(ctx.quadData().quads())); + } + + public DeleteDataRequestAst deleteDataToAst(SparqlParser.DeleteDataContext ctx) { + return new DeleteDataRequestAst(quadsFromQuads(ctx.quadData().quads())); + } + + public DeleteWhereRequestAst deleteWhereToAst(SparqlParser.DeleteWhereContext ctx) { + return new DeleteWhereRequestAst(quadsFromQuads(ctx.quadPattern().quads())); + } + + private QuadsAst quadsFromQuads(SparqlParser.QuadsContext ctx) { + List defaultTriples = new ArrayList<>(); + List namedGraphBlocks = new ArrayList<>(); + + for (SparqlParser.TriplesTemplateContext tt : ctx.triplesTemplate()) { + defaultTriples.addAll(triplesFromTriplesTemplate(tt)); + } + + for (SparqlParser.QuadsNotTriplesContext qnt : ctx.quadsNotTriples()) { + TermAst graph = termFromVarOrIriRef(qnt.varOrIri()); + List graphTriples = new ArrayList<>(); + if (qnt.triplesTemplate() != null) { + graphTriples.addAll(triplesFromTriplesTemplate(qnt.triplesTemplate())); + } + namedGraphBlocks.add(new NamedGraphQuadsAst(graph, graphTriples)); + } + + return new QuadsAst(defaultTriples, namedGraphBlocks); + } + + private List triplesFromTriplesTemplate(SparqlParser.TriplesTemplateContext ctx) { + List triples = new ArrayList<>(); + SparqlParser.TriplesTemplateContext current = ctx; + while (current != null) { + SparqlParser.TriplesSameSubjectContext tss = current.triplesSameSubject(); + if (tss != null && tss.varOrTerm() != null && tss.propertyListNotEmpty() != null) { + TermAst subject = termFromVarOrTerm(tss.varOrTerm()); + var propertyList = tss.propertyListNotEmpty(); + for (int verbIndex = 0; verbIndex < propertyList.verb().size(); verbIndex++) { + TermAst predicate = termFromVerb(propertyList.verb(verbIndex)); + List objects = termListFromObjectList(propertyList.objectList(verbIndex)); + for (TermAst object : objects) { + triples.add(new TriplePatternAst(subject, predicate, object)); + } + } + } + current = current.triplesTemplate(); + } + return triples; + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpAstListener.java index 0006965c2..500a2ebb8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpAstListener.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/BgpAstListener.java @@ -67,6 +67,9 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) { if (ctx.getParent() instanceof SparqlParser.ConstructTriplesContext) { return; } + if (ctx.getParent() instanceof SparqlParser.TriplesTemplateContext) { + return; + } if (ctx.varOrTerm() == null || ctx.propertyListNotEmpty() == null) { return; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteDataUpdateAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteDataUpdateAstListener.java new file mode 100644 index 000000000..268355bd6 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteDataUpdateAstListener.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.DeleteDataRequestAst; + +/** + * AST feature listener for DELETE DATA SPARQL update query. + */ +public class DeleteDataUpdateAstListener extends AbstractSparqlUpdateAstListener { + public DeleteDataUpdateAstListener(SparqlUpdateAstBuilder builder) { + super(builder); + } + + @Override + public void exitDeleteData(SparqlParser.DeleteDataContext ctx) { + DeleteDataRequestAst ast = this.updateBuilder().deleteDataToAst(ctx); + this.updateBuilder().addRequest(ast); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteWhereUpdateAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteWhereUpdateAstListener.java new file mode 100644 index 000000000..0fc81525f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/DeleteWhereUpdateAstListener.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.DeleteWhereRequestAst; + +/** + * AST feature listener for DELETE WHERE SPARQL update query. + */ +public class DeleteWhereUpdateAstListener extends AbstractSparqlUpdateAstListener { + public DeleteWhereUpdateAstListener(SparqlUpdateAstBuilder builder) { + super(builder); + } + + @Override + public void exitDeleteWhere(SparqlParser.DeleteWhereContext ctx) { + DeleteWhereRequestAst ast = this.updateBuilder().deleteWhereToAst(ctx); + this.updateBuilder().addRequest(ast); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/InsertDataUpdateAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/InsertDataUpdateAstListener.java new file mode 100644 index 000000000..60041a5f5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/InsertDataUpdateAstListener.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.InsertDataRequestAst; + +/** + * AST feature listener for INSERT DATA SPARQL update query. + */ +public class InsertDataUpdateAstListener extends AbstractSparqlUpdateAstListener { + public InsertDataUpdateAstListener(SparqlUpdateAstBuilder builder) { + super(builder); + } + + @Override + public void exitInsertData(SparqlParser.InsertDataContext ctx) { + InsertDataRequestAst ast = this.updateBuilder().insertDataToAst(ctx); + this.updateBuilder().addRequest(ast); + } +} 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 b70036c43..05275f7c4 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 @@ -107,4 +107,14 @@ public void visit(GraphRefAst ast) { public void visit(PathAst ast) { } + + @Override + public void visit(QuadsAst ast) { + + } + + @Override + public void visit(NamedGraphQuadsAst 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 5f4859d46..e42a9ba84 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 @@ -27,4 +27,6 @@ public interface AstVisitor { void visit(ServiceAst ast); void visit(GraphRefAst ast); void visit(PathAst ast); + void visit(QuadsAst ast); + void visit(NamedGraphQuadsAst ast); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java new file mode 100644 index 000000000..457c1edcb --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java @@ -0,0 +1,21 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + +/** + * Represents the {@code DELETE DATA} operation as defined in the + * SPARQL 1.1 recommendation. + * + * @param data the quad data to delete (ground triples only — no variables) + */ +public record DeleteDataRequestAst(QuadsAst data) implements UpdateRequestUnitAst { + public DeleteDataRequestAst { + if (data == null) throw new IllegalArgumentException("data must be non-null"); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + data.accept(visitor); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java new file mode 100644 index 000000000..163682d17 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java @@ -0,0 +1,23 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + +/** + * Represents the {@code DELETE WHERE} operation as defined in the + * SPARQL 1.1 recommendation. + * + *

The quad pattern is used both as the pattern to match and as the template of triples to delete. + * + * @param pattern the quad pattern (may contain variables) + */ +public record DeleteWhereRequestAst(QuadsAst pattern) implements UpdateRequestUnitAst { + public DeleteWhereRequestAst { + if (pattern == null) throw new IllegalArgumentException("pattern must be non-null"); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + pattern.accept(visitor); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java new file mode 100644 index 000000000..2a12beaf8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java @@ -0,0 +1,21 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; + +/** + * Represents the {@code INSERT DATA} operation as defined in the + * SPARQL 1.1 recommendation. + * + * @param data the quad data to insert (ground triples only — no variables) + */ +public record InsertDataRequestAst(QuadsAst data) implements UpdateRequestUnitAst { + public InsertDataRequestAst { + if (data == null) throw new IllegalArgumentException("data must be non-null"); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + data.accept(visitor); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java new file mode 100644 index 000000000..309e92357 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java @@ -0,0 +1,26 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +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 java.util.List; + +/** + * A {@code GRAPH varOrIri { triplesTemplate? }} block inside a {@code quads} body. + * + * @param graph the named graph IRI or variable + * @param triples the triple patterns inside the named graph block + */ +public record NamedGraphQuadsAst(TermAst graph, List triples) implements VisitableAst { + public NamedGraphQuadsAst { + if (graph == null) throw new IllegalArgumentException("graph must be non-null"); + triples = triples != null ? List.copyOf(triples) : List.of(); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + graph.accept(visitor); + triples.forEach(t -> t.accept(visitor)); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java new file mode 100644 index 000000000..73edf72ee --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java @@ -0,0 +1,28 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +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 java.util.List; + +/** + * Represents the body of a {@code quadData} or {@code quadPattern} block as defined in + * SPARQL 1.1 Update. + * + * @param defaultTriples triple patterns targeting the default graph + * @param namedGraphBlocks named-graph triple blocks + */ +public record QuadsAst(List defaultTriples, List namedGraphBlocks) + implements VisitableAst { + public QuadsAst { + defaultTriples = defaultTriples != null ? List.copyOf(defaultTriples) : List.of(); + namedGraphBlocks = namedGraphBlocks != null ? List.copyOf(namedGraphBlocks) : List.of(); + } + + @Override + public void accept(AstVisitor visitor) { + visitor.visit(this); + defaultTriples.forEach(t -> t.accept(visitor)); + namedGraphBlocks.forEach(b -> b.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 9ac5cc642..cad1a6e62 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, CopyRequestAst, MoveRequestAst { +public sealed interface UpdateRequestUnitAst extends VisitableAst permits LoadRequestAst, ClearRequestAst, CreateRequestAst, DropRequestAst, AddRequestAst, CopyRequestAst, MoveRequestAst, InsertDataRequestAst, DeleteDataRequestAst, DeleteWhereRequestAst { } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteDataQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteDataQueryTest.java new file mode 100644 index 000000000..5656666e7 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteDataQueryTest.java @@ -0,0 +1,81 @@ +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.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SparqlParserDeleteDataQueryTest extends AbstractSparqlParserFeatureTest { + + private DeleteDataRequestAst parseUnit(String query) { + QueryAst ast = newParserDefault().parse(query); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size(), "one update operation"); + return assertInstanceOf(DeleteDataRequestAst.class, update.operations().getFirst()); + } + + @Test + @DisplayName("DELETE DATA { s p o }: single triple in default graph") + void deleteDataSingleTriple() { + DeleteDataRequestAst delete = parseUnit( + "DELETE DATA { . }"); + assertEquals(1, delete.data().defaultTriples().size()); + assertTrue(delete.data().namedGraphBlocks().isEmpty()); + TriplePatternAst triple = delete.data().defaultTriples().getFirst(); + assertEquals("", ((IriAst) triple.subject()).raw()); + assertEquals("", ((IriAst) triple.object()).raw()); + } + + @Test + @DisplayName("DELETE DATA { s p o1 , o2 }: multiple objects") + void deleteDataMultipleObjects() { + DeleteDataRequestAst delete = parseUnit( + "DELETE DATA { , . }"); + assertEquals(2, delete.data().defaultTriples().size()); + } + + @Test + @DisplayName("DELETE DATA with PREFIX: prefixed IRI is resolved") + void deleteDataWithPrefix() { + QueryAst ast = newParserDefault().parse( + "PREFIX ex: DELETE DATA { ex:a ex:knows ex:b . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + DeleteDataRequestAst delete = assertInstanceOf(DeleteDataRequestAst.class, update.operations().getFirst()); + assertEquals(1, delete.data().defaultTriples().size()); + assertNotNull(update.prologue()); + assertEquals(1, update.prologue().prefixDeclarations().size()); + } + + @Test + @DisplayName("DELETE DATA { GRAPH { s p o } }: triple in named graph") + void deleteDataNamedGraph() { + DeleteDataRequestAst delete = parseUnit( + "DELETE DATA { GRAPH { . } }"); + assertTrue(delete.data().defaultTriples().isEmpty()); + assertEquals(1, delete.data().namedGraphBlocks().size()); + NamedGraphQuadsAst block = delete.data().namedGraphBlocks().getFirst(); + assertEquals("", ((IriAst) block.graph()).raw()); + assertEquals(1, block.triples().size()); + } + + @Test + @DisplayName("chained DELETE DATA operations separated by ';' produce two update operations") + void chainedDeleteDataProducesTwoOperations() { + QueryAst ast = newParserDefault().parse( + "DELETE DATA { . } ; " + + "DELETE DATA { . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(2, update.operations().size()); + assertInstanceOf(DeleteDataRequestAst.class, update.operations().getFirst()); + assertInstanceOf(DeleteDataRequestAst.class, update.operations().get(1)); + } + + @Test + @DisplayName("DELETE DATA without braces should throw a syntax exception") + void deleteDataMissingBracesShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("DELETE DATA ")); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteWhereQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteWhereQueryTest.java new file mode 100644 index 000000000..29dc1cb0f --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserDeleteWhereQueryTest.java @@ -0,0 +1,91 @@ +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.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SparqlParserDeleteWhereQueryTest extends AbstractSparqlParserFeatureTest { + + private DeleteWhereRequestAst parseUnit(String query) { + QueryAst ast = newParserDefault().parse(query); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size(), "one update operation"); + return assertInstanceOf(DeleteWhereRequestAst.class, update.operations().getFirst()); + } + + @Test + @DisplayName("DELETE WHERE { ?s p o }: pattern with variable subject") + void deleteWhereVariableSubject() { + DeleteWhereRequestAst delete = parseUnit( + "DELETE WHERE { ?s . }"); + assertEquals(1, delete.pattern().defaultTriples().size()); + assertTrue(delete.pattern().namedGraphBlocks().isEmpty()); + TriplePatternAst triple = delete.pattern().defaultTriples().getFirst(); + assertEquals("s", ((VarAst) triple.subject()).name()); + } + + @Test + @DisplayName("DELETE WHERE { ?s ?p ?o }: pattern with all variables") + void deleteWhereAllVariables() { + DeleteWhereRequestAst delete = parseUnit( + "DELETE WHERE { ?s ?p ?o . }"); + assertEquals(1, delete.pattern().defaultTriples().size()); + TriplePatternAst triple = delete.pattern().defaultTriples().getFirst(); + assertEquals("s", ((VarAst) triple.subject()).name()); + assertEquals("o", ((VarAst) triple.object()).name()); + } + + @Test + @DisplayName("DELETE WHERE { s p o }: ground triple pattern") + void deleteWhereGroundTriple() { + DeleteWhereRequestAst delete = parseUnit( + "DELETE WHERE { . }"); + assertEquals(1, delete.pattern().defaultTriples().size()); + assertTrue(delete.pattern().namedGraphBlocks().isEmpty()); + } + + @Test + @DisplayName("DELETE WHERE { GRAPH { ?s ?p ?o } }: pattern in named graph") + void deleteWhereNamedGraph() { + DeleteWhereRequestAst delete = parseUnit( + "DELETE WHERE { GRAPH { ?s ?p ?o . } }"); + assertTrue(delete.pattern().defaultTriples().isEmpty()); + assertEquals(1, delete.pattern().namedGraphBlocks().size()); + NamedGraphQuadsAst block = delete.pattern().namedGraphBlocks().getFirst(); + assertEquals("", ((IriAst) block.graph()).raw()); + assertEquals(1, block.triples().size()); + } + + @Test + @DisplayName("DELETE WHERE with PREFIX: prologue is preserved") + void deleteWhereMustKeepItsPrologue() { + QueryAst ast = newParserDefault().parse( + "PREFIX ex: DELETE WHERE { ?s ex:temp ?o . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertInstanceOf(DeleteWhereRequestAst.class, update.operations().getFirst()); + assertNotNull(update.prologue()); + assertEquals(1, update.prologue().prefixDeclarations().size()); + } + + @Test + @DisplayName("chained DELETE WHERE operations separated by ';' produce two update operations") + void chainedDeleteWhereProducesTwoOperations() { + QueryAst ast = newParserDefault().parse( + "DELETE WHERE { ?s ?o . } ; " + + "DELETE WHERE { ?s ?o . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(2, update.operations().size()); + assertInstanceOf(DeleteWhereRequestAst.class, update.operations().getFirst()); + assertInstanceOf(DeleteWhereRequestAst.class, update.operations().get(1)); + } + + @Test + @DisplayName("DELETE WHERE without braces should throw a syntax exception") + void deleteWhereMissingBracesShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("DELETE WHERE ?s ?p ?o")); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserInsertDataQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserInsertDataQueryTest.java new file mode 100644 index 000000000..071da5fa0 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserInsertDataQueryTest.java @@ -0,0 +1,93 @@ +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.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SparqlParserInsertDataQueryTest extends AbstractSparqlParserFeatureTest { + + private InsertDataRequestAst parseUnit(String query) { + QueryAst ast = newParserDefault().parse(query); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(1, update.operations().size(), "one update operation"); + return assertInstanceOf(InsertDataRequestAst.class, update.operations().getFirst()); + } + + @Test + @DisplayName("INSERT DATA { s p o }: single triple in default graph") + void insertDataSingleTriple() { + InsertDataRequestAst insert = parseUnit( + "INSERT DATA { . }"); + assertEquals(1, insert.data().defaultTriples().size()); + assertTrue(insert.data().namedGraphBlocks().isEmpty()); + TriplePatternAst triple = insert.data().defaultTriples().getFirst(); + assertEquals("", ((IriAst) triple.subject()).raw()); + assertEquals("", ((IriAst) triple.object()).raw()); + } + + @Test + @DisplayName("INSERT DATA { s p o1 ; p o2 }: multiple objects via semicolon") + void insertDataMultipleObjects() { + InsertDataRequestAst insert = parseUnit( + "INSERT DATA { , . }"); + assertEquals(2, insert.data().defaultTriples().size()); + } + + @Test + @DisplayName("INSERT DATA with PREFIX: prefixed IRI is resolved") + void insertDataWithPrefix() { + QueryAst ast = newParserDefault().parse( + "PREFIX ex: INSERT DATA { ex:a ex:knows ex:b . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + InsertDataRequestAst insert = assertInstanceOf(InsertDataRequestAst.class, update.operations().getFirst()); + assertEquals(1, insert.data().defaultTriples().size()); + assertNotNull(update.prologue()); + assertEquals(1, update.prologue().prefixDeclarations().size()); + } + + @Test + @DisplayName("INSERT DATA { GRAPH { s p o } }: triple in named graph") + void insertDataNamedGraph() { + InsertDataRequestAst insert = parseUnit( + "INSERT DATA { GRAPH { . } }"); + assertTrue(insert.data().defaultTriples().isEmpty()); + assertEquals(1, insert.data().namedGraphBlocks().size()); + NamedGraphQuadsAst block = insert.data().namedGraphBlocks().getFirst(); + assertEquals("", ((IriAst) block.graph()).raw()); + assertEquals(1, block.triples().size()); + } + + @Test + @DisplayName("INSERT DATA with default and named graph triples") + void insertDataDefaultAndNamedGraph() { + InsertDataRequestAst insert = parseUnit( + "INSERT DATA { " + + " . " + + "GRAPH { . } " + + "}"); + assertEquals(1, insert.data().defaultTriples().size()); + assertEquals(1, insert.data().namedGraphBlocks().size()); + } + + @Test + @DisplayName("chained INSERT DATA operations separated by ';' produce two update operations") + void chainedInsertDataProducesTwoOperations() { + QueryAst ast = newParserDefault().parse( + "INSERT DATA { . } ; " + + "INSERT DATA { . }"); + UpdateRequestAst update = assertInstanceOf(UpdateRequestAst.class, ast); + assertEquals(2, update.operations().size()); + assertInstanceOf(InsertDataRequestAst.class, update.operations().getFirst()); + assertInstanceOf(InsertDataRequestAst.class, update.operations().get(1)); + } + + @Test + @DisplayName("INSERT DATA without braces should throw a syntax exception") + void insertDataMissingBracesShouldFail() { + assertThrows(QuerySyntaxException.class, + () -> newParserDefault().parse("INSERT DATA ")); + } +}