Skip to content

Commit dcbf7df

Browse files
committed
feat(bridge): transactions
Implements `docs/PROTOCOL.md` §3.1 for the Nitrite adapter. A transaction is a second adapter over the same database rather than a mode on the first, so one connection's uncommitted documents can never reach another connection's reads. Nitrite's transaction lives above the storage engine — a `TransactionStore` buffers the writes and a journal replays them on commit — so this works identically on MVStore, RocksDB and in memory; nothing here re-implements either half. - `beginTransaction()` returns an `AdapterTransaction` whose `adapter()` is the transactional twin. `commit` and `rollback` both close the session, which is what makes the ending safe on either path. - `listStores` counts through `resolve` rather than off `db` directly: a total that left out the rows the person just staged is not read-your-own-writes. - `capabilities.transactions` follows `allowWrite` — that flag is the permission, this reports what the engine can undo — and the transactional twin reports it `false`, because Nitrite does not nest one. Not everything Nitrite does is transactional: creating or dropping an index, clearing a collection and dropping one are auto-committed. None of the protocol's writes is on that list, so nothing this bridge can be asked to do falls through — but a sixth method would have to check it.
1 parent 61c6805 commit dcbf7df

2 files changed

Lines changed: 391 additions & 10 deletions

File tree

‎nitrite-bridge/src/main/java/org/dizitart/no2/bridge/NitriteAdapter.java‎

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.dizitart.no2.bridge;
22

33
import org.dizitart.dbinspect.AdapterCapabilities;
4+
import org.dizitart.dbinspect.AdapterTransaction;
45
import org.dizitart.dbinspect.BridgeAdapter;
56
import org.dizitart.dbinspect.BridgeErrorKind;
67
import org.dizitart.dbinspect.BridgeException;
@@ -23,6 +24,8 @@
2324
import org.dizitart.no2.common.tuples.Pair;
2425
import org.dizitart.no2.filters.Filter;
2526
import org.dizitart.no2.repository.ObjectRepository;
27+
import org.dizitart.no2.transaction.Session;
28+
import org.dizitart.no2.transaction.Transaction;
2629

