The Store interface extends StoreClient with the ability to group multiple operations into an atomic unit of work.
Transaction semantics follow the same cross-backend principle adopted by
@metreeca/qest: each connector targets the
strongest guarantees its backend supports — drawn from the well-defined counterparts across
SQL:2011,
GQL:2024, and SPARQL 1.1 (
via RDF4J) — and documents the result.
All operations within a transaction either succeed together or are rolled back as a whole. If the task function throws or its returned promise rejects, the transaction is rolled back and the error is propagated to the caller.
Calls to execute are not reentrant. The task receives a StoreClient that does not expose execute, so transaction
boundaries are flat and any composition must share a single outer call site. Nested transactions and savepoints are out
of scope.
Implementations provide best-effort isolation: each connector targets the strongest level its backend supports — ideally snapshot isolation, where the transaction observes a consistent snapshot of the store as of its start time with no phantom or non-repeatable reads — and degrades gracefully to the maximum level achievable by the underlying storage, down to no isolation at all when the backend offers no transactional guarantees.
The exact isolation level is implementation-defined and must be documented by each backend connector.
Standard isolation levels are defined by the anomalies they prevent. Each level subsumes all guarantees of the levels below it.
| Level | Dirty Reads | Non-repeatable Reads | Phantoms | Write Skew |
|---|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible | Possible |
| Read Committed | Prevented | Possible | Possible | Possible |
| Repeatable Read | Prevented | Prevented | Possible | Possible |
| Snapshot Isolation | Prevented | Prevented | Prevented | Possible |
| Serialisable | Prevented | Prevented | Prevented | Prevented |
Snapshot isolation is not part of the SQL standard, which was defined before SI existed. Several engines label their SI implementation as "Repeatable Read" (PostgreSQL, MySQL/InnoDB), blurring the distinction. The table above reflects the theoretical guarantees; see Backend Comparison for actual engine behaviour.
The tables below document how each target backend natively handles transaction isolation, and where connector-level normalisation is required.
The table below documents which isolation levels each target backend supports (★ default, ☆ supported, · not supported). Levels map to the reference table above.
| Engine | RU | RC | RR | SI | SER |
|---|---|---|---|---|---|
| SQL:2011 | |||||
| PostgreSQL | ☆ ¹ | ★ | ☆ ² | · | ☆ |
| MySQL/InnoDB | ☆ | ☆ | ★ ³ | · | ☆ |
| SQL Server | ☆ | ★ | ☆ | ☆ ⁴ | ☆ |
| SQLite | · | · | · | · | ★ |
| GQL:2024 | |||||
| Neo4j | · | ★ | · | · | · |
| Memgraph | ☆ | ☆ | · | ★ | · |
| SPARQL 1.1 (RDF4J) | |||||
| RDF4J MemoryStore | ☆ | ☆ | ★ ⁵ | ☆ ⁵ | ☆ |
| RDF4J NativeStore | ☆ | ☆ | ★ ⁵ | ☆ ⁵ | ☆ ⁶ |
| RDF4J LmdbStore | ☆ | ☆ | ★ ⁵ | ☆ ⁵ | ☆ |
| GraphDB | ☆ | ★ | · | · | · |
| Amazon Neptune | · | ☆ ⁷ | · | ★ ⁷ | · |
¹ PostgreSQL accepts READ UNCOMMITTED syntactically but treats it as Read Committed.
² PostgreSQL's "Repeatable Read" uses Multi-Version Concurrency Control (MVCC) snapshots at transaction start, effectively providing snapshot isolation (no phantom reads). Stronger than the SQL standard definition of RR.
³ MySQL/InnoDB's "Repeatable Read" uses MVCC snapshots for consistent reads (SI-like), but locking reads (SELECT ... FOR UPDATE), UPDATE, and DELETE acquire gap locks and next-key locks, behaving as Read Committed for those
operations.
⁴ SQL Server's snapshot isolation requires explicit database-level configuration
(ALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON). Additionally, READ_COMMITTED_SNAPSHOT ON makes the default
Read Committed use row versioning (statement-level snapshot).
⁵ RDF4J defines SNAPSHOT_READ (consistent snapshot per query, closest to RR) and SNAPSHOT (consistent snapshot for
entire transaction, closest to SI) as separate levels. Stores declare supported levels via
getSupportedIsolationLevels(). Native stores default to SNAPSHOT_READ.
⁶ RDF4J NativeStore SERIALIZABLE had memory leak issues tracked in
eclipse-rdf4j#1031.
⁷ Amazon Neptune's isolation level is not configurable — it is determined automatically by query type. Read-only queries (SELECT, ASK, CONSTRUCT, DESCRIBE) run at snapshot isolation via MVCC. Mutation queries (INSERT, DELETE) run at an enhanced Read Committed with range locks that prevent phantoms and non-repeatable reads, effectively approaching Serialisable. Neptune does not implement the RDF4J transaction API; isolation semantics are baked into the engine.
ConcurrentModificationException with retry logic| Backend Family | Concurrency Model | Conflict Handling |
|---|---|---|
| SQL (PostgreSQL, MySQL) | MVCC, multiple writers | Serialisation failure → retry |
| SQL (SQLite) | WAL, single writer | Writer queue; readers never blocked |
| GQL (Neo4j) | Optimistic locking | Write-write conflict → abort |
| GQL (Memgraph) | MVCC | Serialisation failure → abort |
| RDF4J (native stores) | Versioning, single writer | Writer queue; readers never blocked |
| RDF4J (GraphDB) | Sequential writes, parallel reads | No conflict — writes are serialised |
| RDF4J (Neptune) | MVCC reads, pessimistic write locks | ConcurrentModificationException → retry |
Given the backend landscape, the framework does not impose a uniform isolation floor — each connector targets the strongest level its backend supports and documents the resulting guarantee. Most backends natively provide snapshot isolation or better; outliers like GraphDB cap at read-committed and connectors built on them inherit that ceiling.
Read visibility within a transaction depends on the connector's implementation strategy:
When the backend supports atomic transactions natively, connectors route mutations directly through the backend's transaction primitive and commit atomically.
When it does not, the Store interface requires connectors to emulate atomicity by buffering all mutation requests
in memory during the transaction and flushing them in order at commit time. This pattern:
Buffering shifts memory pressure to the client: callers are responsible for sizing transactions according to available
memory. For bulk data loading, this means splitting large batches of insert or remove calls across multiple
transactions.
Under the buffer-and-flush pattern, backend constraint checks (uniqueness, referential integrity) only fire at flush time. Errors from conflicting writes surface at commit, not at the point of the buffered call.
Backend connectors should: