diff --git a/src/main/java/fr/inria/corese/core/next/data/Values.java b/src/main/java/fr/inria/corese/core/next/data/Values.java
index 8ce79cd09..be7262e46 100644
--- a/src/main/java/fr/inria/corese/core/next/data/Values.java
+++ b/src/main/java/fr/inria/corese/core/next/data/Values.java
@@ -1,15 +1,38 @@
package fr.inria.corese.core.next.data;
-import fr.inria.corese.core.next.data.api.factory.ValueFactory;
import fr.inria.corese.core.next.data.api.factory.NativeValueFactory;
+import fr.inria.corese.core.next.data.api.factory.ValueFactory;
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.data.api.term.BNode;
+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.data.api.term.Resource;
+import fr.inria.corese.core.next.data.api.term.Triple;
+import fr.inria.corese.core.next.data.api.term.Value;
+
+import javax.xml.datatype.XMLGregorianCalendar;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalAmount;
+import java.util.Objects;
/**
* Public entry point for creating RDF terms and statements independently of a
* repository.
*
- *
The returned factory is thread-safe and may be shared. When working with a
- * repository, prefer its own value factory so backend-specific implementations
- * remain possible.
+ * All static methods delegate to the thread-safe shared default {@link ValueFactory}.
+ * When working inside a specific repository or storage backend, prefer obtaining the factory
+ * from the repository itself so backend-optimized implementations can be used.
+ *
+ * Usage Example
+ * {@code
+ * IRI person = Values.iri("http://schema.org/Person");
+ * IRI name = Values.iri("http://schema.org/name");
+ * BNode alice = Values.bnode();
+ * Literal label = Values.literal("Alice", "en");
+ * Statement stmt = Values.statement(alice, name, label);
+ * }
*/
public final class Values {
@@ -26,4 +49,233 @@ private Values() {
public static ValueFactory factory() {
return DEFAULT_FACTORY;
}
+
+ /**
+ * Creates an IRI from a full IRI string.
+ *
+ * @param iri valid IRI string
+ * @return IRI instance
+ */
+ public static IRI iri(String iri) {
+ return DEFAULT_FACTORY.createIRI(iri);
+ }
+
+ /**
+ * Creates an IRI by concatenating a namespace and a local name.
+ *
+ * @param namespace namespace prefix or base URI
+ * @param localName local name
+ * @return IRI instance
+ */
+ public static IRI iri(String namespace, String localName) {
+ return DEFAULT_FACTORY.createIRI(namespace, localName);
+ }
+
+ /**
+ * Creates an anonymous blank node with an automatically generated identifier.
+ *
+ * @return unique blank node
+ */
+ public static BNode bnode() {
+ return DEFAULT_FACTORY.createBNode();
+ }
+
+ /**
+ * Creates a blank node with the given identifier.
+ *
+ * @param nodeId blank node label or identifier
+ * @return blank node instance
+ */
+ public static BNode bnode(String nodeId) {
+ return DEFAULT_FACTORY.createBNode(nodeId);
+ }
+
+ /**
+ * Creates an untyped plain literal (xsd:string).
+ *
+ * @param label lexical value
+ * @return Literal instance
+ */
+ public static Literal literal(String label) {
+ return DEFAULT_FACTORY.createLiteral(label);
+ }
+
+ /**
+ * Creates a language-tagged literal.
+ *
+ * @param label lexical value
+ * @param language BCP-47 language tag
+ * @return Literal instance typed as rdf:langString
+ */
+ public static Literal literal(String label, String language) {
+ return DEFAULT_FACTORY.createLiteral(label, language);
+ }
+
+ /**
+ * Creates a typed literal with an explicit datatype IRI.
+ *
+ * @param label lexical value
+ * @param datatype datatype IRI
+ * @return Literal instance
+ */
+ public static Literal literal(String label, IRI datatype) {
+ return DEFAULT_FACTORY.createLiteral(label, datatype);
+ }
+
+ /**
+ * Creates a literal typed as xsd:boolean.
+ *
+ * @param value boolean value
+ * @return Literal instance
+ */
+ public static Literal literal(boolean value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:byte.
+ *
+ * @param value byte value
+ * @return Literal instance
+ */
+ public static Literal literal(byte value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:short.
+ *
+ * @param value short value
+ * @return Literal instance
+ */
+ public static Literal literal(short value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:int.
+ *
+ * @param value int value
+ * @return Literal instance
+ */
+ public static Literal literal(int value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:long.
+ *
+ * @param value long value
+ * @return Literal instance
+ */
+ public static Literal literal(long value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:float.
+ *
+ * @param value float value
+ * @return Literal instance
+ */
+ public static Literal literal(float value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:double.
+ *
+ * @param value double value
+ * @return Literal instance
+ */
+ public static Literal literal(double value) {
+ return DEFAULT_FACTORY.createLiteral(value);
+ }
+
+ /**
+ * Creates a literal typed as xsd:decimal.
+ *
+ * @param decimal BigDecimal value
+ * @return Literal instance
+ */
+ public static Literal literal(BigDecimal decimal) {
+ return DEFAULT_FACTORY.createLiteral(decimal);
+ }
+
+ /**
+ * Creates a literal typed as xsd:integer.
+ *
+ * @param integer BigInteger value
+ * @return Literal instance
+ */
+ public static Literal literal(BigInteger integer) {
+ return DEFAULT_FACTORY.createLiteral(integer);
+ }
+
+ /**
+ * Creates a literal typed as xsd:dateTime from a temporal accessor.
+ *
+ * @param temporal temporal value
+ * @return Literal instance
+ */
+ public static Literal literal(TemporalAccessor temporal) {
+ return DEFAULT_FACTORY.createLiteral(temporal);
+ }
+
+ /**
+ * Creates a literal typed as xsd:duration from a temporal amount.
+ *
+ * @param amount temporal amount value
+ * @return Literal instance
+ */
+ public static Literal literal(TemporalAmount amount) {
+ return DEFAULT_FACTORY.createLiteral(amount);
+ }
+
+ /**
+ * Creates a literal typed as xsd:dateTime from an XMLGregorianCalendar.
+ *
+ * @param calendar XMLGregorianCalendar value
+ * @return Literal instance
+ */
+ public static Literal literal(XMLGregorianCalendar calendar) {
+ return DEFAULT_FACTORY.createLiteral(calendar);
+ }
+
+ /**
+ * Creates an RDF statement in the default graph (context is {@code null}).
+ *
+ * @param subject statement subject (Resource)
+ * @param predicate statement predicate (IRI)
+ * @param object statement object (Value)
+ * @return Statement instance
+ */
+ public static Statement statement(Resource subject, IRI predicate, Value object) {
+ return DEFAULT_FACTORY.createStatement(subject, predicate, object);
+ }
+
+ /**
+ * Creates an RDF quad statement in the designated named graph context.
+ *
+ * @param subject statement subject (Resource)
+ * @param predicate statement predicate (IRI)
+ * @param object statement object (Value)
+ * @param context graph context (Resource), or {@code null} for default graph
+ * @return Statement instance
+ */
+ public static Statement statement(Resource subject, IRI predicate, Value object, Resource context) {
+ return DEFAULT_FACTORY.createStatement(subject, predicate, object, context);
+ }
+
+ /**
+ * Creates an RDF-star embedded triple term.
+ *
+ * @param subject triple subject (Resource)
+ * @param predicate triple predicate (IRI)
+ * @param object triple object (Value)
+ * @return Triple instance
+ */
+ public static Triple triple(Resource subject, IRI predicate, Value object) {
+ return DEFAULT_FACTORY.createTriple(subject, predicate, object);
+ }
}
diff --git a/src/main/java/fr/inria/corese/core/next/data/api/model/Statement.java b/src/main/java/fr/inria/corese/core/next/data/api/model/Statement.java
index 0b45a82e7..aa932e7fa 100644
--- a/src/main/java/fr/inria/corese/core/next/data/api/model/Statement.java
+++ b/src/main/java/fr/inria/corese/core/next/data/api/model/Statement.java
@@ -7,40 +7,72 @@
import fr.inria.corese.core.next.data.api.term.Value;
/**
- * Represents an RDF statement or quad, composed of a subject, predicate, object, and optional context.
+ * Represents an RDF statement or quad in Corese v5, composed of a subject, predicate, object,
+ * and optional context (named graph).
+ *
+ * Immutability and Value Semantics
+ * Implementations of this interface must be strictly immutable and thread-safe. Once constructed,
+ * neither the statement itself nor the references to its component terms may change.
+ *
+ * Graph and Context Semantics
+ * When {@link #getContext()} returns {@code null}, the statement belongs to the default graph.
+ * When {@link #getContext()} returns a non-null {@link Resource} (an {@link IRI} or {@link fr.inria.corese.core.next.data.api.term.BNode}),
+ * the statement is a quad belonging to that designated named graph.
+ *
+ * Equality Contract
+ * Two statements {@code s1} and {@code s2} are equal if and only if:
+ *
+ * - {@code s1.getSubject().equals(s2.getSubject())}
+ * - {@code s1.getPredicate().equals(s2.getPredicate())}
+ * - {@code s1.getObject().equals(s2.getObject())}
+ * - {@code Objects.equals(s1.getContext(), s2.getContext())}
+ *
+ * Equality must be symmetric across different implementations of this interface.
*/
public interface Statement extends Serializable {
/**
- * Gets the subject of this statement.
+ * Gets the subject resource of this statement (IRI, BNode, or Triple term).
+ *
+ * @return the non-null subject
*/
Resource getSubject();
/**
- * Gets the predicate of this statement.
+ * Gets the predicate IRI of this statement.
+ *
+ * @return the non-null predicate IRI
*/
IRI getPredicate();
/**
- * Gets the object of this statement.
+ * Gets the object value of this statement (Resource or Literal).
+ *
+ * @return the non-null object value
*/
Value getObject();
/**
- * Gets the context of this statement.
+ * Gets the context of this statement, identifying its containing graph.
+ *
+ * @return the graph resource identifier, or {@code null} if this statement belongs to the default graph
*/
Resource getContext();
/**
- * Compares this statement to another object.
+ * Compares this statement to another object according to RDF statement equality rules.
+ *
+ * @param other the object to compare against
+ * @return {@code true} if both statements have equal subjects, predicates, objects, and contexts
*/
@Override
boolean equals(Object other);
/**
- * Computes the hash code of this statement.
+ * Computes the hash code of this statement based on its subject, predicate, object, and context.
+ *
+ * @return integer hash code consistent with {@link #equals(Object)}
*/
@Override
int hashCode();
-
}
diff --git a/src/main/java/fr/inria/corese/core/next/data/package-info.java b/src/main/java/fr/inria/corese/core/next/data/package-info.java
index 850ab0675..8d5c0a482 100644
--- a/src/main/java/fr/inria/corese/core/next/data/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/data/package-info.java
@@ -1,8 +1,31 @@
/**
- * RDF data model, public contracts, extension SPI, and Corese implementations.
+ * Core RDF data model, value representation, and terminology for Corese v5.
*
- * The package is split into stable {@code api} contracts, a public
- * {@code spi} for implementers, and an implementation-only {@code impl}
- * boundary.
+ * Three-Tier Package Architecture
+ *
+ * - {@link fr.inria.corese.core.next.data.api}: Public API contracts (interfaces and models)
+ * intended for end-users, query evaluators, and client applications. Includes {@code term},
+ * {@code model}, {@code factory}, {@code namespace}, and {@code literal}.
+ * - {@link fr.inria.corese.core.next.data.spi}: Service-provider interfaces (SPI) intended for
+ * backend implementers, custom datatypes, and parser/serializer plugins.
+ * - {@link fr.inria.corese.core.next.data.impl}: Internal, encapsulated default implementations
+ * ({@code SimpleModel}, {@code SimpleStatement}, internal dictionary encoders).
+ *
+ *
+ * Entry Points
+ *
+ * - {@link fr.inria.corese.core.next.data.Values}: Static convenience factory for creating
+ * IRIs, blank nodes, typed literals, statements, and RDF-star triple terms.
+ * - {@link fr.inria.corese.core.next.data.api.model.Model}: Mutable and immutable in-memory
+ * RDF graphs supporting collection-like set operations, namespace mappings, and filtered views.
+ *
+ *
+ * Thread Safety and Immutability
+ * All RDF terms ({@link fr.inria.corese.core.next.data.api.term.IRI},
+ * {@link fr.inria.corese.core.next.data.api.term.Literal},
+ * {@link fr.inria.corese.core.next.data.api.term.BNode}, and
+ * {@link fr.inria.corese.core.next.data.api.model.Statement}) are strictly immutable and thread-safe.
+ * {@link fr.inria.corese.core.next.data.api.model.Model} instances are not thread-safe unless
+ * explicitly decorated or backed by a transactional storage manager.
*/
package fr.inria.corese.core.next.data;
diff --git a/src/main/java/fr/inria/corese/core/next/io/package-info.java b/src/main/java/fr/inria/corese/core/next/io/package-info.java
index 7de0fd9f5..9b882ac5e 100644
--- a/src/main/java/fr/inria/corese/core/next/io/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/io/package-info.java
@@ -1,5 +1,18 @@
/**
- * Unified public facade for reading RDF and exporting RDF graphs or SPARQL
- * query results.
+ * Unified I/O and serialization facade for RDF graphs, datasets, and SPARQL query results in Corese v5.
+ *
+ * CoreseIO Facade
+ * {@link fr.inria.corese.core.next.io.CoreseIO} provides standard static utility methods for:
+ *
+ * - Reading RDF: Parsing files, input streams, and readers into {@link fr.inria.corese.core.next.data.api.model.Model}
+ * instances across standard formats (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD, RDFa).
+ * - Writing RDF: Serializing models and statements to disk or streams with configurable formatting options.
+ * - Exporting Query Results: Formatting SPARQL SELECT/ASK results into standard XML, JSON, CSV, and TSV formats.
+ *
+ *
+ * Stream and Resource Management
+ * Methods accepting or returning streams assume standard caller ownership rules: callers remain
+ * responsible for closing input streams and writers passed into {@code CoreseIO}. Methods returning
+ * internal stream views must be wrapped in {@code try-with-resources} blocks.
*/
package fr.inria.corese.core.next.io;
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java b/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java
index f20d29d1f..8c6d57840 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java
@@ -8,11 +8,24 @@
import fr.inria.corese.core.next.storage.api.transaction.TransactionManager;
/**
- * Service-provider contract for an RDF storage backend.
+ * Service-provider contract (SPI) for an RDF storage backend in Corese v5.
*
- * Repository users normally interact with the query API rather than this
- * interface. Storage implementations expose focused capabilities for reads,
- * writes, metadata, transactions, and lifecycle management.
+ * Repository users normally interact with the query engine or repository facade rather than
+ * this low-level SPI. Storage implementations expose focused, cohesive capabilities for reads
+ * ({@link #queries()}), writes ({@link #mutations()}), structural introspection ({@link #metadata()}),
+ * transactions ({@link #transactions()}), and lifecycle management ({@link #lifecycle()}).
+ *
+ * Threading & Session Confinement
+ *
+ * - Root Manager: The root {@link StorageManager} instance represents the shared
+ * database state and its lifecycle. It is thread-safe for opening connection-scoped sessions.
+ * - Working Sessions: Working sessions obtained via {@link #openSession()} and
+ * their associated operation facets ({@link #queries()}, {@link #mutations()}) are confined
+ * to a single thread. Multiple threads must each obtain their own session.
+ * - Snapshot Views: Independent, point-in-time read views obtained via
+ * {@link #openReadSnapshot()} provide immutable snapshots. Reads are confined to the calling
+ * thread, but {@link #close()} must be idempotent and safe to invoke concurrently from a cancellation thread.
+ *
*/
public interface StorageManager extends AutoCloseable {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/model/StatementPattern.java b/src/main/java/fr/inria/corese/core/next/storage/api/model/StatementPattern.java
index a43186307..2017e1daa 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/model/StatementPattern.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/model/StatementPattern.java
@@ -1,5 +1,6 @@
package fr.inria.corese.core.next.storage.api.model;
+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.Resource;
import fr.inria.corese.core.next.data.api.term.Value;
@@ -71,6 +72,23 @@ public static StatementPattern of(Resource subject, IRI predicate, Value object,
.build();
}
+ /**
+ * Creates an exact pattern matching the given statement in its designated context.
+ *
+ * If the statement specifies a non-null context, the pattern restricts matching
+ * strictly to that named graph. If the statement has a {@code null} context (default graph),
+ * the pattern selects the default graph specifically via {@code new Resource[] { null }}.
+ *
+ * @param statement statement to match (must not be {@code null})
+ * @return exact statement pattern
+ * @throws NullPointerException if {@code statement} is {@code null}
+ */
+ public static StatementPattern of(Statement statement) {
+ Objects.requireNonNull(statement, "statement");
+ Resource context = statement.getContext();
+ return of(statement.getSubject(), statement.getPredicate(), statement.getObject(), new Resource[] { context });
+ }
+
/**
* Gets the subject pattern.
*
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperations.java b/src/main/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperations.java
index f9d561146..ecfa1fba3 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperations.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperations.java
@@ -7,10 +7,23 @@
import fr.inria.corese.core.next.storage.api.model.PredicateStatistics;
import fr.inria.corese.core.next.storage.api.model.StorageStatistics;
+import java.util.Optional;
import java.util.Set;
/**
- * Structural summaries exposed by an RDF storage backend.
+ * Structural summaries and cardinality statistics exposed by an RDF storage backend.
+ *
+ * Usage & Performance Considerations
+ *
+ * - Materialized Sets: Methods such as {@link #getPredicates()}, {@link #getSubjects()},
+ * and {@link #getObjects()} return fully materialized, unmodifiable sets. On very large stores,
+ * calling {@code getSubjects()} or {@code getObjects()} can incur significant heap allocation overhead.
+ * - Cost-Based Optimization (CBO): {@link #getStatistics()} and
+ * {@link #findPredicateStatistics(IRI)} provide exact or sampled cardinality metrics
+ * utilized by query planners to reorder triple join patterns efficiently.
+ * - Named Graph Registry: {@link #getContexts()} returns strictly the identifiers
+ * of named graphs; the default graph is not considered a named context and is not part of this set.
+ *
*/
public interface MetadataOperations {
@@ -65,4 +78,18 @@ public interface MetadataOperations {
default PredicateStatistics getPredicateStatistics(IRI predicate) throws StorageException {
return null;
}
+
+ /**
+ * Returns statistics about a specific predicate wrapped in an {@link Optional}.
+ *
+ * This method provides a modern, type-safe alternative to {@link #getPredicateStatistics(IRI)}
+ * that avoids returning raw {@code null} values.
+ *
+ * @param predicate the predicate IRI to inspect (must not be {@code null})
+ * @return an {@link Optional} containing the predicate statistics if available, or {@link Optional#empty()}
+ * @throws StorageException if statistics cannot be computed
+ */
+ default Optional findPredicateStatistics(IRI predicate) throws StorageException {
+ return Optional.ofNullable(getPredicateStatistics(predicate));
+ }
}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/operations/MutationOperations.java b/src/main/java/fr/inria/corese/core/next/storage/api/operations/MutationOperations.java
index 2b5b4b70d..7c87a4b40 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/operations/MutationOperations.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/operations/MutationOperations.java
@@ -8,11 +8,31 @@
import java.util.Objects;
/**
- * Write operations implemented by an RDF storage backend.
+ * Write and update operations implemented by an RDF storage backend session.
*
- * Duplicate insertion and removal of absent statements are normal set
- * operations, not failures. Exceptional backend failures are reported with
- * {@link StorageException}.
+ * Threading & Session Confinement
+ * Instances obtained from a {@link fr.inria.corese.core.next.storage.api.StorageManager} session
+ * are not thread-safe. Mutations must be confined to the session's owning thread.
+ *
+ * Transactions & Visibility
+ *
+ * - When operating outside an active transaction, mutations may auto-commit immediately
+ * or participate in an implicit session transaction depending on backend configuration.
+ * - When an explicit transaction is active (via {@link fr.inria.corese.core.next.storage.api.transaction.TransactionManager}),
+ * uncommitted mutations are immediately visible to the current session but remain isolated
+ * from other concurrent sessions until commit.
+ * - The default implementations of batch operations (such as {@link #addAll}) iterate sequentially;
+ * backends with transactional staging may override them to provide atomic batch publishing.
+ *
+ *
+ * Graph Semantics & Invariants
+ *
+ * - Inserting a statement with a {@code null} context targets the default graph.
+ * - {@link #createGraph(Resource)} registers an empty named graph without requiring statements.
+ * Registered empty named graphs survive {@link #clear(Resource...)} but are removed by {@link #dropGraph(Resource)}.
+ * - Duplicate insertion and removal of absent statements are idempotent set operations,
+ * returning {@code false} without raising exceptions.
+ *
*/
public interface MutationOperations {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java b/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java
index 5a9dd62d1..a95cac56b 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java
@@ -7,7 +7,29 @@
import java.util.stream.Stream;
/**
- * Read operations implemented by an RDF storage backend.
+ * Read and pattern-matching operations implemented by an RDF storage backend session.
+ *
+ * Threading & Session Confinement
+ * Instances obtained from a {@link fr.inria.corese.core.next.storage.api.StorageManager} session
+ * are not thread-safe. Each thread must operate on its own session obtained via
+ * {@link fr.inria.corese.core.next.storage.api.StorageManager#openSession()} or an independent
+ * snapshot via {@link fr.inria.corese.core.next.storage.api.StorageManager#openReadSnapshot()}.
+ *
+ * Stream & Resource Lifecycle
+ * Streams returned by {@link #find(StatementPattern)} are lazy, single-use, and may hold low-level
+ * cursor and snapshot partition resources. Callers must always close returned streams,
+ * preferably using a {@code try-with-resources} block. This requirement applies even when using
+ * short-circuiting terminal operations such as {@link Stream#findAny()} or {@link Stream#limit(long)},
+ * as the terminal operation alone does not automatically close the underlying stream. Failure to close
+ * streams can lead to partition retention and increased memory footprint.
+ *
+ * Graph Matching Semantics
+ * Pattern matching respects the dataset semantics defined by {@link StatementPattern}:
+ *
+ * - A {@code null} or empty contexts array matches statements across all graphs (default graph and named graphs).
+ * - An explicit {@code null} array element matches only statements in the default graph.
+ * - Specific resource IRIs or blank nodes match only statements in those designated named graphs.
+ *
*/
public interface QueryOperations {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/operations/package-info.java b/src/main/java/fr/inria/corese/core/next/storage/api/operations/package-info.java
index c11988b92..4c0c95d52 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/operations/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/operations/package-info.java
@@ -1,2 +1,29 @@
-/** Focused query, mutation, and metadata contracts for storage providers. */
+/**
+ * Focused query, mutation, and metadata operation contracts for RDF storage backends.
+ *
+ * Operation Facets
+ *
+ * - {@link fr.inria.corese.core.next.storage.api.operations.QueryOperations}: Read operations providing
+ * lazy statement streams ({@code find}), exact counts ({@code count}), fast existence tests
+ * ({@code contains}), and cache invalidation tokens ({@code getDataVersion}).
+ * - {@link fr.inria.corese.core.next.storage.api.operations.MutationOperations}: Write operations providing
+ * single and batch additions/removals, pattern-based deletions, and graph registry lifecycle.
+ * - {@link fr.inria.corese.core.next.storage.api.operations.MetadataOperations}: Structural summaries
+ * and cardinality statistics used by the cost-based query optimizer (CBO).
+ *
+ *
+ * Stream and Resource Lifecycle
+ * Streams returned by {@link fr.inria.corese.core.next.storage.api.operations.QueryOperations#find}
+ * are lazy, single-use, and may hold low-level snapshot references. Callers must always close them,
+ * preferably using {@code try-with-resources}, to prevent snapshot and partition leaks.
+ *
+ * Graph Matching Semantics
+ * Operations accepting {@link fr.inria.corese.core.next.storage.api.model.StatementPattern} follow standard
+ * SPARQL dataset semantics:
+ *
+ * - A {@code null} or empty contexts array matches across all graphs (the union of default and named graphs).
+ * - An explicit {@code null} array element (e.g., {@code new Resource[] { null }}) matches strictly the default graph.
+ * - Named graph identifiers restrict matching to the specified named graph(s).
+ *
+ */
package fr.inria.corese.core.next.storage.api.operations;
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/package-info.java b/src/main/java/fr/inria/corese/core/next/storage/api/package-info.java
index 4fcffaebc..97bd4116d 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/package-info.java
@@ -1,2 +1,23 @@
-/** Service-provider contracts for pluggable RDF storage backends. */
+/**
+ * Service-provider contracts (SPI) for pluggable RDF storage backends in Corese v5.
+ *
+ * Core Interfaces
+ *
+ * - {@link fr.inria.corese.core.next.storage.api.StorageManager}: Central coordinator for a storage
+ * instance, exposing read, write, metadata, transaction, and lifecycle facets.
+ * - {@link fr.inria.corese.core.next.storage.api.operations.QueryOperations}: High-performance pattern
+ * matching and statement streaming.
+ * - {@link fr.inria.corese.core.next.storage.api.operations.MutationOperations}: Statement additions,
+ * deletions, batch operations, and named graph registry management.
+ * - {@link fr.inria.corese.core.next.storage.api.operations.MetadataOperations}: Introspection of
+ * predicates, subjects, objects, named contexts, and cardinality statistics for cost-based query optimization.
+ * - {@link fr.inria.corese.core.next.storage.api.transaction.TransactionManager}: ACID / Snapshot transaction
+ * boundaries and isolation control.
+ *
+ *
+ * Design Principles
+ * Contracts in this package follow the Single Responsibility Principle (SRP) by segregating
+ * query, mutation, introspection, and transaction capabilities into distinct operation interfaces.
+ * Implementations are expected to be backend-neutral, hiding internal indexing or memory layout details.
+ */
package fr.inria.corese.core.next.storage.api;
diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/transaction/TransactionManager.java b/src/main/java/fr/inria/corese/core/next/storage/api/transaction/TransactionManager.java
index 76b2e9152..749a1b498 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/api/transaction/TransactionManager.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/api/transaction/TransactionManager.java
@@ -6,7 +6,19 @@
import java.util.Set;
/**
- * Transaction management for the StorageManager.
+ * Transaction boundary and isolation management for an RDF storage session.
+ *
+ * Session Scoping & Operation Participation
+ * A {@link TransactionManager} instance is obtained from a {@link fr.inria.corese.core.next.storage.api.StorageManager}
+ * session via {@link fr.inria.corese.core.next.storage.api.StorageManager#transactions()}.
+ * Any transaction started through this manager implicitly governs all subsequent read and write
+ * operations performed on that same session (via {@link fr.inria.corese.core.next.storage.api.StorageManager#queries()}
+ * and {@link fr.inria.corese.core.next.storage.api.StorageManager#mutations()}).
+ *
+ * Snapshot Isolation Semantics
+ * Backends supporting {@link IsolationLevel#SNAPSHOT} capture a consistent point-in-time view
+ * of the database upon transaction start. Uncommitted writes within the transaction are visible
+ * to the owning session but remain completely hidden from concurrent sessions until committed.
*/
public interface TransactionManager {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/package-info.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/package-info.java
index 4daf92a5d..e25ee5f45 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/package-info.java
@@ -1,20 +1,35 @@
/**
- * Native in-memory RDF storage with insertion-ordered SPO, POS and OSP indexes.
+ * Native in-memory RDF storage backend with insertion-ordered SPO, POS, and OSP indexes.
*
- *
- * Graph partitions share immutable RDF terms and statements. Transactions and lazy reads retain partition versions;
- * writers detach shared index containers before mutation. Ownership changes and publication use a database monitor,
- * while sequential snapshot traversal runs outside it. This does not make a single transaction or stream safe for
- * concurrent use by multiple consumers.
+ *
Internal Architecture
+ *
+ * - {@link fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager}: Facade implementing
+ * the SPI {@link fr.inria.corese.core.next.storage.api.StorageManager}, coordinating query, mutation,
+ * metadata, and transaction delegates.
+ * - {@link fr.inria.corese.core.next.storage.impl.memory.IndexedStatementStore}: Core engine managing
+ * partition-based indexing, snapshot lifecycle, and transaction commits.
+ * - {@link fr.inria.corese.core.next.storage.impl.memory.TripleIndex}: Indexing container
+ * organizing statements in primary (SPO) and secondary (POS, OSP) orders with fast prefix iteration.
+ * - {@link fr.inria.corese.core.next.storage.impl.memory.DenseTermTable} &
+ * {@link fr.inria.corese.core.next.storage.impl.memory.CompactTermMap}: Memory-efficient hash and
+ * array-backed dictionary structures avoiding standard {@code java.util.HashMap} object overhead.
+ *
*
- *
- * Callers must close streams returned by query operations, preferably with try-with-resources, even after a
- * short-circuiting terminal operation. Completion of a terminal operation alone does not guarantee release of the
- * captured state. Empty named graphs survive CLEAR and disappear on DROP. Batch ingestion reads its source once and
- * publishes atomically after staging and index construction.
+ *
Copy-On-Write (COW) Concurrency Model
+ * Graph partitions share immutable RDF terms and statements. Transactions and lazy reads retain
+ * partition versions without deep copies; writers detach shared index containers before mutation
+ * (Copy-On-Write). Ownership changes and publication use a database monitor, while sequential snapshot
+ * traversal runs outside it. This does not make a single transaction or stream safe for concurrent
+ * use by multiple consumers.
*
- *
- * This is the default in-memory backend selected by {@code Storages.create()} or through the {@code "memory"}
- * storage configuration.
+ *
Stream and Resource Lifecycle
+ * Callers must close streams returned by query operations, preferably with {@code try-with-resources},
+ * even after a short-circuiting terminal operation (such as {@code findAny()} or {@code limit()}).
+ * Completion of a terminal operation alone does not guarantee release of the captured snapshot partition state.
+ * Empty named graphs survive {@code CLEAR} and disappear on {@code DROP}. Batch ingestion reads its source once
+ * and publishes atomically after staging and index construction.
+ *
+ * This is the default in-memory backend selected by {@code Storages.create()} or through
+ * the {@code "memory"} storage configuration.
*/
package fr.inria.corese.core.next.storage.impl.memory;
diff --git a/src/main/java/fr/inria/corese/core/next/storage/package-info.java b/src/main/java/fr/inria/corese/core/next/storage/package-info.java
index 7d030a625..b0cdc845a 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/package-info.java
@@ -1,7 +1,33 @@
/**
- * Backend-neutral RDF storage services.
+ * Backend-neutral RDF storage architecture for Corese v5.
*
- * {@code api} defines storage capabilities; {@code impl} provides the in-memory and legacy
- * Corese graph backends.
+ * Architecture Overview
+ * The {@code storage} module decouples low-level RDF statement persistence and indexing
+ * from higher-level query evaluation, reasoning, and I/O. It exposes focused SPI contracts
+ * that allow pluggable storage backends (in-memory, persistent, or federated).
+ *
+ * Package Organization
+ *
+ * - {@link fr.inria.corese.core.next.storage.api}: Public service-provider interfaces (SPI)
+ * defining storage management, queries, mutations, transactions, metadata, and lifecycle.
+ * - {@link fr.inria.corese.core.next.storage.impl.memory}: High-performance in-memory
+ * storage backend leveraging Copy-On-Write (COW) indexing, compact dictionary encoding,
+ * and partition-based snapshot isolation.
+ * - {@link fr.inria.corese.core.next.storage.Storages}: Static factory and service locator
+ * for creating and configuring storage managers.
+ * - {@link fr.inria.corese.core.next.storage.StorageModels}: Bridge exposing a storage backend
+ * as a mutable, live RDF {@link fr.inria.corese.core.next.data.api.model.Model}.
+ *
+ *
+ * Threading and Concurrency Model
+ *
+ * - Shared Database vs. Sessions: The root {@link fr.inria.corese.core.next.storage.api.StorageManager}
+ * instance represents the shared database lifecycle and is thread-safe. Working sessions
+ * obtained via {@link fr.inria.corese.core.next.storage.api.StorageManager#openSession()} are
+ * confined to a single client thread.
+ * - Snapshot Isolation: Independent read views obtained via
+ * {@link fr.inria.corese.core.next.storage.api.StorageManager#openReadSnapshot()} provide immutable,
+ * point-in-time snapshots unaffected by concurrent or subsequent writes.
+ *
*/
package fr.inria.corese.core.next.storage;
diff --git a/src/test/java/fr/inria/corese/core/next/data/ValuesTest.java b/src/test/java/fr/inria/corese/core/next/data/ValuesTest.java
new file mode 100644
index 000000000..456c8b4d0
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/data/ValuesTest.java
@@ -0,0 +1,156 @@
+package fr.inria.corese.core.next.data;
+
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.data.api.term.BNode;
+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.data.api.term.Triple;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Values Facade Tests")
+class ValuesTest {
+
+ @Test
+ @DisplayName("Values.factory() returns a non-null default factory")
+ void testFactory() {
+ assertNotNull(Values.factory());
+ }
+
+ @Test
+ @DisplayName("Values.iri creates valid IRIs")
+ void testIri() {
+ IRI iri1 = Values.iri("http://example.org/alice");
+ assertNotNull(iri1);
+ assertEquals("http://example.org/alice", iri1.stringValue());
+
+ IRI iri2 = Values.iri("http://example.org/", "bob");
+ assertNotNull(iri2);
+ assertEquals("http://example.org/bob", iri2.stringValue());
+ }
+
+ @Test
+ @DisplayName("Values.bnode creates blank nodes with generated and explicit IDs")
+ void testBnode() {
+ BNode b1 = Values.bnode();
+ BNode b2 = Values.bnode();
+ assertNotNull(b1);
+ assertNotNull(b2);
+ assertNotEquals(b1.getID(), b2.getID());
+
+ BNode bExplicit = Values.bnode("b42");
+ assertNotNull(bExplicit);
+ assertEquals("b42", bExplicit.getID());
+ }
+
+ @Test
+ @DisplayName("Values.literal creates plain, language, and typed literals")
+ void testLiteral() {
+ Literal lPlain = Values.literal("hello");
+ assertEquals("hello", lPlain.getLabel());
+
+ Literal lLang = Values.literal("bonjour", "fr");
+ assertEquals("bonjour", lLang.getLabel());
+ assertTrue(lLang.getLanguage().isPresent());
+ assertEquals("fr", lLang.getLanguage().get());
+
+ IRI xsdInteger = Values.iri("http://www.w3.org/2001/XMLSchema#integer");
+ Literal lTyped = Values.literal("123", xsdInteger);
+ assertEquals("123", lTyped.getLabel());
+ assertEquals(xsdInteger, lTyped.getDatatype());
+ }
+
+ @Test
+ @DisplayName("Values.literal creates primitive numeric and boolean literals")
+ void testPrimitiveLiterals() {
+ Literal lBool = Values.literal(true);
+ assertEquals("true", lBool.getLabel());
+
+ Literal lByte = Values.literal((byte) 8);
+ assertEquals("8", lByte.getLabel());
+
+ Literal lShort = Values.literal((short) 16);
+ assertEquals("16", lShort.getLabel());
+
+ Literal lInt = Values.literal(42);
+ assertEquals("42", lInt.getLabel());
+
+ Literal lLong = Values.literal(123456789L);
+ assertEquals("123456789", lLong.getLabel());
+
+ Literal lFloat = Values.literal(1.25f);
+ assertEquals("1.25", lFloat.getLabel());
+
+ Literal lDouble = Values.literal(2.5);
+ assertEquals("2.5", lDouble.getLabel());
+
+ Literal lDec = Values.literal(new BigDecimal("99.99"));
+ assertEquals("99.99", lDec.getLabel());
+
+ Literal lBigInt = Values.literal(BigInteger.valueOf(1000));
+ assertEquals("1000", lBigInt.getLabel());
+ }
+
+ @Test
+ @DisplayName("Values.literal creates temporal literals")
+ void testTemporalLiterals() throws DatatypeConfigurationException {
+ Instant now = Instant.parse("2026-10-23T15:30:00Z");
+ Literal lTemporal = Values.literal(now);
+ assertNotNull(lTemporal);
+ assertTrue(lTemporal.getLabel().contains("2026-10-23"));
+
+ Duration duration = Duration.ofHours(2);
+ Literal lDuration = Values.literal(duration);
+ assertNotNull(lDuration);
+ assertEquals("PT2H", lDuration.getLabel());
+
+ XMLGregorianCalendar cal = DatatypeFactory.newInstance().newXMLGregorianCalendar("2026-10-23T15:30:00Z");
+ Literal lXmlCal = Values.literal(cal);
+ assertNotNull(lXmlCal);
+ assertEquals("2026-10-23T15:30:00Z", lXmlCal.getLabel());
+ }
+
+ @Test
+ @DisplayName("Values.statement creates default graph and named graph statements")
+ void testStatement() {
+ IRI s = Values.iri("http://example.org/s");
+ IRI p = Values.iri("http://example.org/p");
+ Literal o = Values.literal("o");
+ IRI g = Values.iri("http://example.org/g");
+
+ Statement defaultGraphStmt = Values.statement(s, p, o);
+ assertNotNull(defaultGraphStmt);
+ assertEquals(s, defaultGraphStmt.getSubject());
+ assertEquals(p, defaultGraphStmt.getPredicate());
+ assertEquals(o, defaultGraphStmt.getObject());
+ assertNull(defaultGraphStmt.getContext());
+
+ Statement namedGraphStmt = Values.statement(s, p, o, g);
+ assertNotNull(namedGraphStmt);
+ assertEquals(g, namedGraphStmt.getContext());
+ }
+
+ @Test
+ @DisplayName("Values.triple creates RDF-star triple terms")
+ void testTriple() {
+ IRI s = Values.iri("http://example.org/s");
+ IRI p = Values.iri("http://example.org/p");
+ Literal o = Values.literal("o");
+
+ Triple triple = Values.triple(s, p, o);
+ assertNotNull(triple);
+ assertEquals(s, triple.subject());
+ assertEquals(p, triple.predicate());
+ assertEquals(o, triple.object());
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/storage/api/model/StatementPatternTest.java b/src/test/java/fr/inria/corese/core/next/storage/api/model/StatementPatternTest.java
index c7347a99c..ce0e4b4ae 100644
--- a/src/test/java/fr/inria/corese/core/next/storage/api/model/StatementPatternTest.java
+++ b/src/test/java/fr/inria/corese/core/next/storage/api/model/StatementPatternTest.java
@@ -1,5 +1,6 @@
package fr.inria.corese.core.next.storage.api.model;
+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.Resource;
import fr.inria.corese.core.next.data.api.term.Value;
@@ -148,4 +149,55 @@ void contextSelectionSemantics() {
assertFalse(defaultGraph.isContextWildcard());
assertArrayEquals(new Resource[]{null}, defaultGraph.getContexts());
}
+
+ @Test
+ @DisplayName("Should create exact pattern from statement with named graph context")
+ void testOfStatementWithNamedGraph() {
+ Resource subject = mock(Resource.class);
+ IRI predicate = mock(IRI.class);
+ Value object = mock(Value.class);
+ Resource context = mock(Resource.class);
+
+ Statement statement = mock(Statement.class);
+ org.mockito.Mockito.when(statement.getSubject()).thenReturn(subject);
+ org.mockito.Mockito.when(statement.getPredicate()).thenReturn(predicate);
+ org.mockito.Mockito.when(statement.getObject()).thenReturn(object);
+ org.mockito.Mockito.when(statement.getContext()).thenReturn(context);
+
+ StatementPattern pattern = StatementPattern.of(statement);
+
+ assertEquals(subject, pattern.getSubject());
+ assertEquals(predicate, pattern.getPredicate());
+ assertEquals(object, pattern.getObject());
+ assertArrayEquals(new Resource[]{context}, pattern.getContexts());
+ assertFalse(pattern.isContextWildcard());
+ }
+
+ @Test
+ @DisplayName("Should create exact pattern from statement with default graph context")
+ void testOfStatementWithDefaultGraph() {
+ Resource subject = mock(Resource.class);
+ IRI predicate = mock(IRI.class);
+ Value object = mock(Value.class);
+
+ Statement statement = mock(Statement.class);
+ org.mockito.Mockito.when(statement.getSubject()).thenReturn(subject);
+ org.mockito.Mockito.when(statement.getPredicate()).thenReturn(predicate);
+ org.mockito.Mockito.when(statement.getObject()).thenReturn(object);
+ org.mockito.Mockito.when(statement.getContext()).thenReturn(null);
+
+ StatementPattern pattern = StatementPattern.of(statement);
+
+ assertEquals(subject, pattern.getSubject());
+ assertEquals(predicate, pattern.getPredicate());
+ assertEquals(object, pattern.getObject());
+ assertArrayEquals(new Resource[]{null}, pattern.getContexts());
+ assertFalse(pattern.isContextWildcard());
+ }
+
+ @Test
+ @DisplayName("Should reject null statement in StatementPattern.of(Statement)")
+ void testOfStatementNullRejected() {
+ assertThrows(NullPointerException.class, () -> StatementPattern.of((Statement) null));
+ }
}
diff --git a/src/test/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperationsTest.java b/src/test/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperationsTest.java
new file mode 100644
index 000000000..cdd5bed59
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/storage/api/operations/MetadataOperationsTest.java
@@ -0,0 +1,101 @@
+package fr.inria.corese.core.next.storage.api.operations;
+
+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 fr.inria.corese.core.next.storage.api.exception.StorageException;
+import fr.inria.corese.core.next.storage.api.model.PredicateStatistics;
+import fr.inria.corese.core.next.storage.api.model.StorageStatistics;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+
+@DisplayName("MetadataOperations Default Methods Tests")
+class MetadataOperationsTest {
+
+ private static class StubMetadataOperations implements MetadataOperations {
+ private PredicateStatistics returnedStats;
+
+ void setReturnedStats(PredicateStatistics stats) {
+ this.returnedStats = stats;
+ }
+
+ @Override
+ public Set getPredicates() throws StorageException {
+ return Set.of();
+ }
+
+ @Override
+ public Set getSubjects() throws StorageException {
+ return Set.of();
+ }
+
+ @Override
+ public Set getObjects() throws StorageException {
+ return Set.of();
+ }
+
+ @Override
+ public Set getContexts() throws StorageException {
+ return Set.of();
+ }
+
+ @Override
+ public StorageStatistics getStatistics() throws StorageException {
+ return null;
+ }
+
+ @Override
+ public PredicateStatistics getPredicateStatistics(IRI predicate) throws StorageException {
+ return returnedStats;
+ }
+ }
+
+ @Test
+ @DisplayName("findPredicateStatistics should return empty optional when getPredicateStatistics is null")
+ void testFindPredicateStatisticsEmpty() throws StorageException {
+ StubMetadataOperations operations = new StubMetadataOperations();
+ IRI predicate = mock(IRI.class);
+
+ Optional stats = operations.findPredicateStatistics(predicate);
+
+ assertNotNull(stats);
+ assertTrue(stats.isEmpty());
+ }
+
+ @Test
+ @DisplayName("findPredicateStatistics should return present optional when getPredicateStatistics returns stats")
+ void testFindPredicateStatisticsPresent() throws StorageException {
+ StubMetadataOperations operations = new StubMetadataOperations();
+ IRI predicate = mock(IRI.class);
+ PredicateStatistics expected = new PredicateStatistics(predicate, 42L, 10L, 20L);
+ operations.setReturnedStats(expected);
+
+ Optional stats = operations.findPredicateStatistics(predicate);
+
+ assertNotNull(stats);
+ assertTrue(stats.isPresent());
+ assertEquals(expected, stats.get());
+ }
+
+ @Test
+ @DisplayName("Default implementation of getPredicateStatistics returns null")
+ void testDefaultGetPredicateStatisticsReturnsNull() throws StorageException {
+ MetadataOperations defaultOps = new MetadataOperations() {
+ @Override public Set getPredicates() { return Set.of(); }
+ @Override public Set getSubjects() { return Set.of(); }
+ @Override public Set getObjects() { return Set.of(); }
+ @Override public Set getContexts() { return Set.of(); }
+ @Override public StorageStatistics getStatistics() { return null; }
+ };
+
+ IRI predicate = mock(IRI.class);
+ assertNull(defaultOps.getPredicateStatistics(predicate));
+ assertTrue(defaultOps.findPredicateStatistics(predicate).isEmpty());
+ }
+}