diff --git a/src/main/java/fr/inria/corese/core/next/query/Repositories.java b/src/main/java/fr/inria/corese/core/next/query/Repositories.java index f4734e4e1..ffe92128a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/Repositories.java +++ b/src/main/java/fr/inria/corese/core/next/query/Repositories.java @@ -1,5 +1,6 @@ package fr.inria.corese.core.next.query; +import fr.inria.corese.core.next.data.api.io.format.RDFFormat; 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.impl.repository.CoreseRepository; @@ -8,7 +9,11 @@ import fr.inria.corese.core.next.storage.Storages; import fr.inria.corese.core.next.storage.api.plugin.PluginException; +import java.io.InputStream; +import java.nio.file.Path; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; /** * Public entry point for creating ready-to-use Corese repositories. @@ -82,4 +87,79 @@ public static Repository create(StorageManager storage, StorageConfig config) { Objects.requireNonNull(storage, "storage"), Objects.requireNonNull(config, "config")); } + + /** + * Creates an open in-memory repository pre-loaded with RDF statements from the specified file. + * The file format is inferred from the file extension. + * + * @param source RDF source file + * @return a ready-to-use repository containing the loaded data + * @throws RepositoryException if repository creation or RDF loading fails + * @throws NullPointerException if {@code source} is {@code null} + */ + public static Repository createFrom(Path source) { + Objects.requireNonNull(source, "source"); + return createAndLoad(repo -> repo.load(source)); + } + + /** + * Creates an open in-memory repository pre-loaded with RDF statements from the specified input stream. + * The input stream is consumed by the parser but remains caller-owned. + * + * @param source RDF input stream + * @param format explicit RDF serialization format + * @return a ready-to-use repository containing the loaded data + * @throws RepositoryException if repository creation or RDF loading fails + * @throws NullPointerException if {@code source} or {@code format} is {@code null} + */ + public static Repository createFrom(InputStream source, RDFFormat format) { + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(format, "format"); + return createAndLoad(repo -> repo.load(source, format)); + } + + private static Repository createAndLoad(Consumer loader) { + Repository repository = create(); + try { + loader.accept(repository); + return repository; + } catch (RuntimeException | Error e) { + try { + repository.close(); + } catch (Exception closeEx) { + e.addSuppressed(closeEx); + } + throw e; + } + } + + /** + * Executes work against a temporary in-memory repository, ensuring deterministic closure. + * + * @param consumer repository callback + * @throws RepositoryException if repository creation, execution, or closure fails + * @throws NullPointerException if {@code consumer} is {@code null} + */ + public static void withMemory(Consumer consumer) { + Objects.requireNonNull(consumer, "consumer"); + try (Repository repository = create()) { + consumer.accept(repository); + } + } + + /** + * Computes a result against a temporary in-memory repository, ensuring deterministic closure. + * + * @param function computation callback + * @param result type + * @return the computed value + * @throws RepositoryException if repository creation, execution, or closure fails + * @throws NullPointerException if {@code function} is {@code null} + */ + public static T computeWithMemory(Function function) { + Objects.requireNonNull(function, "function"); + try (Repository repository = create()) { + return function.apply(repository); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/api/dataset/Dataset.java b/src/main/java/fr/inria/corese/core/next/query/api/dataset/Dataset.java index 4cedbe3de..652ba69d0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/dataset/Dataset.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/dataset/Dataset.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.data.api.term.IRI; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Objects; @@ -38,6 +39,25 @@ public static Dataset empty() { return EMPTY; } + /** + * Creates an immutable dataset with the specified default graphs and no named graphs. + * + * @param defaultGraphs default graph IRIs + * @return an immutable Dataset instance + * @throws NullPointerException if {@code defaultGraphs} or any element is {@code null} + */ + public static Dataset of(IRI... defaultGraphs) { + Objects.requireNonNull(defaultGraphs, "defaultGraphs"); + if (defaultGraphs.length == 0) { + return empty(); + } + Builder builder = builder(); + for (IRI graph : defaultGraphs) { + builder.defaultGraph(graph); + } + return builder.build(); + } + /** Returns a builder for an immutable dataset. */ public static Builder builder() { return new Builder(); @@ -94,12 +114,30 @@ public Builder defaultGraph(IRI graph) { return this; } + /** Adds multiple default graphs. */ + public Builder defaultGraphs(Collection graphs) { + Objects.requireNonNull(graphs, "graphs"); + for (IRI graph : graphs) { + defaultGraph(graph); + } + return this; + } + /** Adds a named graph. */ public Builder namedGraph(IRI graph) { namedGraphs.add(Objects.requireNonNull(graph, "graph")); return this; } + /** Adds multiple named graphs. */ + public Builder namedGraphs(Collection graphs) { + Objects.requireNonNull(graphs, "graphs"); + for (IRI graph : graphs) { + namedGraph(graph); + } + return this; + } + /** Builds an immutable snapshot. */ public Dataset build() { if (defaultGraphs.isEmpty() && namedGraphs.isEmpty()) { diff --git a/src/main/java/fr/inria/corese/core/next/query/api/dataset/package-info.java b/src/main/java/fr/inria/corese/core/next/query/api/dataset/package-info.java index ea78e9e73..235e92832 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/dataset/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/dataset/package-info.java @@ -1,11 +1,22 @@ /** - * Dataset abstraction for SPARQL query evaluation scope. + * Dataset abstraction and dataset override specifications for SPARQL query evaluation in Corese v5. * - *

{@link fr.inria.corese.core.next.query.api.dataset.Dataset} models the FROM / FROM NAMED - * graph sets that restrict which graphs are visible during query execution. - * It can be applied at the connection level - * ({@link fr.inria.corese.core.next.query.api.repository.RepositoryConnection#setDataset}) - * or overridden per individual query - * ({@link fr.inria.corese.core.next.query.api.Query#setDataset}).

+ *

SPARQL Dataset Semantics

+ *

{@link fr.inria.corese.core.next.query.api.dataset.Dataset} models the {@code FROM} (default graph) + * and {@code FROM NAMED} (named graph) collections that define the active RDF dataset for query evaluation.

+ * + *

Dataset Override vs. Reset

+ *
    + *
  • Override: Providing a non-null {@code Dataset} via + * {@link fr.inria.corese.core.next.query.api.repository.RepositoryConnection#setDataset(Dataset)} or + * {@link fr.inria.corese.core.next.query.api.Query#setDataset(Dataset)} replaces all {@code FROM} and + * {@code FROM NAMED} clauses specified in the SPARQL query text.
  • + *
  • Empty Override ({@link fr.inria.corese.core.next.query.api.dataset.Dataset#empty()}): Explicitly + * restricts the dataset to contain zero graphs, yielding empty results.
  • + *
  • Reset ({@code null}): Passing {@code null} removes any active override, restoring the default + * behavior where the query text's own clauses or all repository graphs are consulted.
  • + *
+ * + *

All {@code Dataset} instances are immutable and inherently thread-safe.

*/ package fr.inria.corese.core.next.query.api.dataset; diff --git a/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java b/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java index 7f2cb8c9d..de3133cb5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/repository/MaterializedResults.java @@ -7,6 +7,7 @@ import fr.inria.corese.core.next.query.api.result.GraphQueryResult; import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -20,19 +21,20 @@ private MaterializedResults() { } static TupleQueryResult tuple(List bindingNames, List rows) { + List orderedNames = List.copyOf(bindingNames); List snapshots = rows.stream() - .map(MaterializedResults::snapshot) + .map(row -> snapshot(orderedNames, row)) .toList(); - return new TupleResult(List.copyOf(bindingNames), snapshots.iterator()); + return new TupleResult(orderedNames, snapshots.iterator()); } static GraphQueryResult graph(List statements) { return new GraphResult(List.copyOf(statements).iterator()); } - private static BindingSet snapshot(BindingSet source) { + private static BindingSet snapshot(List bindingNames, BindingSet source) { Map values = new LinkedHashMap<>(); - for (String name : source.getBindingNames()) { + for (String name : bindingNames) { Value value = source.getValue(name); if (value != null) { values.put(name, value); @@ -44,7 +46,7 @@ private static BindingSet snapshot(BindingSet source) { private static final class TupleResult implements TupleQueryResult { private final List bindingNames; private final Iterator rows; - private boolean closed; + private volatile boolean closed; private TupleResult(List bindingNames, Iterator rows) { this.bindingNames = bindingNames; @@ -82,7 +84,7 @@ private void checkOpen() { private static final class GraphResult implements GraphQueryResult { private final Iterator statements; - private boolean closed; + private volatile boolean closed; private GraphResult(Iterator statements) { this.statements = statements; @@ -115,7 +117,7 @@ private void checkOpen() { private record ImmutableBindingSet(Map values) implements BindingSet { private ImmutableBindingSet { - values = Map.copyOf(values); + values = Collections.unmodifiableMap(new LinkedHashMap<>(values)); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/api/repository/Repository.java b/src/main/java/fr/inria/corese/core/next/query/api/repository/Repository.java index 412e03686..08178172a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/repository/Repository.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/repository/Repository.java @@ -10,6 +10,8 @@ import fr.inria.corese.core.next.query.Repositories; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.io.InputStream; import java.io.Reader; @@ -19,7 +21,10 @@ import java.util.function.Consumer; import java.util.function.Function; import fr.inria.corese.core.next.data.api.io.format.RDFFormat; +import fr.inria.corese.core.next.data.api.term.IRI; import fr.inria.corese.core.next.data.api.term.Resource; +import fr.inria.corese.core.next.data.api.term.Value; +import java.util.Objects; /** * A Corese repository that contains RDF data that can be queried and updated. @@ -99,6 +104,35 @@ default TupleQueryResult select(String sparqlSelect) throws QuerySyntaxException return MaterializedResults.tuple(names, bindings); } + /** + * Evaluates a SPARQL SELECT query directly against this repository in one call, + * mapping each solution {@link BindingSet} using the provided mapper function. + * + *

The repository connection is automatically opened and closed within this call. + * The mapper should extract needed terms eagerly rather than retaining references + * to the row, because the underlying query cursor is closed upon return.

+ * + * @param sparqlSelect the SPARQL SELECT query text + * @param mapper mapping function converting each {@link BindingSet} into a result element + * @param the mapped result type + * @return an unmodifiable list of mapped results + * @throws QuerySyntaxException if the query string is syntactically invalid + * @throws RepositoryException if evaluation fails + * @throws NullPointerException if {@code mapper} is {@code null} + */ + default List select(String sparqlSelect, Function mapper) + throws QuerySyntaxException, RepositoryException { + Objects.requireNonNull(mapper, "mapper"); + List results = new ArrayList<>(); + try (RepositoryConnection conn = getConnection(); + TupleQueryResult raw = conn.prepareTupleQuery(sparqlSelect).evaluate()) { + while (raw.hasNext()) { + results.add(mapper.apply(raw.next())); + } + } + return Collections.unmodifiableList(results); + } + /** * Evaluates a SPARQL ASK query directly against this repository in one call. * @@ -139,6 +173,67 @@ default GraphQueryResult graph(String sparqlGraph) throws QuerySyntaxException, return MaterializedResults.graph(statements); } + /** + * Evaluates a SPARQL CONSTRUCT or DESCRIBE query directly against this repository in one call. + * + *

This is a natural semantic alias for {@link #graph(String)} following SPARQL 1.1 terminology.

+ * + * @param sparqlConstruct the SPARQL CONSTRUCT query text + * @return a materialized {@link GraphQueryResult} containing output statements + * @throws QuerySyntaxException if the query string is syntactically invalid + * @throws RepositoryException if evaluation fails + * @see #graph(String) + */ + default GraphQueryResult construct(String sparqlConstruct) throws QuerySyntaxException, RepositoryException { + return graph(sparqlConstruct); + } + + /** + * Tests whether at least one statement matching the specified pattern exists in this repository. + * A {@code null} term is treated as a wildcard. + * + * @param subject statement subject to match, or {@code null} for wildcard + * @param predicate statement predicate to match, or {@code null} for wildcard + * @param object statement object to match, or {@code null} for wildcard + * @param contexts contexts to match, or empty for every graph + * @return {@code true} if a matching statement exists, {@code false} otherwise + * @throws RepositoryException if the check fails + */ + default boolean hasStatement(Resource subject, IRI predicate, Value object, Resource... contexts) + throws RepositoryException { + try (RepositoryConnection conn = getConnection()) { + return conn.hasStatement(subject, predicate, object, contexts); + } + } + + /** + * Returns the total number of statements in the designated graph contexts. + * An empty {@code contexts} array counts across all graphs (default and named graphs). + * + * @param contexts contexts to count, or none for every graph + * @return number of statements in the selected contexts + * @throws RepositoryException if the count cannot be performed + */ + default long size(Resource... contexts) throws RepositoryException { + try (RepositoryConnection conn = getConnection()) { + return conn.size(contexts); + } + } + + /** + * Adds the specified statements to this repository in an auto-managed connection. + * + * @param statements statements to add + * @throws RepositoryException if the addition fails + * @throws NullPointerException if {@code statements} or any element is {@code null} + */ + default void add(Statement... statements) throws RepositoryException { + Objects.requireNonNull(statements, "statements"); + try (RepositoryConnection conn = getConnection()) { + conn.add(Arrays.asList(statements)); + } + } + /** * Executes a SPARQL UPDATE request directly against this repository in one call. * diff --git a/src/main/java/fr/inria/corese/core/next/query/api/repository/package-info.java b/src/main/java/fr/inria/corese/core/next/query/api/repository/package-info.java index f4303a9a3..390e429cb 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/repository/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/repository/package-info.java @@ -1,5 +1,25 @@ /** - * Repository and connection contracts for executing SPARQL operations. - * Create repositories with {@link fr.inria.corese.core.next.query.Repositories}. + * Repository and connection contracts for executing SPARQL operations in Corese v5. + * + *

Lifecycle and Thread-Safety Contracts

+ *
    + *
  • {@link fr.inria.corese.core.next.query.api.repository.Repository}: Long-lived, thread-safe root container + * owning the underlying storage backend. Closing a repository is idempotent, terminates all active sessions, + * and releases storage resources.
  • + *
  • {@link fr.inria.corese.core.next.query.api.repository.RepositoryConnection}: Lightweight, mutable session + * confining transactions, dataset scopes, and query preparation to a single thread. Connections are + * not safe for concurrent access across multiple threads.
  • + *
+ * + *

Convenience Shortcuts vs. Prepared Operations

+ *

Convenience methods on {@code Repository} (such as {@link fr.inria.corese.core.next.query.api.repository.Repository#select(String)}, + * {@link fr.inria.corese.core.next.query.api.repository.Repository#ask(String)}, + * {@link fr.inria.corese.core.next.query.api.repository.Repository#construct(String)}, + * and {@link fr.inria.corese.core.next.query.api.repository.Repository#size}) + * manage their own connection lifecycle and return fully materialized, detached results safe for use across threads.

+ * + *

For high-throughput, streaming, or parameterized query execution, obtain an explicit + * {@link fr.inria.corese.core.next.query.api.repository.RepositoryConnection} via {@code getConnection()}, + * prepare operations, and consume results progressively within a {@code try-with-resources} block.

*/ package fr.inria.corese.core.next.query.api.repository; diff --git a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java index 8d29faf40..50dddf1c7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java @@ -90,6 +90,11 @@ public BindingSet next() { /** * Returns a sequential {@link Stream} over the query results. * + *

Closing the returned stream triggers {@link #close()} on this result. + * Note that terminal operations (such as {@code forEach}, {@code findFirst}, or {@code limit}) + * do not automatically close the stream. Callers must manage the stream + * with {@code try-with-resources} to guarantee underlying snapshot partition release.

+ * * @return a sequential stream over the binding sets in this result * @throws IllegalStateException if this result has been closed */ diff --git a/src/main/java/fr/inria/corese/core/next/query/api/result/package-info.java b/src/main/java/fr/inria/corese/core/next/query/api/result/package-info.java index 4e3599869..776cb14f6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/result/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/result/package-info.java @@ -1,5 +1,22 @@ /** - * Closeable result types for SPARQL queries and progressive RDF statement - * access, plus immutable query bindings. + * Closeable result types and solution bindings for SPARQL query evaluation in Corese v5. + * + *

Result Types

+ *
    + *
  • {@link fr.inria.corese.core.next.query.api.result.TupleQueryResult}: Solution sequence for SPARQL SELECT queries, + * maintaining deterministic projection variable order via {@code getBindingNames()}.
  • + *
  • {@link fr.inria.corese.core.next.query.api.result.GraphQueryResult} & + * {@link fr.inria.corese.core.next.query.api.result.StatementResult}: Progressive sequences of RDF statements + * produced by SPARQL CONSTRUCT and DESCRIBE queries or repository scans.
  • + *
  • {@link fr.inria.corese.core.next.query.api.result.BindingSet} & + * {@link fr.inria.corese.core.next.query.api.result.Binding}: Immutable mappings of variable names to RDF terms.
  • + *
+ * + *

Stream and Resource Lifecycle

+ *

Query results retain active storage snapshot references until exhausted, closed, or interrupted. + * The {@code stream()} method exposes a standard Java {@link java.util.stream.Stream} registered with an {@code onClose} + * hook that closes the parent result. Because terminal stream operations (such as {@code findFirst()}, {@code count()}, + * or {@code forEach()}) do not trigger stream closure, callers must manage query results and their streams + * using {@code try-with-resources}.

*/ package fr.inria.corese.core.next.query.api.result; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java index d5ce7ecaa..37a60793f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java @@ -28,7 +28,7 @@ public final class CoreseBindingSet implements BindingSet { * @throws NullPointerException if the mapping is null */ public CoreseBindingSet(Mapping mapping) { - this(mapping, Set.copyOf(mapping.getVariableNames())); + this(mapping, Collections.unmodifiableSet(new LinkedHashSet<>(mapping.getVariableNames()))); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseTupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseTupleQueryResult.java index 1e4e670e3..d7d0daa83 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseTupleQueryResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseTupleQueryResult.java @@ -6,7 +6,9 @@ import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -24,7 +26,8 @@ public final class CoreseTupleQueryResult implements TupleQueryResult { public CoreseTupleQueryResult(Mappings mappings) { this.mappings = Objects.requireNonNull(mappings, "mappings"); this.iterator = this.mappings.iterator(); - this.projectedNames = Set.copyOf(this.mappings.getSelect().stream().map(Node::getLabel).toList()); + this.projectedNames = Collections.unmodifiableSet( + new LinkedHashSet<>(this.mappings.getSelect().stream().map(Node::getLabel).toList())); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java index 05469415c..c0956f620 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java @@ -13,6 +13,8 @@ import fr.inria.corese.core.next.storage.api.StorageManager; import fr.inria.corese.core.next.storage.api.exception.StorageException; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; @@ -66,7 +68,7 @@ private static ScheduledThreadPoolExecutor deadlines() { this.timeoutMillis = timeoutMillis; this.started = started; names = query.getSelect().stream().map(Node::getLabel).toList(); - projected = Set.copyOf(names); + projected = Collections.unmodifiableSet(new LinkedHashSet<>(names)); long remaining = TimeUnit.MILLISECONDS.toNanos(timeoutMillis) - (System.nanoTime() - started); deadline = timeoutMillis <= 0 ? null : DEADLINES.schedule(this::expire, Math.max(0, remaining), TimeUnit.NANOSECONDS); diff --git a/src/main/java/fr/inria/corese/core/next/query/package-info.java b/src/main/java/fr/inria/corese/core/next/query/package-info.java index 2e4e259a7..14f0a74d4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/package-info.java @@ -1,8 +1,28 @@ /** - * SPARQL query and update services. + * Unified SPARQL query, update, and repository management facade for Corese v5. * - *

{@link fr.inria.corese.core.next.query.Repositories} is the public creation - * entry point. {@code api} contains the supported contracts; {@code impl} - * contains the Corese implementation and internal execution machinery.

+ *

Entry Points

+ *

{@link fr.inria.corese.core.next.query.Repositories} serves as the root factory for creating and + * executing operations against RDF repositories:

+ *
    + *
  • {@link fr.inria.corese.core.next.query.Repositories#create()}: Instantiates an in-memory repository + * backed by the native Copy-On-Write index engine.
  • + *
  • {@link fr.inria.corese.core.next.query.Repositories#createFrom(java.nio.file.Path)}: Instantiates and + * pre-loads an RDF graph or dataset directly from a file.
  • + *
  • {@link fr.inria.corese.core.next.query.Repositories#withMemory(java.util.function.Consumer)}: + * Executes a scoped action against an auto-closed repository.
  • + *
+ * + *

Architecture & Lifecycle

+ *

The query engine is organized into four complementary layers:

+ *
    + *
  • Facade Layer: {@code Repositories} and convenience shortcuts on {@code Repository}.
  • + *
  • API Contracts ({@code api}): Pure, implementation-independent interfaces for repositories, + * connections, prepared queries, and results.
  • + *
  • Parser & AST ({@code impl.sparql}): High-performance ANTLR4-based SPARQL 1.1 parser + * and abstract syntax tree.
  • + *
  • Execution Pipeline ({@code impl.engine}): Cost-based optimizer (CBO), streaming cursors, + * and join evaluation engine.
  • + *
*/ package fr.inria.corese.core.next.query; diff --git a/src/test/java/fr/inria/corese/core/next/query/RepositoriesTest.java b/src/test/java/fr/inria/corese/core/next/query/RepositoriesTest.java index c26aed6cd..16239bc53 100644 --- a/src/test/java/fr/inria/corese/core/next/query/RepositoriesTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/RepositoriesTest.java @@ -1,5 +1,6 @@ package fr.inria.corese.core.next.query; +import fr.inria.corese.core.next.data.api.io.format.RDFFormat; 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; @@ -9,7 +10,13 @@ import fr.inria.corese.core.next.storage.api.lifecycle.StorageLifecycle; import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -109,4 +116,42 @@ void rejectsNullCreationArguments() { assertThrows(NullPointerException.class, () -> Repositories.create((StorageManager) null)); assertThrows(NullPointerException.class, () -> Repositories.create(mock(StorageManager.class), null)); } + + @Test + void withMemoryAndComputeWithMemoryExecuteAndCloseRepository() { + AtomicBoolean wasExecuted = new AtomicBoolean(false); + AtomicReference capturedRepo = new AtomicReference<>(); + Repositories.withMemory(repo -> { + capturedRepo.set(repo); + assertTrue(repo.isOpen()); + repo.update("INSERT DATA { }"); + assertTrue(repo.ask("ASK { }")); + wasExecuted.set(true); + }); + assertTrue(wasExecuted.get()); + assertNotNull(capturedRepo.get()); + assertFalse(capturedRepo.get().isOpen()); + + AtomicReference capturedComputeRepo = new AtomicReference<>(); + boolean present = Repositories.computeWithMemory(repo -> { + capturedComputeRepo.set(repo); + repo.update("INSERT DATA { 42 }"); + return repo.ask("ASK { 42 }"); + }); + assertTrue(present); + assertNotNull(capturedComputeRepo.get()); + assertFalse(capturedComputeRepo.get().isOpen()); + } + + @Test + void createFromInputStreamLoadsDataImmediately() { + String turtle = " \"Alice\" ."; + ByteArrayInputStream input = new ByteArrayInputStream(turtle.getBytes(StandardCharsets.UTF_8)); + + try (Repository repo = Repositories.createFrom(input, RDFFormat.TURTLE)) { + assertTrue(repo.isOpen()); + assertEquals(1L, repo.size()); + assertTrue(repo.ask("ASK { \"Alice\" }")); + } + } } diff --git a/src/test/java/fr/inria/corese/core/next/query/api/dataset/DatasetTest.java b/src/test/java/fr/inria/corese/core/next/query/api/dataset/DatasetTest.java index 55c08cba0..64682f416 100644 --- a/src/test/java/fr/inria/corese/core/next/query/api/dataset/DatasetTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/api/dataset/DatasetTest.java @@ -5,6 +5,7 @@ import fr.inria.corese.core.next.data.Values; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +45,27 @@ void builderCreatesAnImmutableOrderedSnapshot() { namedGraphs::clear); } + @Test + void ofFactoryAndCollectionBuilderMethodsCreateExpectedDatasets() { + IRI g1 = iri("urn:g1"); + IRI g2 = iri("urn:g2"); + IRI n1 = iri("urn:n1"); + + Dataset datasetOf = Dataset.of(g1, g2); + assertEquals(Set.of(g1, g2), datasetOf.getDefaultGraphs()); + assertTrue(datasetOf.getNamedGraphs().isEmpty()); + + Dataset datasetEmpty = Dataset.of(); + assertSame(Dataset.empty(), datasetEmpty); + + Dataset datasetCollections = Dataset.builder() + .defaultGraphs(List.of(g1, g2)) + .namedGraphs(List.of(n1)) + .build(); + assertEquals(Set.of(g1, g2), datasetCollections.getDefaultGraphs()); + assertEquals(Set.of(n1), datasetCollections.getNamedGraphs()); + } + private IRI iri(String value) { return values.createIRI(value); } 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 index a86d119f1..ff8e894e3 100644 --- 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 @@ -1,5 +1,9 @@ package fr.inria.corese.core.next.query.impl.repository; +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.model.Statement; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Literal; 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.storage.impl.memory.MemoryStorageManager; @@ -9,6 +13,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; class CoreseRepositoryTest { @@ -123,6 +129,60 @@ void constructOneLinerTest() throws RepositoryException { assertEquals("http://ex.org/s", stmt.getSubject().stringValue()); } assertThrows(IllegalStateException.class, result::hasNext); + + var aliasResult = repository.construct("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"); + try (aliasResult) { + assertTrue(aliasResult.hasNext()); + var stmt = aliasResult.next(); + assertEquals("http://ex.org/s", stmt.getSubject().stringValue()); + } + } + + @Test + @DisplayName("select() with mapper executes and transforms solutions cleanly") + void selectWithMapperTest() throws RepositoryException { + repository.update("INSERT DATA { \"Alice\" . \"Bob\" }"); + + List names = repository.select( + "SELECT ?name WHERE { ?s ?name } ORDER BY ?name", + bs -> bs.getValue("name").stringValue()); + + assertEquals(List.of("Alice", "Bob"), names); + } + + @Test + @DisplayName("size(), hasStatement() and add() convenience methods operate on repository") + void repositoryDirectConvenienceMethods() throws RepositoryException { + IRI s = Values.iri("http://ex.org/s1"); + IRI p = Values.iri("http://ex.org/p1"); + Literal o = Values.literal("o1"); + Statement stmt = Values.statement(s, p, o); + + assertEquals(0L, repository.size()); + assertFalse(repository.hasStatement(s, p, o)); + + repository.add(stmt); + assertEquals(1L, repository.size()); + assertTrue(repository.hasStatement(s, p, o)); + assertTrue(repository.hasStatement(s, null, null)); + + Statement stmt2 = Values.statement(s, p, Values.literal("o2")); + Statement stmt3 = Values.statement(s, p, Values.literal("o3")); + repository.add(stmt2, stmt3); + assertEquals(3L, repository.size()); + } + + @Test + @DisplayName("MaterializedResults preserves binding variable projection order") + void materializedResultsPreservesBindingOrder() throws RepositoryException { + repository.update("INSERT DATA { }"); + + try (var result = repository.select("SELECT ?first ?second WHERE { ?first ?second }")) { + assertTrue(result.hasNext()); + var bs = result.next(); + List names = List.copyOf(bs.getBindingNames()); + assertEquals(List.of("first", "second"), names); + } } } }