11package org .dizitart .no2 .bridge ;
22
33import org .dizitart .dbinspect .AdapterCapabilities ;
4+ import org .dizitart .dbinspect .AdapterTransaction ;
45import org .dizitart .dbinspect .BridgeAdapter ;
56import org .dizitart .dbinspect .BridgeErrorKind ;
67import org .dizitart .dbinspect .BridgeException ;
2324import org .dizitart .no2 .common .tuples .Pair ;
2425import org .dizitart .no2 .filters .Filter ;
2526import org .dizitart .no2 .repository .ObjectRepository ;
27+ import org .dizitart .no2 .transaction .Session ;
28+ import org .dizitart .no2 .transaction .Transaction ;
2629
2730import java .util .ArrayList ;
2831import 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