Delegates evaluation to {@link NextSparqlPipelineExecutor}. Initial bindings,
+ * dataset override, and timeout are propagated into the KGRAM evaluation layer
+ * on each {@link #evaluate()} call.
+ */
+public final class CoreseTupleQuery extends AbstractCoreseOperation implements TupleQuery {
+
+ private final NextSparqlPipelineExecutor executor;
+ private long timeoutMillis = 0;
+
+ public CoreseTupleQuery(String queryString, QueryLanguage language, NextSparqlPipelineExecutor executor) {
+ super(queryString, language);
+ this.executor = executor;
+ }
+
+ @Override
+ public Query setTimeout(long timeoutMillis) {
+ this.timeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ @Override
+ public QueryType getQueryType() {
+ return QueryType.TUPLE;
+ }
+
+ @Override
+ public TupleQueryResult evaluate() throws QueryEvaluationException {
+ return executor.evaluateTuple(getQueryString(), getBindings(), getDataset(), effectiveTimeoutMillis());
+ }
+
+ private long effectiveTimeoutMillis() {
+ long fromQuery = this.timeoutMillis;
+ long fromOperation = (long) getMaxExecutionTime() * 1000L;
+ if (fromQuery > 0 && fromOperation > 0) {
+ return Math.min(fromQuery, fromOperation);
+ }
+ return fromQuery > 0 ? fromQuery : fromOperation;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java
new file mode 100644
index 000000000..c5c4b7cd6
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java
@@ -0,0 +1,137 @@
+package fr.inria.corese.core.next.query.impl.query;
+
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Resource;
+import fr.inria.corese.core.next.data.api.Statement;
+import fr.inria.corese.core.next.data.api.Value;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.api.QueryLanguage;
+import fr.inria.corese.core.next.query.api.Update;
+import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
+import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException;
+import fr.inria.corese.core.next.query.impl.parser.SparqlParser;
+import fr.inria.corese.core.next.query.impl.sparql.ast.DeleteDataRequestAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.InsertDataRequestAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.NamedGraphQuadsAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.QuadsAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestUnitAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst;
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.operations.MutationOperations;
+import fr.inria.corese.core.next.util.StringUtils;
+
+import java.util.Objects;
+
+/**
+ * Prepared SPARQL UPDATE operation.
+ *
+ * Supports {@code INSERT DATA} and {@code DELETE DATA} through the next storage
+ * pipeline. Other update forms (LOAD, CLEAR, DROP, DELETE/INSERT WHERE, etc.) throw
+ * {@link UnsupportedQueryFeatureException}.
+ */
+public final class CoreseUpdate extends AbstractCoreseOperation implements Update {
+
+ private final StorageManager storage;
+ private final SparqlParser parser;
+
+ public CoreseUpdate(String updateString, QueryLanguage language, StorageManager storage, SparqlParser parser) {
+ super(updateString, language);
+ this.storage = Objects.requireNonNull(storage, "storage");
+ this.parser = Objects.requireNonNull(parser, "parser");
+ }
+
+ @Override
+ public void execute() throws QueryEvaluationException {
+ UpdateRequestAst request = (UpdateRequestAst) parser.parse(getQueryString());
+ MutationOperations mutations = storage.getMutationOperations();
+ CoreseValueFactory factory = new CoreseValueFactory();
+
+ for (UpdateRequestUnitAst operation : request.operations()) {
+ switch (operation) {
+ case InsertDataRequestAst insert -> applyQuads(insert.data(), mutations, factory, true);
+ case DeleteDataRequestAst delete -> applyQuads(delete.data(), mutations, factory, false);
+ default -> throw new UnsupportedQueryFeatureException(
+ "SPARQL UPDATE operation not yet supported: "
+ + operation.getClass().getSimpleName());
+ }
+ }
+ }
+
+ @Override
+ public Update setBinding(String name, Value value) {
+ super.setBinding(name, value);
+ return this;
+ }
+
+ // -------------------------------------------------------------------------
+ // Quad mutation helpers
+ // -------------------------------------------------------------------------
+
+ private void applyQuads(QuadsAst quads, MutationOperations mutations,
+ CoreseValueFactory factory, boolean insert) {
+ for (TriplePatternAst triple : quads.defaultTriples()) {
+ Statement stmt = toStatement(triple, null, factory);
+ if (insert) {
+ mutations.insertStatement(stmt);
+ } else {
+ mutations.deleteStatement(stmt);
+ }
+ }
+ for (NamedGraphQuadsAst block : quads.namedGraphBlocks()) {
+ Resource context = (Resource) termToValue(block.graph(), factory);
+ for (TriplePatternAst triple : block.triples()) {
+ Statement stmt = toStatement(triple, context, factory);
+ if (insert) {
+ mutations.insertStatement(stmt);
+ } else {
+ mutations.deleteStatement(stmt);
+ }
+ }
+ }
+ }
+
+ private Statement toStatement(TriplePatternAst triple, Resource context, CoreseValueFactory factory) {
+ Value subject = termToValue(triple.subject(), factory);
+ Value object = termToValue(triple.object(), factory);
+
+ // Resolve predicate — INSERT/DELETE DATA only allows simple predicate IRIs
+ if (!(triple.predicate() instanceof PredicatePathAst pp)) {
+ throw new UnsupportedQueryFeatureException(
+ "Property paths are not allowed in INSERT/DELETE DATA");
+ }
+ Value predicate = termToValue(pp.predicate(), factory);
+
+ if (!(subject instanceof Resource s)) {
+ throw new QueryEvaluationException("UPDATE subject must be a Resource, got: " + subject);
+ }
+ if (!(predicate instanceof IRI p)) {
+ throw new QueryEvaluationException("UPDATE predicate must be an IRI, got: " + predicate);
+ }
+ if (context != null) {
+ return factory.createStatement(s, p, object, context);
+ }
+ return factory.createStatement(s, p, object);
+ }
+
+ private Value termToValue(TermAst term, CoreseValueFactory factory) {
+ return switch (term) {
+ case IriAst iri -> factory.createIRI(StringUtils.trimChevronIRIs(iri.raw()));
+ case LiteralAst lit -> {
+ if (lit.lang() != null && !lit.lang().isBlank()) {
+ yield factory.createLiteral(lit.lexical(), lit.lang());
+ }
+ if (lit.datatype() != null && !lit.datatype().isBlank()) {
+ yield factory.createLiteral(lit.lexical(), factory.createIRI(lit.datatype()));
+ }
+ yield factory.createLiteral(lit.lexical());
+ }
+ default -> throw new UnsupportedQueryFeatureException(
+ "Variables are not allowed in INSERT/DELETE DATA: " + term);
+ };
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java
new file mode 100644
index 000000000..03ecd10d0
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java
@@ -0,0 +1,16 @@
+/**
+ * Prepared-query implementations for the public SPARQL API.
+ *
+ * Each class implements one of the public query contracts:
+ *
+ * - {@link fr.inria.corese.core.next.query.impl.query.CoreseTupleQuery} — SELECT
+ * - {@link fr.inria.corese.core.next.query.impl.query.CoreseBooleanQuery} — ASK
+ * - {@link fr.inria.corese.core.next.query.impl.query.CoreseGraphQuery} — CONSTRUCT / DESCRIBE
+ * - {@link fr.inria.corese.core.next.query.impl.query.CoreseUpdate} — SPARQL UPDATE (INSERT DATA / DELETE DATA)
+ *
+ *
+ * All classes extend {@link fr.inria.corese.core.next.query.impl.query.AbstractCoreseOperation},
+ * which manages initial bindings, dataset override, timeout, and inference flag.
+ * None of these classes expose the parser, AST, bridge, or KGRAM engine.
+ */
+package fr.inria.corese.core.next.query.impl.query;
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java
new file mode 100644
index 000000000..d1e26fe2b
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java
@@ -0,0 +1,105 @@
+package fr.inria.corese.core.next.query.impl.repository;
+
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.api.exception.RepositoryException;
+import fr.inria.corese.core.next.query.api.repository.Repository;
+import fr.inria.corese.core.next.query.api.repository.RepositoryConnection;
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.next.storagemanager.api.lifecycle.LifecycleState;
+import fr.inria.corese.core.next.storagemanager.api.support.config.StorageConfig;
+import fr.inria.corese.core.next.storagemanager.api.support.exception.StorageException;
+
+import java.io.File;
+
+/**
+ * Corese implementation of {@link Repository}.
+ *
+ * Wraps a {@link StorageManager} and exposes the public query API through
+ * {@link RepositoryConnection} instances. Users create connections via
+ * {@link #getConnection()} and interact only with the public query API —
+ * the parser, AST, bridge, and KGRAM remain invisible.
+ *
+ * Usage
+ * {@code
+ * StorageManager storage = MemoryStorageManager.builder().build();
+ * Repository repo = new CoreseRepository(storage);
+ * repo.init();
+ *
+ * try (RepositoryConnection conn = repo.getConnection()) {
+ * TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ * try (TupleQueryResult result = q.evaluate()) {
+ * result.stream().forEach(bs -> System.out.println(bs));
+ * }
+ * }
+ * repo.shutDown();
+ * }
+ */
+public final class CoreseRepository implements Repository {
+
+ private final StorageManager storage;
+ private final ValueFactory valueFactory;
+ private File dataDir;
+
+ public CoreseRepository(StorageManager storage) {
+ this.storage = storage;
+ this.valueFactory = new CoreseValueFactory();
+ }
+
+ @Override
+ public void setDataDir(File dataDir) {
+ if (isInitialized()) {
+ throw new IllegalStateException("Cannot set data directory after the repository has been initialized.");
+ }
+ this.dataDir = dataDir;
+ }
+
+ @Override
+ public File getDataDir() {
+ return dataDir;
+ }
+
+ @Override
+ public void init() throws RepositoryException {
+ if (isInitialized()) {
+ throw new IllegalStateException("Repository is already initialized.");
+ }
+ try {
+ storage.getLifecycle().initialize(StorageConfig.builder().build());
+ } catch (StorageException e) {
+ throw new RepositoryException("Failed to initialize repository: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return storage.getLifecycle().getState() == LifecycleState.RUNNING;
+ }
+
+ @Override
+ public void shutDown() throws RepositoryException {
+ try {
+ storage.getLifecycle().shutdown();
+ } catch (StorageException e) {
+ throw new RepositoryException("Failed to shut down repository: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isWritable() {
+ return isInitialized();
+ }
+
+ @Override
+ public RepositoryConnection getConnection() throws RepositoryException {
+ if (!isInitialized()) {
+ throw new RepositoryException("Repository is not initialized. Call init() first.");
+ }
+ return new CoreseRepositoryConnection(this, storage);
+ }
+
+ @Override
+ public ValueFactory getValueFactory() {
+ return valueFactory;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java
new file mode 100644
index 000000000..71b5a5ea6
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java
@@ -0,0 +1,180 @@
+package fr.inria.corese.core.next.query.impl.repository;
+
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.query.api.BooleanQuery;
+import fr.inria.corese.core.next.query.api.GraphQuery;
+import fr.inria.corese.core.next.query.api.QueryLanguage;
+import fr.inria.corese.core.next.query.api.TupleQuery;
+import fr.inria.corese.core.next.query.api.Update;
+import fr.inria.corese.core.next.query.api.dataset.Dataset;
+import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
+import fr.inria.corese.core.next.query.api.exception.RepositoryException;
+import fr.inria.corese.core.next.query.api.repository.Repository;
+import fr.inria.corese.core.next.query.api.repository.RepositoryConnection;
+import fr.inria.corese.core.next.query.impl.parser.SparqlParser;
+import fr.inria.corese.core.next.query.impl.query.CoreseBooleanQuery;
+import fr.inria.corese.core.next.query.impl.query.CoreseGraphQuery;
+import fr.inria.corese.core.next.query.impl.query.CoreseTupleQuery;
+import fr.inria.corese.core.next.query.impl.query.CoreseUpdate;
+import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.DescribeQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst;
+import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor;
+import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+
+/**
+ * Connection to a Corese repository.
+ *
+ * Validates query syntax at preparation time and hides the internal pipeline
+ * (parser, AST, bridge, KGRAM) behind the public {@link RepositoryConnection} API.
+ * Users only interact with {@link TupleQuery}, {@link BooleanQuery},
+ * {@link GraphQuery}, and {@link Update} interfaces.
+ */
+public final class CoreseRepositoryConnection implements RepositoryConnection {
+
+ private final Repository repository;
+ private final StorageManager storage;
+ private final NextSparqlPipelineExecutor executor;
+ private final SparqlParser parser;
+ private Dataset connectionDataset;
+ private boolean open = true;
+
+ CoreseRepositoryConnection(Repository repository, StorageManager storage) {
+ this.repository = repository;
+ this.storage = storage;
+ this.executor = new NextSparqlPipelineExecutor(storage);
+ this.parser = new SparqlParser();
+ }
+
+ @Override
+ public Repository getRepository() {
+ return repository;
+ }
+
+ @Override
+ public ValueFactory getValueFactory() {
+ return repository.getValueFactory();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public void close() throws RepositoryException {
+ open = false;
+ }
+
+ @Override
+ public TupleQuery prepareTupleQuery(QueryLanguage queryLanguage, String queryString)
+ throws QuerySyntaxException, RepositoryException {
+ checkOpen();
+ QueryAst ast = parse(queryString);
+ if (!(ast instanceof SelectQueryAst)) {
+ throw new QuerySyntaxException(
+ "Expected a SELECT query, got: " + ast.getClass().getSimpleName());
+ }
+ CoreseTupleQuery q = new CoreseTupleQuery(queryString, queryLanguage, executor);
+ applyConnectionDataset(q);
+ return q;
+ }
+
+ @Override
+ public GraphQuery prepareGraphQuery(QueryLanguage queryLanguage, String queryString)
+ throws QuerySyntaxException, RepositoryException {
+ checkOpen();
+ QueryAst ast = parse(queryString);
+ if (!(ast instanceof ConstructQueryAst) && !(ast instanceof DescribeQueryAst)) {
+ throw new QuerySyntaxException(
+ "Expected a CONSTRUCT or DESCRIBE query, got: " + ast.getClass().getSimpleName());
+ }
+ CoreseGraphQuery q = new CoreseGraphQuery(queryString, queryLanguage, executor);
+ applyConnectionDataset(q);
+ return q;
+ }
+
+ @Override
+ public BooleanQuery prepareBooleanQuery(QueryLanguage queryLanguage, String queryString)
+ throws QuerySyntaxException, RepositoryException {
+ checkOpen();
+ QueryAst ast = parse(queryString);
+ if (!(ast instanceof AskQueryAst)) {
+ throw new QuerySyntaxException(
+ "Expected an ASK query, got: " + ast.getClass().getSimpleName());
+ }
+ CoreseBooleanQuery q = new CoreseBooleanQuery(queryString, queryLanguage, executor);
+ applyConnectionDataset(q);
+ return q;
+ }
+
+ @Override
+ public Update prepareUpdate(QueryLanguage queryLanguage, String updateString)
+ throws QuerySyntaxException, RepositoryException {
+ checkOpen();
+ QueryAst ast = parse(updateString);
+ if (!(ast instanceof UpdateRequestAst)) {
+ throw new QuerySyntaxException(
+ "Expected a SPARQL UPDATE request, got: " + ast.getClass().getSimpleName());
+ }
+ CoreseUpdate u = new CoreseUpdate(updateString, queryLanguage, storage, parser);
+ applyConnectionDataset(u);
+ return u;
+ }
+
+ @Override
+ public void setDataset(Dataset dataset) {
+ this.connectionDataset = dataset;
+ }
+
+ @Override
+ public Dataset getDataset() {
+ return connectionDataset;
+ }
+
+ @Override
+ public void begin() throws RepositoryException {
+ checkOpen();
+ throw new RepositoryException("Transactions are not yet supported.");
+ }
+
+ @Override
+ public void commit() throws RepositoryException {
+ checkOpen();
+ throw new RepositoryException("Transactions are not yet supported.");
+ }
+
+ @Override
+ public void rollback() throws RepositoryException {
+ checkOpen();
+ throw new RepositoryException("Transactions are not yet supported.");
+ }
+
+ private void checkOpen() {
+ if (!open) {
+ throw new RepositoryException("This connection is closed.");
+ }
+ }
+
+ /**
+ * Applies the connection-level dataset to an operation as its initial dataset,
+ * only when no query-level dataset has been set yet.
+ * The user can still override it by calling {@link fr.inria.corese.core.next.query.api.Operation#setDataset(Dataset)}
+ * on the returned operation.
+ */
+ private void applyConnectionDataset(fr.inria.corese.core.next.query.api.Operation operation) {
+ if (connectionDataset != null && operation.getDataset() == null) {
+ operation.setDataset(connectionDataset);
+ }
+ }
+
+ /**
+ * Parses the query string, throwing {@link QuerySyntaxException} on any syntax error.
+ */
+ private QueryAst parse(String queryString) throws QuerySyntaxException {
+ return parser.parse(queryString);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java
new file mode 100644
index 000000000..c3beb221b
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java
@@ -0,0 +1,15 @@
+/**
+ * Concrete implementations of the public repository API.
+ *
+ * Entry point for users: create a {@link fr.inria.corese.core.next.query.impl.repository.CoreseRepository}
+ * backed by any {@link fr.inria.corese.core.next.storagemanager.api.StorageManager},
+ * call {@code init()}, then open a
+ * {@link fr.inria.corese.core.next.query.api.repository.RepositoryConnection} via
+ * {@code getConnection()}.
+ *
+ * The parser, AST, bridge, and KGRAM engine are invisible from this package.
+ * {@link fr.inria.corese.core.next.query.impl.repository.CoreseRepositoryConnection}
+ * hides {@link fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor}
+ * and validates query syntax at preparation time before returning typed query objects.
+ */
+package fr.inria.corese.core.next.query.impl.repository;
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/result/CoreseGraphQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/impl/result/CoreseGraphQueryResult.java
index 9d4c2de67..6f82181c4 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/result/CoreseGraphQueryResult.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/result/CoreseGraphQueryResult.java
@@ -6,11 +6,15 @@
import java.util.Iterator;
/**
- * Basic implementation around a Model
+ * Basic implementation backed by a pre-built list of statements.
*/
public class CoreseGraphQueryResult implements GraphQueryResult {
- private Iterator iterator;
+ private final Iterator iterator;
+
+ public CoreseGraphQueryResult(Iterator iterator) {
+ this.iterator = iterator;
+ }
@Override
public boolean hasNext() {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
index 3ae32a2c7..0574490d4 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java
@@ -1,23 +1,54 @@
package fr.inria.corese.core.next.query.impl.sparql.execution;
+import fr.inria.corese.core.next.data.api.BNode;
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Literal;
+import fr.inria.corese.core.next.data.api.Resource;
+import fr.inria.corese.core.next.data.api.Statement;
+import fr.inria.corese.core.next.data.api.Value;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.api.dataset.Dataset;
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
+import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException;
+import fr.inria.corese.core.next.query.api.result.Binding;
+import fr.inria.corese.core.next.query.api.result.BindingSet;
+import fr.inria.corese.core.next.query.api.result.GraphQueryResult;
import fr.inria.corese.core.next.query.api.result.TupleQueryResult;
import fr.inria.corese.core.next.query.impl.parser.SparqlParser;
+import fr.inria.corese.core.next.query.impl.result.CoreseGraphQueryResult;
import fr.inria.corese.core.next.query.impl.result.CoreseTupleQueryResult;
import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.ConstructQueryAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.DescribeQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder;
+import fr.inria.corese.core.next.query.kgram.api.core.Edge;
+import fr.inria.corese.core.next.query.kgram.api.core.Node;
import fr.inria.corese.core.next.query.kgram.core.Eval;
+import fr.inria.corese.core.next.query.kgram.core.Exp;
+import fr.inria.corese.core.next.query.kgram.core.Mapping;
import fr.inria.corese.core.next.query.kgram.core.Mappings;
import fr.inria.corese.core.next.query.kgram.core.Query;
import fr.inria.corese.core.next.query.kgram.core.SparqlException;
import fr.inria.corese.core.next.query.kgram.execution.RdfTermMatcher;
import fr.inria.corese.core.next.query.kgram.execution.SparqlKgramEvaluator;
+import fr.inria.corese.core.next.query.kgram.tool.NodeImpl;
import fr.inria.corese.core.next.query.kgram.tool.StorageManagerProducer;
import fr.inria.corese.core.next.storagemanager.api.StorageManager;
+import fr.inria.corese.core.sparql.api.IDatatype;
+import fr.inria.corese.core.sparql.datatype.DatatypeMap;
+import fr.inria.corese.core.sparql.triple.parser.Constant;
+import fr.inria.corese.core.sparql.triple.parser.Variable;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Internal orchestrator for the Corese-next SPARQL query path.
@@ -34,6 +65,18 @@
*/
public final class NextSparqlPipelineExecutor {
+ /**
+ * Shared scheduler used to enforce query timeouts. A single daemon thread is
+ * sufficient because the scheduled task is lightweight (set a flag and call
+ * {@link Eval#finish()}).
+ */
+ private static final ScheduledExecutorService TIMEOUT_SCHEDULER =
+ Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "sparql-query-timeout");
+ t.setDaemon(true);
+ return t;
+ });
+
private final StorageManager storage;
private final SparqlParser parser;
private final CoreseAstQueryBuilder queryBuilder;
@@ -63,49 +106,360 @@ public NextSparqlPipelineExecutor(StorageManager storage) {
this.queryBuilder = Objects.requireNonNull(queryBuilder, "queryBuilder");
}
+ // -------------------------------------------------------------------------
+ // Public API — called by CoreseTupleQuery / CoreseBooleanQuery
+ // -------------------------------------------------------------------------
+
/**
- * Evaluates a SELECT query through the next pipeline.
+ * Evaluates a SELECT query through the next pipeline with no initial bindings,
+ * no dataset override, and no timeout.
*
* @param sparql SPARQL query string to parse and evaluate
* @return tuple result backed by the KGRAM mappings produced from next storage
- * @throws IllegalArgumentException when the query is not a SELECT query
+ * @throws IllegalArgumentException when the query is not a SELECT query
* @throws QueryEvaluationException when KGRAM evaluation fails
*/
public TupleQueryResult evaluateTuple(String sparql) {
+ return evaluateTuple(sparql, null, null, 0L);
+ }
+
+ /**
+ * Evaluates a SELECT query through the next pipeline.
+ *
+ * @param sparql SPARQL query string to parse and evaluate
+ * @param bindings initial variable bindings to inject, or {@code null}
+ * @param dataset dataset override (FROM / FROM NAMED), or {@code null}
+ * @param timeoutMillis maximum evaluation time in milliseconds; 0 means no limit
+ * @return tuple result backed by the KGRAM mappings produced from next storage
+ * @throws IllegalArgumentException when the query is not a SELECT query
+ * @throws QueryEvaluationException when KGRAM evaluation fails
+ * @throws QueryTimeoutException when the evaluation exceeds {@code timeoutMillis}
+ */
+ public TupleQueryResult evaluateTuple(String sparql, BindingSet bindings, Dataset dataset, long timeoutMillis) {
QueryAst ast = parser.parse(sparql);
if (!(ast instanceof SelectQueryAst select)) {
throw new IllegalArgumentException("Tuple evaluation requires a SELECT query, got: "
+ ast.getClass().getSimpleName());
}
- return new CoreseTupleQueryResult(evaluate(queryBuilder.toNextQuery(select)));
+ return new CoreseTupleQueryResult(
+ evaluate(queryBuilder.toNextQuery(select), bindings, dataset, timeoutMillis));
}
/**
- * Evaluates an ASK query through the next pipeline.
+ * Evaluates an ASK query through the next pipeline with no initial bindings,
+ * no dataset override, and no timeout.
*
* @param sparql SPARQL query string to parse and evaluate
* @return {@code true} when at least one mapping matches the ASK pattern
- * @throws IllegalArgumentException when the query is not an ASK query
+ * @throws IllegalArgumentException when the query is not an ASK query
* @throws QueryEvaluationException when KGRAM evaluation fails
*/
public boolean evaluateBoolean(String sparql) {
+ return evaluateBoolean(sparql, null, null, 0L);
+ }
+
+ /**
+ * Evaluates an ASK query through the next pipeline.
+ *
+ * @param sparql SPARQL query string to parse and evaluate
+ * @param bindings initial variable bindings to inject, or {@code null}
+ * @param dataset dataset override (FROM / FROM NAMED), or {@code null}
+ * @param timeoutMillis maximum evaluation time in milliseconds; 0 means no limit
+ * @return {@code true} when at least one mapping matches the ASK pattern
+ * @throws IllegalArgumentException when the query is not an ASK query
+ * @throws QueryEvaluationException when KGRAM evaluation fails
+ * @throws QueryTimeoutException when the evaluation exceeds {@code timeoutMillis}
+ */
+ public boolean evaluateBoolean(String sparql, BindingSet bindings, Dataset dataset, long timeoutMillis) {
QueryAst ast = parser.parse(sparql);
if (!(ast instanceof AskQueryAst ask)) {
throw new IllegalArgumentException("Boolean evaluation requires an ASK query, got: "
+ ast.getClass().getSimpleName());
}
- return evaluate(queryBuilder.toNextQuery(ask)).size() > 0;
+ return evaluate(queryBuilder.toNextQuery(ask), bindings, dataset, timeoutMillis).size() > 0;
}
- private Mappings evaluate(Query query) {
+ /**
+ * Evaluates a CONSTRUCT or DESCRIBE query through the next pipeline with no initial
+ * bindings, no dataset override, and no timeout.
+ *
+ * @param sparql SPARQL query string to parse and evaluate
+ * @return graph result containing the constructed statements
+ * @throws IllegalArgumentException when the query is not a CONSTRUCT or DESCRIBE query
+ * @throws QueryEvaluationException when KGRAM evaluation fails
+ */
+ public GraphQueryResult evaluateGraph(String sparql) {
+ return evaluateGraph(sparql, null, null, 0L);
+ }
+
+ /**
+ * Evaluates a CONSTRUCT or DESCRIBE query through the next pipeline.
+ *
+ * The WHERE clause is evaluated by KGRAM to produce variable bindings. Each
+ * binding is then applied to the CONSTRUCT template to materialise the output
+ * triples. DESCRIBE queries are lowered to a construct-like shape by
+ * {@link CoreseAstQueryBuilder} before evaluation.
+ *
+ * @param sparql SPARQL query string to parse and evaluate
+ * @param bindings initial variable bindings to inject, or {@code null}
+ * @param dataset dataset override (FROM / FROM NAMED), or {@code null}
+ * @param timeoutMillis maximum evaluation time in milliseconds; 0 means no limit
+ * @return graph result containing the constructed statements
+ * @throws IllegalArgumentException when the query is not a CONSTRUCT or DESCRIBE query
+ * @throws QueryEvaluationException when KGRAM evaluation fails
+ * @throws QueryTimeoutException when the evaluation exceeds {@code timeoutMillis}
+ */
+ public GraphQueryResult evaluateGraph(String sparql, BindingSet bindings, Dataset dataset, long timeoutMillis) {
+ QueryAst ast = parser.parse(sparql);
+ Query kgramQuery;
+ if (ast instanceof ConstructQueryAst construct) {
+ kgramQuery = queryBuilder.toNextQuery(construct);
+ } else if (ast instanceof DescribeQueryAst describe) {
+ kgramQuery = queryBuilder.toNextQuery(describe);
+ } else {
+ throw new IllegalArgumentException(
+ "Graph evaluation requires a CONSTRUCT or DESCRIBE query, got: "
+ + ast.getClass().getSimpleName());
+ }
+ Mappings mappings = evaluate(kgramQuery, bindings, dataset, timeoutMillis);
+ List statements = buildConstructStatements(kgramQuery, mappings);
+ return new CoreseGraphQueryResult(statements.iterator());
+ }
+
+ // -------------------------------------------------------------------------
+ // Internal evaluation pipeline
+ // -------------------------------------------------------------------------
+
+ private Mappings evaluate(Query kgramQuery, BindingSet bindings, Dataset dataset, long timeoutMillis) {
+ Eval eval = Eval.create(
+ new StorageManagerProducer(storage),
+ new SparqlKgramEvaluator(),
+ new RdfTermMatcher());
+
+ if (dataset != null) {
+ applyDataset(kgramQuery, dataset);
+ }
+
+ Mapping initialMapping = buildInitialMapping(bindings);
+
+ if (timeoutMillis > 0) {
+ return evaluateWithTimeout(eval, kgramQuery, initialMapping, timeoutMillis);
+ }
+ return evaluateCore(eval, kgramQuery, initialMapping);
+ }
+
+ private Mappings evaluateCore(Eval eval, Query kgramQuery, Mapping initialMapping) {
try {
- Eval eval = Eval.create(
- new StorageManagerProducer(storage),
- new SparqlKgramEvaluator(),
- new RdfTermMatcher());
- return eval.query(query);
+ return eval.query(null, kgramQuery, initialMapping);
} catch (SparqlException e) {
- throw new QueryEvaluationException("Failed to evaluate query with the next pipeline: " + e.getMessage(), e);
+ throw new QueryEvaluationException(
+ "Failed to evaluate query with the next pipeline: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Runs {@code eval.query()} with a cooperative timeout.
+ *
+ * A daemon-thread scheduler calls {@link Eval#finish()} after the deadline
+ * to signal the KGRAM engine to stop at the next opportunity. If the evaluation
+ * completes naturally before the deadline, the scheduler task is cancelled and
+ * results are returned normally.
+ */
+ private Mappings evaluateWithTimeout(Eval eval, Query kgramQuery, Mapping initialMapping, long timeoutMillis) {
+ AtomicBoolean timedOut = new AtomicBoolean(false);
+ ScheduledFuture> canceller = TIMEOUT_SCHEDULER.schedule(() -> {
+ timedOut.set(true);
+ eval.finish();
+ }, timeoutMillis, TimeUnit.MILLISECONDS);
+
+ try {
+ Mappings result = evaluateCore(eval, kgramQuery, initialMapping);
+ if (timedOut.get()) {
+ throw new QueryTimeoutException(timeoutMillis);
+ }
+ return result;
+ } finally {
+ canceller.cancel(false);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // CONSTRUCT template instantiation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Applies the CONSTRUCT template of a KGRAM query to every result mapping and
+ * returns the materialised statements.
+ *
+ * For each mapping produced by the WHERE clause, each template edge is
+ * instantiated by substituting variable nodes with their bound target nodes.
+ * Triples where any component is unbound or cannot be converted to a valid
+ * RDF term are silently skipped, matching standard SPARQL CONSTRUCT semantics.
+ */
+ private List buildConstructStatements(Query kgramQuery, Mappings mappings) {
+ List templateEdges = new ArrayList<>();
+ Exp constructTemplate = kgramQuery.getConstruct();
+ if (constructTemplate == null) {
+ return List.of();
+ }
+ constructTemplate.getEdgeList(templateEdges);
+
+ CoreseValueFactory factory = new CoreseValueFactory();
+ List statements = new ArrayList<>();
+
+ for (Mapping mapping : mappings) {
+ for (Edge templateEdge : templateEdges) {
+ Node subjectNode = resolveTemplateNode(templateEdge.getNode(0), mapping);
+ Node predicateNode = resolveTemplateNode(templateEdge.getProperty(), mapping);
+ Node objectNode = resolveTemplateNode(templateEdge.getNode(1), mapping);
+
+ if (subjectNode == null || predicateNode == null || objectNode == null) {
+ continue;
+ }
+
+ Value subject = kgramNodeToApiValue(subjectNode, factory);
+ Value predicate = kgramNodeToApiValue(predicateNode, factory);
+ Value object = kgramNodeToApiValue(objectNode, factory);
+
+ if (subject instanceof Resource s && predicate instanceof IRI p && object != null) {
+ statements.add(factory.createStatement(s, p, object));
+ }
+ }
+ }
+ return statements;
+ }
+
+ /**
+ * Resolves a CONSTRUCT template node: if the node is a variable, look it up in
+ * the current mapping; if it is already a constant, return it directly.
+ *
+ * @return the bound or constant node, or {@code null} when a variable is unbound
+ */
+ private Node resolveTemplateNode(Node templateNode, Mapping mapping) {
+ if (templateNode == null) {
+ return null;
+ }
+ if (templateNode.isVariable()) {
+ return mapping.getNode(templateNode);
+ }
+ return templateNode;
+ }
+
+ /**
+ * Converts a KGRAM constant {@link Node} to the corresponding API {@link Value}.
+ *
+ * @return the API value, or {@code null} when the datatype kind is not supported
+ */
+ private Value kgramNodeToApiValue(Node node, CoreseValueFactory factory) {
+ IDatatype dt = node.getDatatypeValue();
+ if (dt.isURI()) {
+ return factory.createIRI(dt.getLabel());
+ }
+ if (dt.isBlank()) {
+ return factory.createBNode(dt.getLabel());
+ }
+ if (dt.isLiteral()) {
+ String lang = dt.getLang();
+ if (lang != null && !lang.isEmpty()) {
+ return factory.createLiteral(dt.getLabel(), lang);
+ }
+ String datatypeUri = dt.getDatatypeURI();
+ if (datatypeUri != null) {
+ return factory.createLiteral(dt.getLabel(), factory.createIRI(datatypeUri));
+ }
+ return factory.createLiteral(dt.getLabel());
+ }
+ return null;
+ }
+
+ // -------------------------------------------------------------------------
+ // Dataset wiring
+ // -------------------------------------------------------------------------
+
+ /**
+ * Overrides the FROM / FROM NAMED clauses of a KGRAM query with the API dataset.
+ *
+ * A non-empty {@link Dataset#getDefaultGraphs()} replaces the KGRAM {@code from}
+ * list; a non-empty {@link Dataset#getNamedGraphs()} replaces the {@code named} list.
+ * Empty sets leave the corresponding KGRAM list untouched so that inline FROM clauses
+ * in the query string remain effective when the dataset is only partial.
+ */
+ private void applyDataset(Query kgramQuery, Dataset dataset) {
+ List defaultGraphs = new ArrayList<>(dataset.getDefaultGraphs());
+ List namedGraphs = new ArrayList<>(dataset.getNamedGraphs());
+
+ if (!defaultGraphs.isEmpty()) {
+ kgramQuery.setFrom(urisToKgramNodes(defaultGraphs));
+ }
+ if (!namedGraphs.isEmpty()) {
+ kgramQuery.setNamed(urisToKgramNodes(namedGraphs));
+ }
+ }
+
+ private List urisToKgramNodes(List uris) {
+ List nodes = new ArrayList<>(uris.size());
+ for (String uri : uris) {
+ nodes.add(new NodeImpl(Constant.create(DatatypeMap.newResource(uri))));
+ }
+ return nodes;
+ }
+
+ // -------------------------------------------------------------------------
+ // Initial bindings wiring
+ // -------------------------------------------------------------------------
+
+ /**
+ * Converts an API {@link BindingSet} into a KGRAM {@link Mapping} that can be
+ * passed to {@link Eval#query(Node, Query, Mapping)} as initial variable bindings.
+ *
+ * Each binding entry becomes a (variable-node, value-node) pair. The variable
+ * node carries the variable name as its label so that
+ * {@link Query#getExtNode(String)} can look it up by name during evaluation.
+ * Bindings whose value cannot be converted to a KGRAM node are silently skipped.
+ *
+ * @return a {@link Mapping} with the converted bindings, or {@code null} when the
+ * binding set is empty or all values failed to convert
+ */
+ private Mapping buildInitialMapping(BindingSet bindings) {
+ if (bindings == null || bindings.getBindingNames().isEmpty()) {
+ return null;
+ }
+ List queryNodes = new ArrayList<>();
+ List targetNodes = new ArrayList<>();
+ for (Binding b : bindings) {
+ Node targetNode = valueToKgramNode(b.value());
+ if (targetNode != null) {
+ queryNodes.add(new NodeImpl(new Variable(b.name())));
+ targetNodes.add(targetNode);
+ }
+ }
+ return queryNodes.isEmpty() ? null : Mapping.create(queryNodes, targetNodes);
+ }
+
+ /**
+ * Converts an API {@link Value} into a KGRAM constant {@link Node}.
+ *
+ * @return a constant node, or {@code null} when the value type is not supported
+ */
+ private Node valueToKgramNode(Value value) {
+ IDatatype dt;
+ if (value instanceof IRI iri) {
+ dt = DatatypeMap.newResource(iri.stringValue());
+ } else if (value instanceof BNode bNode) {
+ dt = DatatypeMap.createBlank(bNode.getID());
+ } else if (value instanceof Literal literal) {
+ String lang = literal.getLanguage().orElse(null);
+ if (lang != null && !lang.isEmpty()) {
+ dt = DatatypeMap.createLiteral(literal.getLabel(), null, lang);
+ } else {
+ String datatypeUri = literal.getDatatype() != null
+ ? literal.getDatatype().stringValue()
+ : null;
+ dt = DatatypeMap.createLiteral(literal.getLabel(), datatypeUri, null);
+ }
+ } else {
+ return null;
}
+ return new NodeImpl(Constant.create(dt));
}
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Exp.java b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Exp.java
index 57b3c064f..b05bf1130 100644
--- a/src/main/java/fr/inria/corese/core/next/query/kgram/core/Exp.java
+++ b/src/main/java/fr/inria/corese/core/next/query/kgram/core/Exp.java
@@ -465,7 +465,7 @@ public List getExpList() {
return args;
}
- void getEdgeList(List list) {
+ public void getEdgeList(List list) {
for (Exp exp : getExpList()) {
if (exp.isEdge()) {
list.add(exp.getEdge());
diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnectionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnectionTest.java
new file mode 100644
index 000000000..2923da8a1
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnectionTest.java
@@ -0,0 +1,413 @@
+package fr.inria.corese.core.next.query.impl.repository;
+
+import fr.inria.corese.core.next.data.api.IRI;
+import fr.inria.corese.core.next.data.api.Resource;
+import fr.inria.corese.core.next.data.api.Value;
+import fr.inria.corese.core.next.data.api.ValueFactory;
+import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.api.BooleanQuery;
+import fr.inria.corese.core.next.query.api.GraphQuery;
+import fr.inria.corese.core.next.query.api.Query;
+import fr.inria.corese.core.next.query.api.QueryLanguage;
+import fr.inria.corese.core.next.query.api.TupleQuery;
+import fr.inria.corese.core.next.query.api.Update;
+import fr.inria.corese.core.next.query.api.dataset.Dataset;
+import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
+import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
+import fr.inria.corese.core.next.query.api.exception.RepositoryException;
+import fr.inria.corese.core.next.query.api.result.GraphQueryResult;
+import fr.inria.corese.core.next.data.api.Statement;
+import fr.inria.corese.core.next.query.api.repository.RepositoryConnection;
+import fr.inria.corese.core.next.query.api.result.BindingSet;
+import fr.inria.corese.core.next.query.api.result.TupleQueryResult;
+import fr.inria.corese.core.next.query.impl.dataset.CoreseDataset;
+import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for the public SPARQL API exposed by {@link CoreseRepositoryConnection}.
+ *
+ * Uses a real {@link MemoryStorageManager} to exercise the full pipeline
+ * (parser → AST → bridge → KGRAM) through the API without touching any
+ * internal classes directly.
+ */
+class CoreseRepositoryConnectionTest {
+
+ private static final String ALICE = "http://example.org/alice";
+ private static final String BOB = "http://example.org/bob";
+ private static final String KNOWS = "http://example.org/knows";
+
+ private ValueFactory vf;
+ private MemoryStorageManager storage;
+ private CoreseRepository repository;
+
+ @BeforeEach
+ void setUp() throws RepositoryException {
+ vf = new CoreseValueFactory();
+ storage = MemoryStorageManager.builder().build();
+ repository = new CoreseRepository(storage);
+ repository.init();
+
+ // Insert one triple: alice knows bob
+ storage.getMutationOperations().insertStatement(
+ vf.createStatement(iri(ALICE), iri(KNOWS), iri(BOB)));
+ }
+
+ // -------------------------------------------------------------------------
+ // TupleQuery (SELECT)
+ // -------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("prepareTupleQuery")
+ class PrepareTupleQuery {
+
+ @Test
+ @DisplayName("Returns TupleQuery for a SELECT query")
+ void returnsTupleQueryForSelect() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ assertNotNull(q);
+ assertEquals(Query.QueryType.TUPLE, q.getQueryType());
+ }
+ }
+
+ @Test
+ @DisplayName("evaluate() returns the matching triples")
+ void evaluateReturnsMatchingTriples() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ TupleQueryResult result = q.evaluate();
+
+ assertEquals(List.of("s", "p", "o"), result.getBindingNames());
+ assertTrue(result.hasNext());
+ BindingSet bs = result.next();
+ assertEquals(ALICE, bs.getValue("s").stringValue());
+ assertEquals(KNOWS, bs.getValue("p").stringValue());
+ assertEquals(BOB, bs.getValue("o").stringValue());
+ assertFalse(result.hasNext());
+ }
+ }
+
+ @Test
+ @DisplayName("Throws QuerySyntaxException for a non-SELECT query string")
+ void throwsSyntaxExceptionForAsk() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertThrows(QuerySyntaxException.class, () ->
+ conn.prepareTupleQuery(QueryLanguage.SPARQL, "ASK WHERE { ?s ?p ?o }"));
+ }
+ }
+
+ @Test
+ @DisplayName("Throws QuerySyntaxException for invalid SPARQL")
+ void throwsSyntaxExceptionForInvalidSparql() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertThrows(QuerySyntaxException.class, () ->
+ conn.prepareTupleQuery(QueryLanguage.SPARQL, "THIS IS NOT SPARQL"));
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // BooleanQuery (ASK)
+ // -------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("prepareBooleanQuery")
+ class PrepareBooleanQuery {
+
+ @Test
+ @DisplayName("Returns BooleanQuery for an ASK query")
+ void returnsBooleanQueryForAsk() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ BooleanQuery q = conn.prepareBooleanQuery(QueryLanguage.SPARQL, "ASK WHERE { ?s ?p ?o }");
+ assertNotNull(q);
+ assertEquals(Query.QueryType.BOOLEAN, q.getQueryType());
+ }
+ }
+
+ @Test
+ @DisplayName("evaluate() returns true when data matches")
+ void evaluateReturnsTrueWhenDataExists() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ BooleanQuery q = conn.prepareBooleanQuery(QueryLanguage.SPARQL, "ASK WHERE { ?s ?p ?o }");
+ assertTrue(q.evaluate());
+ }
+ }
+
+ @Test
+ @DisplayName("evaluate() returns false when no data matches")
+ void evaluateReturnsFalseWhenNoMatch() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ BooleanQuery q = conn.prepareBooleanQuery(QueryLanguage.SPARQL,
+ "ASK WHERE { ?p ?o }");
+ assertFalse(q.evaluate());
+ }
+ }
+
+ @Test
+ @DisplayName("Throws QuerySyntaxException for a non-ASK query string")
+ void throwsSyntaxExceptionForSelect() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertThrows(QuerySyntaxException.class, () ->
+ conn.prepareBooleanQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }"));
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // GraphQuery (CONSTRUCT / DESCRIBE)
+ // -------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("prepareGraphQuery")
+ class PrepareGraphQuery {
+
+ @Test
+ @DisplayName("Returns GraphQuery for a CONSTRUCT query")
+ void returnsGraphQueryForConstruct() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ GraphQuery q = conn.prepareGraphQuery(QueryLanguage.SPARQL,
+ "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }");
+ assertNotNull(q);
+ assertEquals(Query.QueryType.GRAPH, q.getQueryType());
+ }
+ }
+
+ @Test
+ @DisplayName("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } returns the stored triple")
+ void constructSpoReturnsStoredTriple() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ GraphQuery q = conn.prepareGraphQuery(QueryLanguage.SPARQL,
+ "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }");
+ GraphQueryResult result = q.evaluate();
+
+ assertTrue(result.hasNext(), "Expected at least one constructed statement");
+ Statement stmt = result.next();
+ assertEquals(ALICE, stmt.getSubject().stringValue());
+ assertEquals(KNOWS, stmt.getPredicate().stringValue());
+ assertEquals(BOB, stmt.getObject().stringValue());
+ assertFalse(result.hasNext(), "Expected exactly one statement");
+ }
+ }
+
+ @Test
+ @DisplayName("CONSTRUCT returns empty result when WHERE clause matches nothing")
+ void constructReturnsEmptyWhenNoMatch() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ GraphQuery q = conn.prepareGraphQuery(QueryLanguage.SPARQL,
+ "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?o }");
+ GraphQueryResult result = q.evaluate();
+ assertFalse(result.hasNext(), "Expected no statements when WHERE has no match");
+ }
+ }
+
+ @Test
+ @DisplayName("Throws QuerySyntaxException for a non-CONSTRUCT/DESCRIBE query string")
+ void throwsSyntaxExceptionForSelect() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertThrows(QuerySyntaxException.class, () ->
+ conn.prepareGraphQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }"));
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Update (SPARQL UPDATE)
+ // -------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("prepareUpdate")
+ class PrepareUpdate {
+
+ @Test
+ @DisplayName("Returns Update for a valid INSERT DATA")
+ void returnsUpdateForInsertData() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ Update u = conn.prepareUpdate(QueryLanguage.SPARQL,
+ "INSERT DATA { }");
+ assertNotNull(u);
+ }
+ }
+
+ @Test
+ @DisplayName("INSERT DATA inserts a triple into the store")
+ void insertDataInsertsTriple() throws Exception {
+ String carol = "http://example.org/carol";
+ try (RepositoryConnection conn = repository.getConnection()) {
+ Update u = conn.prepareUpdate(QueryLanguage.SPARQL,
+ "INSERT DATA { <" + ALICE + "> <" + KNOWS + "> <" + carol + "> }");
+ u.execute();
+ }
+ // verify via SELECT
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL,
+ "SELECT ?o WHERE { <" + ALICE + "> <" + KNOWS + "> ?o }");
+ TupleQueryResult result = q.evaluate();
+ boolean foundCarol = false;
+ while (result.hasNext()) {
+ if (carol.equals(result.next().getValue("o").stringValue())) {
+ foundCarol = true;
+ }
+ }
+ assertTrue(foundCarol, "INSERT DATA should have added alice→knows→carol");
+ }
+ }
+
+ @Test
+ @DisplayName("DELETE DATA removes a triple from the store")
+ void deleteDataRemovesTriple() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ Update u = conn.prepareUpdate(QueryLanguage.SPARQL,
+ "DELETE DATA { <" + ALICE + "> <" + KNOWS + "> <" + BOB + "> }");
+ u.execute();
+ }
+ try (RepositoryConnection conn = repository.getConnection()) {
+ BooleanQuery q = conn.prepareBooleanQuery(QueryLanguage.SPARQL,
+ "ASK WHERE { <" + ALICE + "> <" + KNOWS + "> <" + BOB + "> }");
+ assertFalse(q.evaluate(), "DELETE DATA should have removed alice→knows→bob");
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Operation API (bindings, dataset, timeout, inferredStatements)
+ // -------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("Operation API")
+ class OperationApi {
+
+ @Test
+ @DisplayName("setBinding / getBindings round-trip")
+ void bindingsRoundTrip() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ Value v = vf.createIRI("http://example.org/x");
+
+ q.setBinding("x", v);
+
+ assertTrue(q.getBindings().hasBinding("x"));
+ assertEquals(v, q.getBindings().getValue("x"));
+ }
+ }
+
+ @Test
+ @DisplayName("removeBinding removes the binding")
+ void removeBindingWorks() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ Value v = vf.createIRI("http://example.org/x");
+
+ q.setBinding("x", v);
+ q.removeBinding("x");
+
+ assertFalse(q.getBindings().hasBinding("x"));
+ }
+ }
+
+ @Test
+ @DisplayName("clearBindings removes all bindings")
+ void clearBindingsWorks() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+
+ q.setBinding("x", vf.createIRI("http://example.org/x"));
+ q.setBinding("y", vf.createIRI("http://example.org/y"));
+ q.clearBindings();
+
+ assertFalse(q.getBindings().hasBinding("x"));
+ assertFalse(q.getBindings().hasBinding("y"));
+ }
+ }
+
+ @Test
+ @DisplayName("setDataset / getDataset round-trip")
+ void datasetRoundTrip() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ Dataset ds = new CoreseDataset().addDefaultGraph("http://example.org/g");
+
+ q.setDataset(ds);
+
+ assertSame(ds, q.getDataset());
+ }
+ }
+
+ @Test
+ @DisplayName("setMaxExecutionTime / getMaxExecutionTime round-trip")
+ void timeoutRoundTrip() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+
+ q.setMaxExecutionTime(30);
+
+ assertEquals(30, q.getMaxExecutionTime());
+ }
+ }
+
+ @Test
+ @DisplayName("setIncludeInferred defaults to true")
+ void includeInferredDefaultsToTrue() throws Exception {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }");
+ assertTrue(q.getIncludeInferred());
+ }
+ }
+ }
+
+
+ @Nested
+ @DisplayName("Connection lifecycle")
+ class ConnectionLifecycle {
+
+ @Test
+ @DisplayName("Connection is open after creation")
+ void openAfterCreation() throws RepositoryException {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertTrue(conn.isOpen());
+ }
+ }
+
+ @Test
+ @DisplayName("Connection is closed after close()")
+ void closedAfterClose() throws RepositoryException {
+ RepositoryConnection conn = repository.getConnection();
+ conn.close();
+ assertFalse(conn.isOpen());
+ }
+
+ @Test
+ @DisplayName("prepareTupleQuery on closed connection throws RepositoryException")
+ void prepareOnClosedConnectionThrows() throws RepositoryException {
+ RepositoryConnection conn = repository.getConnection();
+ conn.close();
+ assertThrows(RepositoryException.class, () ->
+ conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * WHERE { ?s ?p ?o }"));
+ }
+
+ @Test
+ @DisplayName("Dataset scoped at connection level")
+ void connectionDatasetRoundTrip() throws RepositoryException {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ Dataset ds = new CoreseDataset().addNamedGraph("http://example.org/g");
+ conn.setDataset(ds);
+ assertSame(ds, conn.getDataset());
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private IRI iri(String iri) {
+ return vf.createIRI(iri);
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryTest.java
new file mode 100644
index 000000000..99d39b87b
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryTest.java
@@ -0,0 +1,70 @@
+package fr.inria.corese.core.next.query.impl.repository;
+
+import fr.inria.corese.core.next.query.api.exception.RepositoryException;
+import fr.inria.corese.core.next.query.api.repository.RepositoryConnection;
+import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CoreseRepositoryTest {
+
+ private MemoryStorageManager storage;
+ private CoreseRepository repository;
+
+ @BeforeEach
+ void setUp() {
+ storage = MemoryStorageManager.builder().build();
+ repository = new CoreseRepository(storage);
+ }
+
+ @Nested
+ @DisplayName("Lifecycle")
+ class Lifecycle {
+
+ @Test
+ @DisplayName("Not initialized before init()")
+ void notInitializedBeforeInit() {
+ assertFalse(repository.isInitialized());
+ }
+
+ @Test
+ @DisplayName("Initialized after init()")
+ void initializedAfterInit() throws RepositoryException {
+ repository.init();
+ assertTrue(repository.isInitialized());
+ }
+
+ @Test
+ @DisplayName("init() twice throws IllegalStateException")
+ void initTwiceThrows() throws RepositoryException {
+ repository.init();
+ assertThrows(IllegalStateException.class, repository::init);
+ }
+
+ @Test
+ @DisplayName("getConnection() before init() throws RepositoryException")
+ void getConnectionBeforeInitThrows() {
+ assertThrows(RepositoryException.class, repository::getConnection);
+ }
+
+ @Test
+ @DisplayName("getConnection() after init() returns open connection")
+ void getConnectionAfterInit() throws RepositoryException {
+ repository.init();
+ try (RepositoryConnection conn = repository.getConnection()) {
+ assertTrue(conn.isOpen());
+ assertSame(repository, conn.getRepository());
+ }
+ }
+
+ @Test
+ @DisplayName("getValueFactory() is not null")
+ void valueFactoryNotNull() {
+ assertNotNull(repository.getValueFactory());
+ }
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
index ad02f12eb..6fc6df0b6 100644
--- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
+++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java
@@ -5,13 +5,20 @@
import fr.inria.corese.core.next.data.api.Value;
import fr.inria.corese.core.next.data.api.ValueFactory;
import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory;
+import fr.inria.corese.core.next.query.api.dataset.Dataset;
+import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException;
+import fr.inria.corese.core.next.query.api.result.Binding;
+import fr.inria.corese.core.next.query.api.result.BindingSet;
import fr.inria.corese.core.next.query.api.result.TupleQueryResult;
+import fr.inria.corese.core.next.query.impl.dataset.CoreseDataset;
import fr.inria.corese.core.next.storagemanager.impl.memory.MemoryStorageManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import java.util.Iterator;
import java.util.List;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -111,11 +118,155 @@ void booleanEvaluationRejectsNonAskQuery() {
() -> executor.evaluateBoolean("SELECT * WHERE { ?s ?p ?o }"));
}
+ // -------------------------------------------------------------------------
+ // Initial bindings
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("SELECT with initial binding filters results to bound variable value")
+ void selectWithInitialBindingFiltersResults() {
+ // bob also knows carol — without binding both alice→bob and bob→carol would match ?s
+ insert(iri(BOB), iri(KNOWS), iri("http://example.org/carol"));
+
+ BindingSet bindings = singleBinding("s", iri(ALICE));
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT ?friend WHERE { ?s <" + KNOWS + "> ?friend }",
+ bindings, null, 0L);
+
+ assertTrue(result.hasNext());
+ assertEquals(BOB, result.next().getValue("friend").stringValue());
+ assertFalse(result.hasNext(), "Only alice's friends should be returned");
+ }
+
+ @Test
+ @DisplayName("ASK with initial binding returns true when bound triple exists")
+ void askWithInitialBindingReturnsTrueWhenMatch() {
+ BindingSet bindings = singleBinding("s", iri(ALICE));
+ assertTrue(executor.evaluateBoolean(
+ "ASK { ?s <" + KNOWS + "> <" + BOB + "> }",
+ bindings, null, 0L));
+ }
+
+ @Test
+ @DisplayName("ASK with initial binding returns false when bound triple absent")
+ void askWithInitialBindingReturnsFalseWhenNoMatch() {
+ BindingSet bindings = singleBinding("s", iri(BOB)); // bob knows nobody
+ assertFalse(executor.evaluateBoolean(
+ "ASK { ?s <" + KNOWS + "> ?o }",
+ bindings, null, 0L));
+ }
+
+ // -------------------------------------------------------------------------
+ // Dataset restriction
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("SELECT with dataset FROM restricts results to the named graph")
+ void selectWithDatasetFromRestrictsToNamedGraph() {
+ String graph1 = "http://example.org/graph1";
+ String graph2 = "http://example.org/graph2";
+ String carol = "http://example.org/carol";
+
+ // alice→knows→bob in graph1, alice→knows→carol in graph2
+ insertInGraph(iri(ALICE), iri(KNOWS), iri(BOB), iri(graph1));
+ insertInGraph(iri(ALICE), iri(KNOWS), iri(carol), iri(graph2));
+
+ Dataset dataset = new CoreseDataset().addDefaultGraph(graph1);
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT ?o WHERE { <" + ALICE + "> <" + KNOWS + "> ?o }",
+ null, dataset, 0L);
+
+ assertTrue(result.hasNext());
+ assertEquals(BOB, result.next().getValue("o").stringValue());
+ assertFalse(result.hasNext(), "Only data from graph1 should be visible");
+ }
+
+ @Test
+ @DisplayName("SELECT with dataset FROM returns nothing when named graph is empty")
+ void selectWithDatasetFromReturnsNothingForEmptyNamedGraph() {
+ String emptyGraph = "http://example.org/empty";
+
+ Dataset dataset = new CoreseDataset().addDefaultGraph(emptyGraph);
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT * WHERE { ?s ?p ?o }",
+ null, dataset, 0L);
+
+ assertFalse(result.hasNext(), "No results expected for an empty named graph");
+ }
+
+ // -------------------------------------------------------------------------
+ // Timeout
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("Query with generous timeout completes normally")
+ void queryWithGenerousTimeoutCompletesNormally() {
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT * WHERE { ?s ?p ?o }",
+ null, null, 10_000L); // 10 seconds — far more than needed
+
+ assertTrue(result.hasNext(), "Result should be non-empty");
+ }
+
+ @Test
+ @DisplayName("Query with zero timeout runs without timeout enforcement")
+ void queryWithZeroTimeoutRunsWithoutEnforcement() {
+ // timeout = 0 means disabled; must complete normally
+ TupleQueryResult result = executor.evaluateTuple(
+ "SELECT * WHERE { ?s ?p ?o }",
+ null, null, 0L);
+
+ assertTrue(result.hasNext(), "Result should be non-empty");
+ }
+
+ @Test
+ @DisplayName("QueryTimeoutException is thrown when evaluation exceeds deadline")
+ void queryTimeoutExceptionThrownWhenDeadlineExceeded() {
+ // Insert many triples to create a cross-product query that takes longer than 1 ms.
+ for (int i = 0; i < 200; i++) {
+ insert(iri("http://example.org/s" + i), iri(KNOWS), iri("http://example.org/o" + i));
+ }
+
+ // Cross-product of 201 triples × 201 triples = 40 401 combinations — expensive enough
+ // to reliably exceed a 1 ms timeout on any machine.
+ assertThrows(QueryTimeoutException.class, () ->
+ executor.evaluateTuple(
+ "SELECT * WHERE { ?s1 ?p1 ?o1 . ?s2 ?p2 ?o2 }",
+ null, null, 1L) // 1 ms
+ .stream()
+ .count());
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
private void insert(Resource subject, IRI predicate, Value object) {
storage.getMutationOperations().insertStatement(valueFactory.createStatement(subject, predicate, object));
}
+ private void insertInGraph(Resource subject, IRI predicate, Value object, Resource context) {
+ storage.getMutationOperations().insertStatement(
+ valueFactory.createStatement(subject, predicate, object, context));
+ }
+
private IRI iri(String iri) {
return valueFactory.createIRI(iri);
}
+
+ /**
+ * Creates a one-entry {@link BindingSet} binding {@code varName} to {@code value}.
+ */
+ private BindingSet singleBinding(String varName, Value value) {
+ Binding b = new Binding() {
+ @Override public String name() { return varName; }
+ @Override public Value value() { return value; }
+ };
+ return new BindingSet() {
+ @Override public Set getBindingNames() { return Set.of(varName); }
+ @Override public boolean hasBinding(String name) { return varName.equals(name); }
+ @Override public Value getValue(String name) { return varName.equals(name) ? value : null; }
+ @Override public Iterator iterator() { return List.of(b).iterator(); }
+ };
+ }
}