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: + *

+ * 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

+ * + * + *

Entry Points

+ * + * + *

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:

+ * + * + *

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

+ * */ 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

+ * */ 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

+ * + * + *

Graph Semantics & Invariants

+ * */ 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}:

+ * */ 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

+ * + * + *

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:

+ * + */ 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

+ * + * + *

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

+ * * - *

- * 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

+ * + * + *

Threading and Concurrency Model

+ * */ 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()); + } +}