/* * Copyright © 2016-2025 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lmdbjava; import static java.nio.ByteBuffer.allocateDirect; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.KeyRange.closed; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.bb; import static org.lmdbjava.Txn.State.DONE; import static org.lmdbjava.Txn.State.READY; import static org.lmdbjava.Txn.State.RELEASED; import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.lmdbjava.Dbi.BadValueSizeException; import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Txn.EnvIsReadOnly; import org.lmdbjava.Txn.IncompatibleParent; import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.NotResetException; import org.lmdbjava.Txn.ReadOnlyRequiredException; import org.lmdbjava.Txn.ReadWriteRequiredException; import org.lmdbjava.Txn.ResetException; /** Test {@link Txn}. */ public final class TxnTest { private Path file; private Env env; private TempDir tempDir; @BeforeEach void beforeEach() { tempDir = new TempDir(); file = tempDir.createTempFile(); env = create() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) .setEnvFlags(MDB_NOSUBDIR) .open(file); } @AfterEach void afterEach() { env.close(); tempDir.cleanup(); } @Test void largeKeysRejected() { assertThatThrownBy( () -> { final Dbi dbi = env.createDbi() .setDbName(DB_1) .withDefaultComparator() .setDbiFlags(MDB_CREATE) .open(); final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); key.limit(key.capacity()); dbi.put(key, bb(2)); }) .isInstanceOf(BadValueSizeException.class); } @Test void rangeSearch() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = allocateDirect(env.getMaxKeySize()); key.put("cherry".getBytes(UTF_8)).flip(); db.put(key, bb(1)); key.clear(); key.put("strawberry".getBytes(UTF_8)).flip(); db.put(key, bb(3)); key.clear(); key.put("pineapple".getBytes(UTF_8)).flip(); db.put(key, bb(2)); try (Txn txn = env.txnRead()) { final ByteBuffer start = allocateDirect(env.getMaxKeySize()); start.put("a".getBytes(UTF_8)).flip(); final ByteBuffer end = allocateDirect(env.getMaxKeySize()); end.put("z".getBytes(UTF_8)).flip(); final List keysFound = new ArrayList<>(); try (CursorIterable ckr = db.iterate(txn, closed(start, end))) { for (final CursorIterable.KeyVal kv : ckr) { keysFound.add(UTF_8.decode(kv.key()).toString()); } } assertThat(keysFound.size()).isEqualTo(3); } } @Test void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { assertThat(roEnv.txnRead()).isNotNull(); } } @Test void readWriteTxnDeniedInReadOnlyEnv() { assertThatThrownBy( () -> { env.createDbi() .setDbName(DB_1) .withDefaultComparator() .setDbiFlags(MDB_CREATE) .open(); env.close(); try (Env roEnv = create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { roEnv.txnWrite(); // error } }) .isInstanceOf(EnvIsReadOnly.class); } @Test void testCheckNotCommitted() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { txn.commit(); txn.checkReady(); } }) .isInstanceOf(NotReadyException.class); } @Test void testCheckReadOnly() { assertThatThrownBy( () -> { try (Txn txn = env.txnWrite()) { txn.checkReadOnly(); } }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void testCheckWritesAllowed() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { txn.checkWritesAllowed(); } }) .isInstanceOf(ReadWriteRequiredException.class); } @Test void testGetId() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final AtomicLong txId1 = new AtomicLong(); final AtomicLong txId2 = new AtomicLong(); try (Txn tx1 = env.txnRead()) { txId1.set(tx1.getId()); } db.put(bb(1), bb(2)); try (Txn tx2 = env.txnRead()) { txId2.set(tx2.getId()); } // should not see the same snapshot assertThat(txId1.get()).isNotEqualTo(txId2.get()); } @Test void txCanCommitThenCloseWithoutError() { try (Txn txn = env.txnRead()) { assertThat(txn.getState()).isEqualTo(READY); txn.commit(); assertThat(txn.getState()).isEqualTo(DONE); } } @Test void txCannotAbortIfAlreadyCommitted() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { assertThat(txn.getState()).isEqualTo(READY); txn.commit(); assertThat(txn.getState()).isEqualTo(DONE); txn.abort(); } }) .isInstanceOf(NotReadyException.class); } @Test void txCannotCommitTwice() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { txn.commit(); txn.commit(); // error } }) .isInstanceOf(NotReadyException.class); } @Test void txConstructionDeniedIfEnvClosed() { assertThatThrownBy( () -> { env.close(); env.txnRead(); }) .isInstanceOf(AlreadyClosedException.class); } @Test void txRenewDeniedIfEnvClosed() { assertThatThrownBy( () -> { final Txn txnRead = env.txnRead(); txnRead.close(); env.close(); txnRead.renew(); }) .isInstanceOf(AlreadyClosedException.class); } @Test void txCloseDeniedIfEnvClosed() { assertThatThrownBy( () -> { final Txn txnRead = env.txnRead(); env.close(); txnRead.close(); }) .isInstanceOf(AlreadyClosedException.class); } @Test void txCommitDeniedIfEnvClosed() { assertThatThrownBy( () -> { final Txn txnRead = env.txnRead(); env.close(); txnRead.commit(); }) .isInstanceOf(AlreadyClosedException.class); } @Test void txAbortDeniedIfEnvClosed() { assertThatThrownBy( () -> { final Txn txnRead = env.txnRead(); env.close(); txnRead.abort(); }) .isInstanceOf(AlreadyClosedException.class); } @Test void txResetDeniedIfEnvClosed() { assertThatThrownBy( () -> { final Txn txnRead = env.txnRead(); env.close(); txnRead.reset(); }) .isInstanceOf(AlreadyClosedException.class); } @Test public void txParent() { try (Txn txRoot = env.txnWrite(); Txn txChild = env.txn(txRoot)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } } @Test public void txParent2() { try (Txn txRoot = env.txnWrite()) { assertThatThrownBy( () -> { env.txn(txRoot, (TxnFlagSet) null); }) .isInstanceOf(NullPointerException.class); } } @Test public void txParent3() { try (Txn txRoot = env.txnWrite(); Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } } @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( () -> { try (Txn txRoot = env.txnWrite(); Txn txChild = env.txn(txRoot)) { env.close(); assertThat(txChild.getParent()).isEqualTo(txRoot); } }) .isInstanceOf(AlreadyClosedException.class); } @Test void txParentROChildRWIncompatible() { assertThatThrownBy( () -> { try (Txn txRoot = env.txnRead()) { env.txn(txRoot); // error } }) .isInstanceOf(IncompatibleParent.class); } @Test void txParentRWChildROIncompatible() { assertThatThrownBy( () -> { try (Txn txRoot = env.txnWrite()) { env.txn(txRoot, MDB_RDONLY_TXN); // error } }) .isInstanceOf(IncompatibleParent.class); } @Test void txReadOnly() { try (Txn txn = env.txnRead()) { assertThat(txn.getParent()).isNull(); assertThat(txn.getState()).isEqualTo(READY); assertThat(txn.isReadOnly()).isTrue(); txn.checkReady(); txn.checkReadOnly(); txn.reset(); assertThat(txn.getState()).isEqualTo(RESET); txn.renew(); assertThat(txn.getState()).isEqualTo(READY); txn.commit(); assertThat(txn.getState()).isEqualTo(DONE); txn.close(); assertThat(txn.getState()).isEqualTo(RELEASED); } } @Test void txReadWrite() { final Txn txn = env.txnWrite(); assertThat(txn.getParent()).isNull(); assertThat(txn.getState()).isEqualTo(READY); assertThat(txn.isReadOnly()).isFalse(); txn.checkReady(); txn.checkWritesAllowed(); txn.commit(); assertThat(txn.getState()).isEqualTo(DONE); txn.close(); assertThat(txn.getState()).isEqualTo(RELEASED); } @Test void txRenewDeniedWithoutPriorReset() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { txn.renew(); } }) .isInstanceOf(NotResetException.class); } @Test void txResetDeniedForAlreadyResetTransaction() { assertThatThrownBy( () -> { try (Txn txn = env.txnRead()) { txn.reset(); txn.renew(); txn.reset(); txn.reset(); } }) .isInstanceOf(ResetException.class); } @Test void txResetDeniedForReadWriteTransaction() { assertThatThrownBy( () -> { try (Txn txn = env.txnWrite()) { txn.reset(); } }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void zeroByteKeysRejected() { assertThatThrownBy( () -> { final Dbi dbi = env.createDbi() .setDbName(DB_1) .withDefaultComparator() .setDbiFlags(MDB_CREATE) .open(); final ByteBuffer key = allocateDirect(4); key.putInt(1); assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped dbi.put(key, bb(2)); }) .isInstanceOf(BadValueSizeException.class); } }