See More

/*- * #%L * LmdbJava * %% * Copyright (C) 2016 - 2023 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. * #L% */ package org.lmdbjava; import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.lang.Long.BYTES; import static java.lang.Long.MIN_VALUE; import static java.nio.ByteBuffer.allocateDirect; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.DbiFlags.MDB_DUPFIXED; import static org.lmdbjava.DbiFlags.MDB_DUPSORT; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; import static org.lmdbjava.PutFlags.MDB_APPENDDUP; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; import static org.lmdbjava.SeekOp.MDB_FIRST; import static org.lmdbjava.SeekOp.MDB_GET_BOTH; import static org.lmdbjava.SeekOp.MDB_LAST; import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.function.Consumer; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.lmdbjava.Cursor.ClosedException; import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.ReadOnlyRequiredException; /** * Test {@link Cursor}. */ public final class CursorTest { @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Env env; @After public void after() { env.close(); } @Before public void before() throws IOException { try { final File path = tmp.newFile(); env = create(PROXY_OPTIMAL) .setMapSize(KIBIBYTES.toBytes(1_024)) .setMaxReaders(1) .setMaxDbs(1) .open(path, POSIX_MODE, MDB_NOSUBDIR); } catch (final IOException e) { throw new LmdbException("IO failure", e); } } @Test(expected = ClosedException.class) public void closedCursorRejectsSubsequentGets() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); try (Txn txn = env.txnWrite()) { final Cursor c = db.openCursor(txn); c.close(); c.seek(MDB_FIRST); } } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsSeekFirstCall() { doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsSeekLastCall() { doEnvClosedTest(null, c -> c.seek(MDB_LAST)); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsSeekNextCall() { doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsCloseCall() { doEnvClosedTest(null, Cursor::close); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsFirstCall() { doEnvClosedTest(null, Cursor::first); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsLastCall() { doEnvClosedTest(null, Cursor::last); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsPrevCall() { doEnvClosedTest( c -> { c.first(); assertThat(c.key().getInt(), is(1)); assertThat(c.val().getInt(), is(10)); c.next(); }, Cursor::prev); } @Test(expected = Env.AlreadyClosedException.class) public void closedEnvRejectsDeleteCall() { doEnvClosedTest( c -> { c.first(); assertThat(c.key().getInt(), is(1)); assertThat(c.val().getInt(), is(10)); }, Cursor::delete); } @Test public void count() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); assertThat(c.count(), is(1L)); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); assertThat(c.count(), is(3L)); c.put(bb(2), bb(1), MDB_APPENDDUP); c.put(bb(2), bb(2), MDB_APPENDDUP); assertThat(c.count(), is(2L)); } } @Test(expected = NotReadyException.class) public void cursorCannotCloseIfTransactionCommitted() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn);) { c.put(bb(1), bb(2), MDB_APPENDDUP); assertThat(c.count(), is(1L)); c.put(bb(1), bb(4), MDB_APPENDDUP); assertThat(c.count(), is(2L)); txn.commit(); } } } @Test public void cursorFirstLastNextPrev() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); c.put(bb(5), bb(6)); c.put(bb(7), bb(8)); assertThat(c.first(), is(true)); assertThat(c.key().getInt(0), is(1)); assertThat(c.val().getInt(0), is(2)); assertThat(c.last(), is(true)); assertThat(c.key().getInt(0), is(7)); assertThat(c.val().getInt(0), is(8)); assertThat(c.prev(), is(true)); assertThat(c.key().getInt(0), is(5)); assertThat(c.val().getInt(0), is(6)); assertThat(c.first(), is(true)); assertThat(c.next(), is(true)); assertThat(c.key().getInt(0), is(3)); assertThat(c.val().getInt(0), is(4)); } } @Test public void delete() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST), is(true)); assertThat(c.key().getInt(), is(1)); assertThat(c.val().getInt(), is(2)); c.delete(); assertThat(c.seek(MDB_FIRST), is(true)); assertThat(c.key().getInt(), is(3)); assertThat(c.val().getInt(), is(4)); c.delete(); assertThat(c.seek(MDB_FIRST), is(false)); } } @Test public void getKeyVal() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); c.put(bb(2), bb(1), MDB_APPENDDUP); c.put(bb(2), bb(2), MDB_APPENDDUP); c.put(bb(2), bb(3), MDB_APPENDDUP); c.put(bb(2), bb(4), MDB_APPENDDUP); assertThat(c.get(bb(1), bb(2), MDB_GET_BOTH), is(true)); assertThat(c.count(), is(3L)); assertThat(c.get(bb(1), bb(3), MDB_GET_BOTH), is(false)); assertThat(c.get(bb(2), bb(1), MDB_GET_BOTH), is(true)); assertThat(c.count(), is(4L)); assertThat(c.get(bb(2), bb(0), MDB_GET_BOTH), is(false)); } } @Test public void putMultiple() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, MDB_DUPFIXED); final int elemCount = 20; final ByteBuffer values = allocateDirect(Integer.BYTES * elemCount); for (int i = 1; i <= elemCount; i++) { values.putInt(i); } values.flip(); final int key = 100; final ByteBuffer k = bb(key); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.putMultiple(k, values, elemCount, MDB_MULTIPLE); assertThat(c.count(), is((long) elemCount)); } } @Test(expected = IllegalArgumentException.class) public void putMultipleWithoutMdbMultipleFlag() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.putMultiple(bb(100), bb(1), 1); } } @Test public void renewTxRo() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); final Cursor c; try (Txn txn = env.txnRead()) { c = db.openCursor(txn); txn.commit(); } try (Txn txn = env.txnRead()) { c.renew(txn); txn.commit(); } c.close(); } @Test(expected = ReadOnlyRequiredException.class) public void renewTxRw() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); try (Txn txn = env.txnWrite()) { assertThat(txn.isReadOnly(), is(false)); try (Cursor c = db.openCursor(txn)) { c.renew(txn); } } } @Test public void repeatedCloseCausesNotError() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite()) { final Cursor c = db.openCursor(txn); c.close(); c.close(); } } @Test public void reserve() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); final ByteBuffer key = bb(5); try (Txn txn = env.txnWrite()) { assertNull(db.get(txn, key)); try (Cursor c = db.openCursor(txn)) { final ByteBuffer val = c.reserve(key, BYTES * 2); assertNotNull(db.get(txn, key)); val.putLong(MIN_VALUE).flip(); } txn.commit(); } try (Txn txn = env.txnWrite()) { final ByteBuffer val = db.get(txn, key); assertThat(val.capacity(), is(BYTES * 2)); assertThat(val.getLong(), is(MIN_VALUE)); } } @Test public void returnValueForNoDupData() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA), is(true)); assertThat(c.put(bb(5), bb(7), MDB_NODUPDATA), is(true)); assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA), is(false)); } } @Test public void returnValueForNoOverwrite() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NOOVERWRITE), is(true)); // fails, but gets exist val assertThat(c.put(bb(5), bb(8), MDB_NOOVERWRITE), is(false)); assertThat(c.val().getInt(0), is(6)); } } @Test public void testCursorByteBufferDuplicate() { final Dbi db = env.openDbi(DB_1, MDB_CREATE); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2)); c.put(bb(3), bb(4)); } txn.commit(); } try (Txn txn = env.txnRead()) { try (Cursor c = db.openCursor(txn)) { c.first(); final ByteBuffer key1 = c.key().duplicate(); final ByteBuffer val1 = c.val().duplicate(); c.last(); final ByteBuffer key2 = c.key().duplicate(); final ByteBuffer val2 = c.val().duplicate(); assertThat(key1.getInt(0), is(1)); assertThat(val1.getInt(0), is(2)); assertThat(key2.getInt(0), is(3)); assertThat(val2.getInt(0), is(4)); } } } private void doEnvClosedTest(final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { final Dbi db = env.openDbi(DB_1, MDB_CREATE); db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); db.put(bb(2), bb(30)); db.put(bb(4), bb(40)); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn)) { if (workBeforeEnvClosed != null) { workBeforeEnvClosed.accept(c); } env.close(); if (workAfterEnvClose != null) { workAfterEnvClose.accept(c); } } } } }