2730
import java.util.ArrayList;
2831
import java.util.Arrays;
@@ -75,13 +78,23 @@ public final class NitriteAdapter implements BridgeAdapter {
7578
private final boolean allowRegex;
7679
private final AdapterCapabilities capabilities;
7780

81+
/**
82+
* The open transaction this adapter is scoped to, or null on the one that is not.
83+
*
84+
* <p>See {@link #beginTransaction()}: a transaction is a second adapter over the same database
85+
* rather than a mode on this one, so that one connection's uncommitted documents can never
86+
* reach another connection's reads.
87+
*/
88+
private final Transaction transaction;
89+
7890
private NitriteAdapter(Builder builder) {
7991
this.db = builder.db;
8092
this.id = builder.id;
8193
this.displayName = builder.displayName;
8294
this.repositories = Collections.unmodifiableList(new ArrayList<>(builder.repositories));
8395
this.sampleSize = builder.sampleSize;
8496
this.allowRegex = builder.allowRegex;
97+
this.transaction = null;
8598

8699
List<String> filterOps = new ArrayList<>(FilterDsl.FILTER_OPS);
87100
if (builder.allowRegex) {
@@ -95,6 +108,11 @@ private NitriteAdapter(Builder builder) {
95108
// Off unless the embedding developer turned it on for this adapter
96109
// (threat model rule 5).
97110
.edit(builder.allowWrite)
111+
// Not an opt-in ({@code docs/PROTOCOL.md} §3.1): Nitrite's transaction is
112+
// implemented above the store, so it is available on every engine this
113+
// adapter can be pointed at — MVStore, RocksDB and in-memory alike.
114+
// `allowWrite` is the permission; this reports what the engine can undo.
115+
.transactions(builder.allowWrite)
98116
.snapshot(builder.allowSnapshot)
99117
// Not an opt-in: `queryPage` already showed the first 64 KB of this very
100118
// cell, and `docs/PROTOCOL.md` §2 has always promised the rest on request.
@@ -104,6 +122,34 @@ private NitriteAdapter(Builder builder) {
104122
.build();
105123
}
106124

125+
/**
126+
* The transactional twin of {@code parent}, resolving stores through {@code transaction}.
127+
*
128+
* <p>Every capability but {@code transactions} is carried over: a gate that changed inside a
129+
* transaction would be a second, invisible permission model. That one drops, because Nitrite
130+
* does not nest one.
131+
*/
132+
private NitriteAdapter(NitriteAdapter parent, Transaction transaction) {
133+
this.db = parent.db;
134+
this.id = parent.id;
135+
this.displayName = parent.displayName;
136+
this.repositories = parent.repositories;
137+
this.sampleSize = parent.sampleSize;
138+
this.allowRegex = parent.allowRegex;
139+
this.transaction = transaction;
140+
141+
AdapterCapabilities from = parent.capabilities;
142+
this.capabilities =
143+
AdapterCapabilities.builder(from.query())
144+
.watch(from.watch(), from.watchScope())
145+
.edit(from.edit())
146+
.transactions(false)
147+
.snapshot(from.snapshot())
148+
.blob(from.blob())
149+
.filterOps(from.filterOps())
150+
.build();
151+
}
152+
107153
public static Builder builder(Nitrite db, String id, String displayName) {
108154
return new Builder(db, id, displayName);
109155
}
@@ -139,20 +185,22 @@ public AdapterCapabilities capabilities() {
139185
return capabilities;
140186
}
141187

188+
/**
189+
* The stores, counted through {@link #resolve} rather than off {@code db} directly.
190+
*
191+
* <p>Which matters inside a transaction: a count taken from the primary collection would show
192+
* the person a total that does not include the rows they just staged, and §3.1's
193+
* read-your-own-writes covers {@code listStores} as much as it covers a page.
194+
*/
142195
@Override
143196
public List<StoreInfo> listStores() {
144197
List<StoreInfo> stores = new ArrayList<>();
145198
for (String name : db.listCollectionNames()) {
146-
stores.add(new StoreInfo(name, "collection", db.getCollection(name).size()));
199+
stores.add(new StoreInfo(name, "collection", resolve(name).size()));
147200
}
148201
for (ObjectRepository<?> repository : repositories) {
149-
NitriteCollection collection = repository.getDocumentCollection();
150-
stores.add(
151-
new StoreInfo(
152-
collection.getName(),
153-
"repository",
154-
collection.size(),
155-
keyOf(collection.getName())));
202+
String name = repository.getDocumentCollection().getName();
203+
stores.add(new StoreInfo(name, "repository", resolve(name).size(), keyOf(name)));
156204
}
157205
return stores;
158206
}
@@ -251,6 +299,68 @@ public QueryPage queryPage(PageRequest request) {
251299
* One row, addressed by {@code _id} — the identity {@code docs/PROTOCOL.md} §3 gives every
252300
* Nitrite implementation.
253301
*/
302+
/**
303+
* Opens one Nitrite transaction ({@code docs/PROTOCOL.md} §3.1).
304+
*
305+
* <p>Nitrite's transaction lives above the storage engine — a {@code TransactionStore} buffers
306+
* the writes and a journal replays them on commit — so this works identically on MVStore,
307+
* RocksDB and in memory. Nothing here re-implements either half; the session and the
308+
* transaction are the engine's own.
309+
*
310+
* <p><b>Not everything Nitrite does is transactional</b>, and the {@link Transaction}
311+
* javadoc is the list: creating or dropping an index, clearing a collection and dropping one
312+
* are auto-committed. The protocol's writes are insert, update and delete, none of which is on
313+
* that list, so nothing this bridge can be asked to do falls through — but an adapter that grows
314+
* a sixth method must check that list before it does.
315+
*/
316+
@Override
317+
public AdapterTransaction beginTransaction() {
318+
Session session = db.createSession();
319+
Transaction started;
320+
try {
321+
started = session.beginTransaction();
322+
} catch (RuntimeException failure) {
323+
session.close();
324+
throw new BridgeException(
325+
BridgeErrorKind.ADAPTER,
326+
"the database would not begin a transaction",
327+
failure.toString());
328+
}
329+
330+
NitriteAdapter scoped = new NitriteAdapter(this, started);
331+
return new AdapterTransaction() {
332+
@Override
333+
public BridgeAdapter adapter() {
334+
return scoped;
335+
}
336+
337+
@Override
338+
public void commit() {
339+
try {
340+
started.commit();
341+
} catch (RuntimeException failure) {
342+
throw new BridgeException(
343+
BridgeErrorKind.ADAPTER,
344+
"the database refused the commit",
345+
failure.toString());
346+
} finally {
347+
// Closing a session rolls back anything still open in it, so this is safe on
348+
// both paths and is the only thing that releases the transactional store.
349+
session.close();
350+
}
351+
}
352+
353+
@Override
354+
public void rollback() {
355+
try {
356+
started.rollback();
357+
} finally {
358+
session.close();
359+
}
360+
}
361+
};
362+
}
363+
254364
@Override
255365
public WriteResult write(WriteRequest request) {
256366
NitriteCollection collection = resolve(request.store());
@@ -382,11 +492,28 @@ private NitriteCollection resolve(String store) {
382492
for (ObjectRepository<?> repository : repositories) {
383493
NitriteCollection collection = repository.getDocumentCollection();
384494
if (collection.getName().equals(store)) {
385-
return collection;
495+
if (transaction == null) {
496+
return collection;
497+
}
498+
// By type, because that is the only handle Nitrite opens a repository by — and it
499+
// has to be the transaction's own, or a read would miss the writes staged beside
500+
// it and a write would land outside the transaction entirely.
501+
//
502+
// The keyed overload is not the same call with a null key: it looks the repository
503+
// up under `entityName+key` and refuses when that name does not exist, so an
504+
// unkeyed repository has to go through the one-argument form.
505+
String key = keyOf(collection.getName());
506+
ObjectRepository<?> scoped =
507+
key == null
508+
? transaction.getRepository(repository.getType())
509+
: transaction.getRepository(repository.getType(), key);
510+
return scoped.getDocumentCollection();
386511
}
387512
}
388513
if (db.listCollectionNames().contains(store)) {
389-
return db.getCollection(store);
514+
return transaction == null
515+
? db.getCollection(store)
516+
: transaction.getCollection(store);
390517
}
391518
throw new BridgeException(BridgeErrorKind.BAD_REQUEST, "unknown store");
392519
}

0 commit comments

Comments
 (0)