clear() {
+ enumSet.clear();
+ return this;
+ }
+
+ /**
+ * Build the {@link DbiFlagSet}
+ *
+ * @return A
+ */
+ public S build() {
+ final int size = enumSet.size();
+ if (size == 0) {
+ return emptySetSupplier.get();
+ } else if (size == 1) {
+ return singletonSetConstructor.apply(enumSet.stream().findFirst().get());
+ } else {
+ return constructor.apply(enumSet);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/lmdbjava/BufferProxy.java b/src/main/java/org/lmdbjava/BufferProxy.java
index d4503731..a3c339bf 100644
--- a/src/main/java/org/lmdbjava/BufferProxy.java
+++ b/src/main/java/org/lmdbjava/BufferProxy.java
@@ -16,10 +16,6 @@
package org.lmdbjava;
import static java.lang.Long.BYTES;
-import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY;
-import static org.lmdbjava.DbiFlags.MDB_UNSIGNEDKEY;
-import static org.lmdbjava.MaskedFlag.isSet;
-import static org.lmdbjava.MaskedFlag.mask;
import java.util.Comparator;
import jnr.ffi.Pointer;
@@ -75,30 +71,22 @@ protected BufferProxy() {}
* The provided comparator must strictly match the lexicographical order of keys in the native
* LMDB database.
*
- * @param flags for the database
+ * @param dbiFlagSet The {@link DbiFlags} set for the database.
* @return a comparator that can be used (never null)
*/
- protected Comparator getComparator(DbiFlags... flags) {
- final int intFlag = mask(flags);
-
- return isSet(intFlag, MDB_INTEGERKEY) || isSet(intFlag, MDB_UNSIGNEDKEY)
- ? getUnsignedComparator()
- : getSignedComparator();
- }
+ public abstract Comparator getComparator(final DbiFlagSet dbiFlagSet);
/**
- * Get a suitable default {@link Comparator} to compare numeric key values as signed.
+ * Get a suitable default {@link Comparator}
*
- * @return a comparator that can be used (never null)
- */
- protected abstract Comparator getSignedComparator();
-
- /**
- * Get a suitable default {@link Comparator} to compare numeric key values as unsigned.
+ * The provided comparator must strictly match the lexicographical order of keys in the native
+ * LMDB database.
*
* @return a comparator that can be used (never null)
*/
- protected abstract Comparator getUnsignedComparator();
+ public Comparator getComparator() {
+ return getComparator(DbiFlagSet.empty());
+ }
/**
* Called when the MDB_val should be set to reflect the passed buffer. This buffer
@@ -138,4 +126,13 @@ protected Comparator getComparator(DbiFlags... flags) {
final KeyVal keyVal() {
return new KeyVal<>(this);
}
+
+ /**
+ * Create a new {@link Key} to hold pointers for this buffer proxy.
+ *
+ * @return a non-null key holder
+ */
+ final Key key() {
+ return new Key<>(this);
+ }
}
diff --git a/src/main/java/org/lmdbjava/ByteArrayProxy.java b/src/main/java/org/lmdbjava/ByteArrayProxy.java
index 853521e0..82b7721c 100644
--- a/src/main/java/org/lmdbjava/ByteArrayProxy.java
+++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java
@@ -36,9 +36,6 @@ public final class ByteArrayProxy extends BufferProxy {
private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager();
- private static final Comparator signedComparator = ByteArrayProxy::compareArraysSigned;
- private static final Comparator unsignedComparator = ByteArrayProxy::compareArrays;
-
private ByteArrayProxy() {}
/**
@@ -48,7 +45,7 @@ private ByteArrayProxy() {}
* @param o2 right operand (required)
* @return as specified by {@link Comparable} interface
*/
- public static int compareArrays(final byte[] o1, final byte[] o2) {
+ public static int compareLexicographically(final byte[] o1, final byte[] o2) {
requireNonNull(o1);
requireNonNull(o2);
if (o1 == o2) {
@@ -68,26 +65,6 @@ public static int compareArrays(final byte[] o1, final byte[] o2) {
return o1.length - o2.length;
}
- /**
- * Compare two byte arrays.
- *
- * @param b1 left operand (required)
- * @param b2 right operand (required)
- * @return as specified by {@link Comparable} interface
- */
- public static int compareArraysSigned(final byte[] b1, final byte[] b2) {
- requireNonNull(b1);
- requireNonNull(b2);
-
- if (b1 == b2) return 0;
-
- for (int i = 0; i < min(b1.length, b2.length); ++i) {
- if (b1[i] != b2[i]) return b1[i] - b2[i];
- }
-
- return b1.length - b2.length;
- }
-
@Override
protected byte[] allocate() {
return new byte[0];
@@ -104,13 +81,8 @@ protected byte[] getBytes(final byte[] buffer) {
}
@Override
- protected Comparator getSignedComparator() {
- return signedComparator;
- }
-
- @Override
- protected Comparator getUnsignedComparator() {
- return unsignedComparator;
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ return ByteArrayProxy::compareLexicographically;
}
@Override
diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java
index 2866e874..bcbb6ebf 100644
--- a/src/main/java/org/lmdbjava/ByteBufProxy.java
+++ b/src/main/java/org/lmdbjava/ByteBufProxy.java
@@ -23,6 +23,7 @@
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import java.lang.reflect.Field;
+import java.nio.ByteOrder;
import java.util.Comparator;
import jnr.ffi.Pointer;
@@ -44,13 +45,6 @@ public final class ByteBufProxy extends BufferProxy {
private static final String FIELD_NAME_ADDRESS = "memoryAddress";
private static final String FIELD_NAME_LENGTH = "length";
private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf";
- private static final Comparator comparator =
- (o1, o2) -> {
- requireNonNull(o1);
- requireNonNull(o2);
-
- return o1.compareTo(o2);
- };
private final long lengthOffset;
private final long addressOffset;
@@ -81,6 +75,71 @@ public ByteBufProxy(final PooledByteBufAllocator allocator) {
}
}
+ /**
+ * Lexicographically compare two buffers.
+ *
+ * @param o1 left operand (required)
+ * @param o2 right operand (required)
+ * @return as specified by {@link Comparable} interface
+ */
+ public static int compareLexicographically(final ByteBuf o1, final ByteBuf o2) {
+ requireNonNull(o1);
+ requireNonNull(o2);
+ return o1.compareTo(o2);
+ }
+
+ /**
+ * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when using
+ * MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically.
+ *
+ * @param o1 left operand (required)
+ * @param o2 right operand (required)
+ * @return as specified by {@link Comparable} interface
+ */
+ public static int compareAsIntegerKeys(final ByteBuf o1, final ByteBuf o2) {
+ requireNonNull(o1);
+ requireNonNull(o2);
+ // Both buffers should be same length according to LMDB API.
+ // From the LMDB docs for MDB_INTEGER_KEY
+ // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the
+ // same size.
+ final int len1 = o1.readableBytes();
+ final int len2 = o2.readableBytes();
+ if (len1 != len2) {
+ throw new RuntimeException(
+ "Length mismatch, len1: "
+ + len1
+ + ", len2: "
+ + len2
+ + ". Lengths must be identical and either 4 or 8 bytes.");
+ }
+ if (len1 == 8) {
+ final long lw;
+ final long rw;
+ if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
+ lw = o1.readLongLE();
+ rw = o2.readLongLE();
+ } else {
+ lw = o1.readLong();
+ rw = o2.readLong();
+ }
+ return Long.compareUnsigned(lw, rw);
+ } else if (len1 == 4) {
+ final int lw;
+ final int rw;
+ if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
+ lw = o1.readIntLE();
+ rw = o2.readIntLE();
+ } else {
+ lw = o1.readInt();
+ rw = o2.readInt();
+ }
+ return Integer.compareUnsigned(lw, rw);
+ } else {
+ return compareLexicographically(o1, o2);
+ }
+ }
+
static Field findField(final String c, final String name) {
Class> clazz;
try {
@@ -114,13 +173,12 @@ protected ByteBuf allocate() {
}
@Override
- protected Comparator getSignedComparator() {
- return comparator;
- }
-
- @Override
- protected Comparator getUnsignedComparator() {
- return comparator;
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) {
+ return ByteBufProxy::compareAsIntegerKeys;
+ } else {
+ return ByteBufProxy::compareLexicographically;
+ }
}
@Override
diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java
index cf75562c..b5dfca0b 100644
--- a/src/main/java/org/lmdbjava/ByteBufferProxy.java
+++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java
@@ -27,6 +27,7 @@
import java.lang.reflect.Field;
import java.nio.Buffer;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.Comparator;
import jnr.ffi.Pointer;
@@ -57,6 +58,8 @@ public final class ByteBufferProxy {
/** The safe, reflective {@link ByteBuffer} proxy for this system. Guaranteed to never be null. */
public static final BufferProxy PROXY_SAFE;
+ private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder();
+
static {
PROXY_SAFE = new ReflectiveProxy();
PROXY_OPTIMAL = getProxyOptimal();
@@ -92,16 +95,6 @@ abstract static class AbstractByteBufferProxy extends BufferProxy {
protected static final String FIELD_NAME_ADDRESS = "address";
protected static final String FIELD_NAME_CAPACITY = "capacity";
- private static final Comparator signedComparator =
- (o1, o2) -> {
- requireNonNull(o1);
- requireNonNull(o2);
-
- return o1.compareTo(o2);
- };
- private static final Comparator unsignedComparator =
- AbstractByteBufferProxy::compareBuff;
-
/**
* A thread-safe pool for a given length. If the buffer found is valid (ie not of a negative
* length) then that buffer is used. If no valid buffer is found, a new buffer is created.
@@ -116,7 +109,7 @@ abstract static class AbstractByteBufferProxy extends BufferProxy {
* @param o2 right operand (required)
* @return as specified by {@link Comparable} interface
*/
- public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) {
+ public static int compareLexicographically(final ByteBuffer o1, final ByteBuffer o2) {
requireNonNull(o1);
requireNonNull(o2);
@@ -146,6 +139,55 @@ public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) {
return o1.remaining() - o2.remaining();
}
+ /**
+ * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when
+ * using MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically.
+ *
+ * @param o1 left operand (required)
+ * @param o2 right operand (required)
+ * @return as specified by {@link Comparable} interface
+ */
+ public static int compareAsIntegerKeys(final ByteBuffer o1, final ByteBuffer o2) {
+ requireNonNull(o1);
+ requireNonNull(o2);
+ // Both buffers should be same length according to LMDB API.
+ // From the LMDB docs for MDB_INTEGER_KEY
+ // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of
+ // the same size.
+ final int len1 = o1.limit();
+ final int len2 = o2.limit();
+ if (len1 != len2) {
+ throw new RuntimeException(
+ "Length mismatch, len1: "
+ + len1
+ + ", len2: "
+ + len2
+ + ". Lengths must be identical and either 4 or 8 bytes.");
+ }
+ // Keys for MDB_INTEGER_KEY are written in native order so ensure we read them in that order
+ o1.order(NATIVE_ORDER);
+ o2.order(NATIVE_ORDER);
+ // TODO it might be worth the DbiBuilder having a method to capture fixedKeyLength() or -1
+ // for variable length keys. This can be passed to getComparator(..) so it can return a
+ // comparator that doesn't need to test the length every time. There may be other benefits
+ // to the Dbi knowing the key length if it is fixed.
+ if (len1 == 8) {
+ final long lw = o1.getLong(0);
+ final long rw = o2.getLong(0);
+ return Long.compareUnsigned(lw, rw);
+ } else if (len1 == 4) {
+ final int lw = o1.getInt(0);
+ final int rw = o2.getInt(0);
+ return Integer.compareUnsigned(lw, rw);
+ } else {
+ // size_t and int are likely to be 8bytes and 4bytes respectively on 64bit.
+ // If 32bit then would be 4/2 respectively.
+ // Short.compareUnsigned is not available in Java8.
+ // For now just fall back to our standard comparator
+ return compareLexicographically(o1, o2);
+ }
+ }
+
static Field findField(final Class> c, final String name) {
Class> clazz = c;
do {
@@ -180,13 +222,12 @@ protected final ByteBuffer allocate() {
}
@Override
- protected Comparator getSignedComparator() {
- return signedComparator;
- }
-
- @Override
- protected Comparator getUnsignedComparator() {
- return unsignedComparator;
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) {
+ return AbstractByteBufferProxy::compareAsIntegerKeys;
+ } else {
+ return AbstractByteBufferProxy::compareLexicographically;
+ }
}
@Override
diff --git a/src/main/java/org/lmdbjava/ByteUnit.java b/src/main/java/org/lmdbjava/ByteUnit.java
new file mode 100644
index 00000000..1c37ad24
--- /dev/null
+++ b/src/main/java/org/lmdbjava/ByteUnit.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/** Simple {@link Enum} for converting various IEC and SI byte units down to a number of bytes. */
+public enum ByteUnit {
+
+ /** IEC/SI byte unit for bytes. */
+ BYTES(1L),
+
+ /** IEC byte unit for 1024 bytes. */
+ KIBIBYTES(1_024L),
+ /** IEC byte unit for 1024^2 bytes. */
+ MEBIBYTES(1_048_576L),
+ /** IEC byte unit for 1024^3 bytes. */
+ GIBIBYTES(1_073_741_824L),
+ /** IEC byte unit for 1024^4 bytes. */
+ TEBIBYTES(1_099_511_627_776L),
+ /** IEC byte unit for 1024^5 bytes. */
+ PEBIBYTES(1_125_899_906_842_624L),
+
+ /** SI byte unit for 1000 bytes. */
+ KILOBYTES(1_000L),
+ /** SI byte unit for 1000^2 bytes. */
+ MEGABYTES(1_000_000L),
+ /** SI byte unit for 1000^3 bytes. */
+ GIGABYTES(1_000_000_000L),
+ /** SI byte unit for 1000^4 bytes. */
+ TERABYTES(1_000_000_000_000L),
+ /** SI byte unit for 1000^5 bytes. */
+ PETABYTES(1_000_000_000_000_000L),
+ ;
+
+ private final long factor;
+
+ ByteUnit(long factor) {
+ this.factor = factor;
+ }
+
+ /**
+ * Convert the value in this byte unit into bytes.
+ *
+ * @param value The value to convert.
+ * @return The number of bytes.
+ */
+ public long toBytes(final long value) {
+ return value * factor;
+ }
+
+ /**
+ * Gets factor to apply when converting this unit into bytes.
+ *
+ * @return The factor to apply when converting this unit into bytes.
+ */
+ public long getFactor() {
+ return factor;
+ }
+}
diff --git a/src/main/java/org/lmdbjava/CopyFlagSet.java b/src/main/java/org/lmdbjava/CopyFlagSet.java
new file mode 100644
index 00000000..c8e35477
--- /dev/null
+++ b/src/main/java/org/lmdbjava/CopyFlagSet.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.nio.file.Path;
+import java.util.Collection;
+import java.util.Objects;
+
+/** An immutable set of flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */
+public interface CopyFlagSet extends FlagSet {
+
+ /** An immutable empty {@link CopyFlagSet}. */
+ CopyFlagSet EMPTY = CopyFlagSetImpl.EMPTY;
+
+ /**
+ * Gets the immutable empty {@link CopyFlagSet} instance.
+ *
+ * @return The immutable empty {@link CopyFlagSet} instance.
+ */
+ static CopyFlagSet empty() {
+ return CopyFlagSetImpl.EMPTY;
+ }
+
+ /**
+ * Creates an immutable {@link CopyFlagSet} containing copyFlag.
+ *
+ * @param copyFlag The flag to include in the {@link CopyFlagSet}
+ * @return An immutable {@link CopyFlagSet} containing just copyFlag.
+ */
+ static CopyFlagSet of(final CopyFlags copyFlag) {
+ Objects.requireNonNull(copyFlag);
+ return copyFlag;
+ }
+
+ /**
+ * Creates an immutable {@link CopyFlagSet} containing copyFlags.
+ *
+ * @param copyFlags The flags to include in the {@link CopyFlagSet}.
+ * @return An immutable {@link CopyFlagSet} containing copyFlags.
+ */
+ static CopyFlagSet of(final CopyFlags... copyFlags) {
+ return builder().setFlags(copyFlags).build();
+ }
+
+ /**
+ * Creates an immutable {@link CopyFlagSet} containing copyFlags.
+ *
+ * @param copyFlags The flags to include in the {@link CopyFlagSet}.
+ * @return An immutable {@link CopyFlagSet} containing copyFlags.
+ */
+ static CopyFlagSet of(final Collection copyFlags) {
+ return builder().setFlags(copyFlags).build();
+ }
+
+ /**
+ * Create a builder for building an {@link CopyFlagSet}.
+ *
+ * @return A builder instance for building an {@link CopyFlagSet}.
+ */
+ static AbstractFlagSet.Builder builder() {
+ return new AbstractFlagSet.Builder<>(
+ CopyFlags.class, CopyFlagSetImpl::new, copyFlag -> copyFlag, () -> CopyFlagSetImpl.EMPTY);
+ }
+}
diff --git a/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java
new file mode 100644
index 00000000..f18af382
--- /dev/null
+++ b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+class CopyFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet
+ implements CopyFlagSet {}
diff --git a/src/main/java/org/lmdbjava/CopyFlagSetImpl.java b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java
new file mode 100644
index 00000000..a566fc2a
--- /dev/null
+++ b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java
@@ -0,0 +1,27 @@
+/*
+ * 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 java.util.EnumSet;
+
+class CopyFlagSetImpl extends AbstractFlagSet implements CopyFlagSet {
+
+ static final CopyFlagSet EMPTY = new CopyFlagSetEmpty();
+
+ CopyFlagSetImpl(final EnumSet flags) {
+ super(flags);
+ }
+}
diff --git a/src/main/java/org/lmdbjava/CopyFlags.java b/src/main/java/org/lmdbjava/CopyFlags.java
index 4365563c..e3677baf 100644
--- a/src/main/java/org/lmdbjava/CopyFlags.java
+++ b/src/main/java/org/lmdbjava/CopyFlags.java
@@ -15,8 +15,12 @@
*/
package org.lmdbjava;
-/** Flags for use when performing a {@link Env#copy(java.io.File, org.lmdbjava.CopyFlags...)}. */
-public enum CopyFlags implements MaskedFlag {
+import java.nio.file.Path;
+import java.util.EnumSet;
+import java.util.Set;
+
+/** Flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */
+public enum CopyFlags implements MaskedFlag, CopyFlagSet {
/** Compacting copy: Omit free space from copy, and renumber all pages sequentially. */
MDB_CP_COMPACT(0x01);
@@ -31,4 +35,29 @@ public enum CopyFlags implements MaskedFlag {
public int getMask() {
return mask;
}
+
+ @Override
+ public Set getFlags() {
+ return EnumSet.of(this);
+ }
+
+ @Override
+ public boolean isSet(final CopyFlags flag) {
+ return this == flag;
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return FlagSet.asString(this);
+ }
}
diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java
index f7fcbc41..0e320930 100644
--- a/src/main/java/org/lmdbjava/Cursor.java
+++ b/src/main/java/org/lmdbjava/Cursor.java
@@ -20,8 +20,6 @@
import static org.lmdbjava.Dbi.KeyNotFoundException.MDB_NOTFOUND;
import static org.lmdbjava.Env.SHOULD_CHECK;
import static org.lmdbjava.Library.LIB;
-import static org.lmdbjava.MaskedFlag.isSet;
-import static org.lmdbjava.MaskedFlag.mask;
import static org.lmdbjava.PutFlags.MDB_MULTIPLE;
import static org.lmdbjava.PutFlags.MDB_NODUPDATA;
import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE;
@@ -98,22 +96,41 @@ public long count() {
return longByReference.longValue();
}
+ /**
+ * @deprecated Instead use {@link Cursor#delete(PutFlagSet)}.
Delete current key/data pair.
+ * This function deletes the key/data pair to which the cursor refers.
+ * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA}
+ */
+ @Deprecated
+ public void delete(final PutFlags... flags) {
+ delete(PutFlagSet.of(flags));
+ }
+
+ /**
+ * Delete current key/data pair.
+ *
+ *
This function deletes the key/data pair to which the cursor refers.
+ */
+ public void delete() {
+ delete(PutFlagSet.EMPTY);
+ }
+
/**
* Delete current key/data pair.
*
*
This function deletes the key/data pair to which the cursor refers.
*
- * @param f flags (either null or {@link PutFlags#MDB_NODUPDATA}
+ * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA}
*/
- public void delete(final PutFlags... f) {
+ public void delete(final PutFlagSet flags) {
if (SHOULD_CHECK) {
env.checkNotClosed();
checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
}
- final int flags = mask(true, f);
- checkRc(LIB.mdb_cursor_del(ptrCursor, flags));
+ final PutFlagSet putFlagSet = flags != null ? flags : PutFlagSet.EMPTY;
+ checkRc(LIB.mdb_cursor_del(ptrCursor, putFlagSet.getMask()));
}
/**
@@ -203,6 +220,10 @@ public T key() {
return kv.key();
}
+ KeyVal keyVal() {
+ return kv;
+ }
+
/**
* Position at last key/data item.
*
@@ -230,6 +251,20 @@ public boolean prev() {
return seek(MDB_PREV);
}
+ /**
+ * @deprecated Use {@link Cursor#put(Object, Object, PutFlagSet)} instead.
Store by cursor.
+ * This function stores key/data pairs into the database.
+ * @param key key to store
+ * @param val data to store
+ * @param flags options for this operation
+ * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
+ * key/value existed already.
+ */
+ @Deprecated
+ public boolean put(final T key, final T val, final PutFlags... flags) {
+ return put(key, val, PutFlagSet.of(flags));
+ }
+
/**
* Store by cursor.
*
@@ -237,14 +272,29 @@ public boolean prev() {
*
* @param key key to store
* @param val data to store
- * @param op options for this operation
* @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
* key/value existed already.
*/
- public boolean put(final T key, final T val, final PutFlags... op) {
+ public boolean put(final T key, final T val) {
+ return put(key, val, PutFlagSet.EMPTY);
+ }
+
+ /**
+ * Store by cursor.
+ *
+ *
This function stores key/data pairs into the database.
+ *
+ * @param key key to store
+ * @param val data to store
+ * @param flags options for this operation
+ * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
+ * key/value existed already.
+ */
+ public boolean put(final T key, final T val, final PutFlagSet flags) {
if (SHOULD_CHECK) {
requireNonNull(key);
requireNonNull(val);
+ requireNonNull(flags);
env.checkNotClosed();
checkNotClosed();
txn.checkReady();
@@ -252,12 +302,11 @@ public boolean put(final T key, final T val, final PutFlags... op) {
}
final Pointer transientKey = kv.keyIn(key);
final Pointer transientVal = kv.valIn(val);
- final int mask = mask(true, op);
- final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), mask);
+ final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags.getMask());
if (rc == MDB_KEYEXIST) {
- if (isSet(mask, MDB_NOOVERWRITE)) {
+ if (flags.isSet(MDB_NOOVERWRITE)) {
kv.valOut(); // marked as in,out in LMDB C docs
- } else if (!isSet(mask, MDB_NODUPDATA)) {
+ } else if (!flags.isSet(MDB_NODUPDATA)) {
checkRc(rc);
}
return false;
@@ -270,6 +319,39 @@ public boolean put(final T key, final T val, final PutFlags... op) {
return true;
}
+ /**
+ * @deprecated Use {@link Cursor#put(Object, Object, PutFlagSet)} instead.
Put multiple
+ * values into the database in one MDB_MULTIPLE operation.
+ * The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The buffer must
+ * contain fixed-sized values to be inserted. The size of each element is calculated from the
+ * buffer's size divided by the given element count. For example, to populate 10 X 4 byte
+ * integers at once, present a buffer of 40 bytes and specify the element as 10.
+ * @param key key to store in the database (not null)
+ * @param val value to store in the database (not null)
+ * @param elements number of elements contained in the passed value buffer
+ * @param flags options for operation (must set MDB_MULTIPLE)
+ */
+ @Deprecated
+ public void putMultiple(final T key, final T val, final int elements, final PutFlags... flags) {
+ putMultiple(key, val, elements, PutFlagSet.of(flags));
+ }
+
+ /**
+ * Put multiple values into the database in one MDB_MULTIPLE operation.
+ *
+ *
The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The buffer must
+ * contain fixed-sized values to be inserted. The size of each element is calculated from the
+ * buffer's size divided by the given element count. For example, to populate 10 X 4 byte integers
+ * at once, present a buffer of 40 bytes and specify the element as 10.
+ *
+ * @param key key to store in the database (not null)
+ * @param val value to store in the database (not null)
+ * @param elements number of elements contained in the passed value buffer
+ */
+ public void putMultiple(final T key, final T val, final int elements) {
+ putMultiple(key, val, elements, PutFlagSet.EMPTY);
+ }
+
/**
* Put multiple values into the database in one MDB_MULTIPLE operation.
*
@@ -281,9 +363,10 @@ public boolean put(final T key, final T val, final PutFlags... op) {
* @param key key to store in the database (not null)
* @param val value to store in the database (not null)
* @param elements number of elements contained in the passed value buffer
- * @param op options for operation (must set MDB_MULTIPLE)
+ * @param flags options for operation (must set MDB_MULTIPLE) Either a {@link
+ * PutFlagSet} or a single {@link PutFlags}.
*/
- public void putMultiple(final T key, final T val, final int elements, final PutFlags... op) {
+ public void putMultiple(final T key, final T val, final int elements, final PutFlagSet flags) {
if (SHOULD_CHECK) {
requireNonNull(txn);
requireNonNull(key);
@@ -291,14 +374,14 @@ public void putMultiple(final T key, final T val, final int elements, final PutF
env.checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
+ if (!flags.isSet(MDB_MULTIPLE)) {
+ throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag");
+ }
}
- final int mask = mask(true, op);
- if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) {
- throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag");
- }
+
final Pointer transientKey = txn.kv().keyIn(key);
final Pointer dataPtr = txn.kv().valInMulti(val, elements);
- final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, mask);
+ final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, flags.getMask());
checkRc(rc);
ReferenceUtil.reachabilityFence0(transientKey);
ReferenceUtil.reachabilityFence0(dataPtr);
@@ -329,23 +412,59 @@ public void renew(final Txn newTxn) {
this.txn = newTxn;
}
+ /**
+ * @deprecated Use {@link Cursor#reserve(Object, int, PutFlagSet)} instead.
Reserve space for
+ * data of the given size, but don't copy the given val. Instead, return a pointer to the
+ * reserved space, which the caller can fill in later - before the next update operation or
+ * the transaction ends. This saves an extra memcpy if the data is being generated later. LMDB
+ * does nothing else with this memory, the caller is expected to modify all of the space
+ * requested.
+ * This flag must not be specified if the database was opened with MDB_DUPSORT
+ * @param key key to store in the database (not null)
+ * @param size size of the value to be stored in the database (not null)
+ * @param flags options for this operation
+ * @return a buffer that can be used to modify the value
+ */
+ @Deprecated
+ public T reserve(final T key, final int size, final PutFlags... flags) {
+ return reserve(key, size, PutFlagSet.of(flags));
+ }
+
+ /**
+ * Reserve space for data of the given size, but don't copy the given val. Instead, return a
+ * pointer to the reserved space, which the caller can fill in later - before the next update
+ * operation or the transaction ends. This saves an extra {@code memcpy} if the data is being
+ * generated later. LMDB does nothing else with this memory, the caller is expected to modify all
+ * the space requested.
+ *
+ *
This flag must not be specified if the database was opened with MDB_DUPSORT
+ *
+ * @param key key to store in the database (not null)
+ * @param size size of the value to be stored in the database (not null)
+ * @return a buffer that can be used to modify the value
+ */
+ public T reserve(final T key, final int size) {
+ return reserve(key, size, PutFlagSet.EMPTY);
+ }
+
/**
* Reserve space for data of the given size, but don't copy the given val. Instead, return a
* pointer to the reserved space, which the caller can fill in later - before the next update
* operation or the transaction ends. This saves an extra memcpy if the data is being generated
- * later. LMDB does nothing else with this memory, the caller is expected to modify all of the
- * space requested.
+ * later. LMDB does nothing else with this memory, the caller is expected to modify all the space
+ * requested.
*
*
This flag must not be specified if the database was opened with MDB_DUPSORT
*
* @param key key to store in the database (not null)
* @param size size of the value to be stored in the database (not null)
- * @param op options for this operation
+ * @param flags options for this operation
* @return a buffer that can be used to modify the value
*/
- public T reserve(final T key, final int size, final PutFlags... op) {
+ public T reserve(final T key, final int size, final PutFlagSet flags) {
if (SHOULD_CHECK) {
requireNonNull(key);
+ requireNonNull(flags);
env.checkNotClosed();
checkNotClosed();
txn.checkReady();
@@ -353,8 +472,9 @@ public T reserve(final T key, final int size, final PutFlags... op) {
}
final Pointer transientKey = kv.keyIn(key);
final Pointer transientVal = kv.valIn(size);
- final int flags = mask(true, op) | MDB_RESERVE.getMask();
- checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags));
+ // This is inconsistent with putMultiple which require MDB_MULTIPLE to be in the set.
+ final int flagsMask = flags.getMaskWith(MDB_RESERVE);
+ checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flagsMask));
kv.valOut();
ReferenceUtil.reachabilityFence0(transientKey);
ReferenceUtil.reachabilityFence0(transientVal);
diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java
index 6a03bd90..65fc1023 100644
--- a/src/main/java/org/lmdbjava/CursorIterable.java
+++ b/src/main/java/org/lmdbjava/CursorIterable.java
@@ -21,10 +21,14 @@
import static org.lmdbjava.CursorIterable.State.REQUIRES_NEXT_OP;
import static org.lmdbjava.CursorIterable.State.TERMINATED;
import static org.lmdbjava.GetOp.MDB_SET_RANGE;
+import static org.lmdbjava.Library.LIB;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.Supplier;
+import jnr.ffi.Pointer;
import org.lmdbjava.KeyRangeType.CursorOp;
import org.lmdbjava.KeyRangeType.IteratorOp;
@@ -38,7 +42,7 @@
*/
public final class CursorIterable implements Iterable>, AutoCloseable {
- private final Comparator comparator;
+ private final RangeComparator rangeComparator;
private final Cursor cursor;
private final KeyVal entry;
private boolean iteratorReturned;
@@ -46,16 +50,32 @@ public final class CursorIterable implements Iterable txn, final Dbi dbi, final KeyRange range, final Comparator comparator) {
+ final Txn txn,
+ final Dbi dbi,
+ final KeyRange range,
+ final Comparator comparator,
+ final BufferProxy proxy) {
this.cursor = dbi.openCursor(txn);
this.range = range;
- this.comparator = comparator;
this.entry = new KeyVal<>();
+
+ if (comparator != null) {
+ // User supplied Java-side comparator so use that
+ this.rangeComparator = new JavaRangeComparator<>(range, comparator, cursor::key);
+ } else {
+ // No Java-side comparator, so call down to LMDB to do the comparison
+ this.rangeComparator = new LmdbRangeComparator<>(txn, dbi, cursor, range, proxy);
+ }
}
@Override
public void close() {
cursor.close();
+ try {
+ rangeComparator.close();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
/**
@@ -95,13 +115,13 @@ public KeyVal next() {
@Override
public void remove() {
- cursor.delete();
+ cursor.delete(PutFlags.EMPTY);
}
};
}
private void executeCursorOp(final CursorOp op) {
- final boolean found;
+ boolean found;
switch (op) {
case FIRST:
found = cursor.first();
@@ -119,7 +139,31 @@ private void executeCursorOp(final CursorOp op) {
found = cursor.get(range.getStart(), MDB_SET_RANGE);
break;
case GET_START_KEY_BACKWARD:
- found = cursor.get(range.getStart(), MDB_SET_RANGE) || cursor.last();
+ found = cursor.get(range.getStart(), MDB_SET_RANGE);
+ if (found) {
+ if (!range.getType().isDirectionForward()
+ && range.getType().isStartKeyRequired()
+ && range.getType().isStartKeyInclusive()) {
+ // We need to ensure we move to the last matching key if using DUPSORT, see issue 267
+ boolean loop = true;
+ while (loop) {
+ if (rangeComparator.compareToStartKey() <= 0) {
+ found = cursor.next();
+ if (!found) {
+ // We got to the end so move last.
+ found = cursor.last();
+ loop = false;
+ }
+ } else {
+ // We have moved past so go back one.
+ found = cursor.prev();
+ loop = false;
+ }
+ }
+ }
+ } else {
+ found = cursor.last();
+ }
break;
default:
throw new IllegalStateException("Unknown cursor operation");
@@ -129,8 +173,7 @@ private void executeCursorOp(final CursorOp op) {
}
private void executeIteratorOp() {
- final IteratorOp op =
- range.getType().iteratorOp(range.getStart(), range.getStop(), entry.key(), comparator);
+ final IteratorOp op = range.getType().iteratorOp(entry.key(), rangeComparator);
switch (op) {
case CALL_NEXT_OP:
executeCursorOp(range.getType().nextOp());
@@ -219,4 +262,100 @@ enum State {
RELEASED,
TERMINATED
}
+
+ static class JavaRangeComparator implements RangeComparator {
+
+ private final Comparator comparator;
+ private final Supplier currentKeySupplier;
+ private final T start;
+ private final T stop;
+
+ JavaRangeComparator(
+ final KeyRange range,
+ final Comparator comparator,
+ final Supplier currentKeySupplier) {
+ this.comparator = comparator;
+ this.currentKeySupplier = currentKeySupplier;
+ this.start = range.getStart();
+ this.stop = range.getStop();
+ }
+
+ @Override
+ public int compareToStartKey() {
+ return comparator.compare(currentKeySupplier.get(), start);
+ }
+
+ @Override
+ public int compareToStopKey() {
+ return comparator.compare(currentKeySupplier.get(), stop);
+ }
+
+ @Override
+ public void close() throws Exception {
+ // Nothing to close
+ }
+ }
+
+ /**
+ * Calls down to mdb_cmp to make use of the comparator that LMDB uses for insertion order. Has a
+ * very slight overhead as compared to {@link JavaRangeComparator}.
+ */
+ private static class LmdbRangeComparator implements RangeComparator {
+
+ private final Pointer txnPointer;
+ private final Pointer dbiPointer;
+ private final Pointer cursorKeyPointer;
+ private final Key startKey;
+ private final Key stopKey;
+ private final Pointer startKeyPointer;
+ private final Pointer stopKeyPointer;
+
+ public LmdbRangeComparator(
+ final Txn txn,
+ final Dbi dbi,
+ final Cursor cursor,
+ final KeyRange range,
+ final BufferProxy proxy) {
+ txnPointer = Objects.requireNonNull(txn).pointer();
+ dbiPointer = Objects.requireNonNull(dbi).pointer();
+ cursorKeyPointer = Objects.requireNonNull(cursor).keyVal().pointerKey();
+ // Allocate buffers for use with the start/stop keys if required.
+ // Saves us copying bytes on each comparison
+ Objects.requireNonNull(range);
+ startKey = createKey(range.getStart(), proxy);
+ stopKey = createKey(range.getStop(), proxy);
+ startKeyPointer = startKey != null ? startKey.pointer() : null;
+ stopKeyPointer = stopKey != null ? stopKey.pointer() : null;
+ }
+
+ @Override
+ public int compareToStartKey() {
+ return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, startKeyPointer);
+ }
+
+ @Override
+ public int compareToStopKey() {
+ return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, stopKeyPointer);
+ }
+
+ @Override
+ public void close() {
+ if (startKey != null) {
+ startKey.close();
+ }
+ if (stopKey != null) {
+ stopKey.close();
+ }
+ }
+
+ private Key createKey(final T keyBuffer, final BufferProxy proxy) {
+ if (keyBuffer != null) {
+ final Key key = proxy.key();
+ key.keyIn(keyBuffer);
+ return key;
+ } else {
+ return null;
+ }
+ }
+ }
}
diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java
index 5449c172..d2afdf8f 100644
--- a/src/main/java/org/lmdbjava/Dbi.java
+++ b/src/main/java/org/lmdbjava/Dbi.java
@@ -31,6 +31,7 @@
import static org.lmdbjava.PutFlags.MDB_RESERVE;
import static org.lmdbjava.ResultCodeMapper.checkRc;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -48,12 +49,26 @@
*/
public final class Dbi {
- private final ComparatorCallback ccb;
+ @SuppressWarnings("FieldCanBeLocal") // Needs to be instance variable for FFI
+ private final ComparatorCallback callbackComparator;
+
private boolean cleaned;
+ // Used for CursorIterable KeyRange testing and/or native callbacks
private final Comparator comparator;
private final Env env;
private final byte[] name;
private final Pointer ptr;
+ private final BufferProxy proxy;
+ private final DbiFlagSet dbiFlagSet;
+
+ Dbi(
+ final Env env,
+ final Txn txn,
+ final byte[] name,
+ final BufferProxy proxy,
+ final DbiFlagSet dbiFlagSet) {
+ this(env, txn, name, null, false, proxy, dbiFlagSet);
+ }
Dbi(
final Env env,
@@ -62,38 +77,50 @@ public final class Dbi {
final Comparator comparator,
final boolean nativeCb,
final BufferProxy proxy,
- final DbiFlags... flags) {
+ final DbiFlagSet dbiFlagSet) {
+
if (SHOULD_CHECK) {
+ if (nativeCb && comparator == null) {
+ throw new IllegalArgumentException("Is nativeCb is true, you must supply a comparator");
+ }
+ requireNonNull(env);
requireNonNull(txn);
+ requireNonNull(proxy);
+ requireNonNull(dbiFlagSet);
txn.checkReady();
}
this.env = env;
this.name = name == null ? null : Arrays.copyOf(name, name.length);
- if (comparator == null) {
- this.comparator = proxy.getComparator(flags);
- } else {
- this.comparator = comparator;
- }
- final int flagsMask = mask(true, flags);
+ this.proxy = proxy;
+ this.comparator = comparator;
+ this.dbiFlagSet = dbiFlagSet;
final Pointer dbiPtr = allocateDirect(RUNTIME, ADDRESS);
- checkRc(LIB.mdb_dbi_open(txn.pointer(), name, flagsMask, dbiPtr));
+ checkRc(LIB.mdb_dbi_open(txn.pointer(), name, this.dbiFlagSet.getMask(), dbiPtr));
ptr = dbiPtr.getPointer(0);
if (nativeCb) {
- this.ccb =
- (keyA, keyB) -> {
- final T compKeyA = proxy.out(proxy.allocate(), keyA);
- final T compKeyB = proxy.out(proxy.allocate(), keyB);
- final int result = this.comparator.compare(compKeyA, compKeyB);
- proxy.deallocate(compKeyA);
- proxy.deallocate(compKeyB);
- return result;
- };
- LIB.mdb_set_compare(txn.pointer(), ptr, ccb);
+ // LMDB will call back to this comparator for insertion/iteration order
+ this.callbackComparator = createCallbackComparator(proxy);
+ LIB.mdb_set_compare(txn.pointer(), ptr, callbackComparator);
} else {
- ccb = null;
+ callbackComparator = null;
}
}
+ private ComparatorCallback createCallbackComparator(final BufferProxy proxy) {
+ return (keyA, keyB) -> {
+ final T compKeyA = proxy.out(proxy.allocate(), keyA);
+ final T compKeyB = proxy.out(proxy.allocate(), keyB);
+ final int result = this.comparator.compare(compKeyA, compKeyB);
+ proxy.deallocate(compKeyA);
+ proxy.deallocate(compKeyB);
+ return result;
+ };
+ }
+
+ Pointer pointer() {
+ return ptr;
+ }
+
/**
* Close the database handle (normally unnecessary; use with caution).
*
@@ -248,12 +275,57 @@ public T get(final Txn txn, final T key) {
/**
* Obtains the name of this database.
*
- * @return the name (may be null)
+ * @return The name (it maybe null)
*/
public byte[] getName() {
return name == null ? null : Arrays.copyOf(name, name.length);
}
+ /**
+ * Convert the passed name into bytes using the default {@link Charset}.
+ *
+ * @param name The name to convert.
+ * @return The name as a byte[] or null if name is null.
+ */
+ public static byte[] getNameBytes(final String name) {
+ return name == null ? null : name.getBytes(Env.DEFAULT_NAME_CHARSET);
+ }
+
+ /**
+ * Obtains the name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}.
+ *
+ * @return The name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}.
+ */
+ public String getNameAsString() {
+ return getNameAsString(Env.DEFAULT_NAME_CHARSET);
+ }
+
+ /**
+ * Obtains the name of this database, using the supplied {@link Charset}.
+ *
+ * @param charset The {@link Charset} to use when converting the DB from a byte[] to a {@link
+ * String}.
+ * @return The name of the database. If this is the unnamed database an empty string will be
+ * returned.
+ * @throws RuntimeException if the name can't be decoded.
+ */
+ public String getNameAsString(final Charset charset) {
+ return getNameAsString(this.name, charset);
+ }
+
+ static String getNameAsString(final byte[] name, final Charset charset) {
+ if (name == null) {
+ return "";
+ } else {
+ // Assume a UTF8 encoding as we don't know, thus swallow if it fails
+ try {
+ return new String(name, requireNonNull(charset));
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to decode database name using charset " + charset);
+ }
+ }
+ }
+
/**
* Iterate the database from the first item and forwards.
*
@@ -278,7 +350,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) {
env.checkNotClosed();
txn.checkReady();
}
- return new CursorIterable<>(txn, this, range, comparator);
+ return new CursorIterable<>(txn, this, range, comparator, proxy);
}
/**
@@ -288,6 +360,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) {
* @return the list of flags this Dbi was created with
*/
public List listFlags(final Txn txn) {
+ // TODO we could just return what is in dbiFlagSet, rather than hitting LMDB.
if (SHOULD_CHECK) {
env.checkNotClosed();
}
@@ -337,15 +410,48 @@ public Cursor openCursor(final Txn txn) {
*
* @param key key to store in the database (not null)
* @param val value to store in the database (not null)
- * @see #put(org.lmdbjava.Txn, java.lang.Object, java.lang.Object, org.lmdbjava.PutFlags...)
+ * @see #put(Txn, Object, Object, PutFlagSet)
*/
public void put(final T key, final T val) {
try (Txn txn = env.txnWrite()) {
- put(txn, key, val);
+ put(txn, key, val, PutFlagSet.EMPTY);
txn.commit();
}
}
+ /**
+ * @param txn transaction handle (not null; not committed; must be R-W)
+ * @param key key to store in the database (not null)
+ * @param val value to store in the database (not null)
+ * @param flags Special options for this operation
+ * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
+ * key/value existed already.
+ * @deprecated Use {@link Dbi#put(Txn, Object, Object, PutFlagSet)} instead, with a statically
+ * held {@link PutFlagSet}.
+ * Store a key/value pair in the database.
+ *
This function stores key/data pairs in the database. The default behavior is to enter
+ * the new key/data pair, replacing any previously existing key if duplicates are disallowed,
+ * or adding a duplicate data item if duplicates are allowed ({@link DbiFlags#MDB_DUPSORT}).
+ */
+ @Deprecated
+ public boolean put(final Txn txn, final T key, final T val, final PutFlags... flags) {
+ return put(txn, key, val, PutFlagSet.of(flags));
+ }
+
+ /**
+ * Store a key/value pair in the database.
+ *
+ * @param txn transaction handle (not null; not committed; must be R-W)
+ * @param key key to store in the database (not null)
+ * @param val value to store in the database (not null)
+ * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
+ * key/value existed already.
+ * @see #put(Txn, Object, Object, PutFlagSet)
+ */
+ public boolean put(final Txn txn, final T key, final T val) {
+ return put(txn, key, val, PutFlagSet.EMPTY);
+ }
+
/**
* Store a key/value pair in the database.
*
@@ -356,28 +462,29 @@ public void put(final T key, final T val) {
* @param txn transaction handle (not null; not committed; must be R-W)
* @param key key to store in the database (not null)
* @param val value to store in the database (not null)
- * @param flags Special options for this operation
+ * @param flags Special options for this operation.
* @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the
* key/value existed already.
*/
- public boolean put(final Txn txn, final T key, final T val, final PutFlags... flags) {
+ public boolean put(final Txn txn, final T key, final T val, final PutFlagSet flags) {
if (SHOULD_CHECK) {
requireNonNull(txn);
requireNonNull(key);
requireNonNull(val);
+ requireNonNull(flags);
env.checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
}
final Pointer transientKey = txn.kv().keyIn(key);
final Pointer transientVal = txn.kv().valIn(val);
- final int mask = mask(true, flags);
final int rc =
- LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), mask);
+ LIB.mdb_put(
+ txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags.getMask());
if (rc == MDB_KEYEXIST) {
- if (isSet(mask, MDB_NOOVERWRITE)) {
+ if (flags.isSet(MDB_NOOVERWRITE)) {
txn.kv().valOut(); // marked as in,out in LMDB C docs
- } else if (!isSet(mask, MDB_NODUPDATA)) {
+ } else if (!flags.isSet(MDB_NODUPDATA)) {
checkRc(rc);
}
return false;
@@ -415,7 +522,7 @@ public T reserve(final Txn txn, final T key, final int size, final PutFlags..
}
final Pointer transientKey = txn.kv().keyIn(key);
final Pointer transientVal = txn.kv().valIn(size);
- final int flags = mask(true, op) | MDB_RESERVE.getMask();
+ final int flags = mask(op) | MDB_RESERVE.getMask();
checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags));
txn.kv().valOut(); // marked as in,out in LMDB C docs
ReferenceUtil.reachabilityFence0(transientKey);
@@ -454,6 +561,17 @@ private void clean() {
cleaned = true;
}
+ @Override
+ public String toString() {
+ String name;
+ try {
+ name = getNameAsString();
+ } catch (Exception e) {
+ name = "?";
+ }
+ return "Dbi{" + "name='" + name + "', dbiFlagSet=" + dbiFlagSet + '}';
+ }
+
/** The specified DBI was changed unexpectedly. */
public static final class BadDbiException extends LmdbNativeException {
diff --git a/src/main/java/org/lmdbjava/DbiBuilder.java b/src/main/java/org/lmdbjava/DbiBuilder.java
new file mode 100644
index 00000000..29bbb8f5
--- /dev/null
+++ b/src/main/java/org/lmdbjava/DbiBuilder.java
@@ -0,0 +1,424 @@
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * Staged builder for building a {@link Dbi}
+ *
+ * @param buffer type
+ */
+public final class DbiBuilder {
+
+ private final Env env;
+ private final BufferProxy proxy;
+ private final boolean readOnly;
+ private byte[] name;
+
+ DbiBuilder(final Env env, final BufferProxy proxy, final boolean readOnly) {
+ this.env = Objects.requireNonNull(env);
+ this.proxy = Objects.requireNonNull(proxy);
+ this.readOnly = readOnly;
+ }
+
+ /**
+ * Create the {@link Dbi} with the passed name.
+ *
+ * The name will be converted into bytes using {@link StandardCharsets#UTF_8}.
+ *
+ * @param name The name of the database or null for the unnamed database (see also {@link
+ * DbiBuilder#withoutDbName()})
+ * @return The next builder stage.
+ */
+ public Stage2 setDbName(final String name) {
+ // Null name is allowed so no null check
+ final byte[] nameBytes = name == null ? null : name.getBytes(Env.DEFAULT_NAME_CHARSET);
+ return setDbName(nameBytes);
+ }
+
+ /**
+ * Create the {@link Dbi} with the passed name in byte[] form.
+ *
+ * @param name The name of the database in byte form.
+ * @return The next builder stage.
+ */
+ public Stage2 setDbName(final byte[] name) {
+ // Null name is allowed so no null check
+ this.name = name;
+ return new Stage2<>(this);
+ }
+
+ /**
+ * Create the {@link Dbi} without a name.
+ *
+ * Equivalent to passing null to {@link DbiBuilder#setDbName(String)} or {@link
+ * DbiBuilder#setDbName(byte[])}.
+ *
+ *
Note: The 'unnamed database' is used by LMDB to store the names of named databases, with the
+ * database name being the key. Use of the unnamed database is intended for simple applications
+ * with only one database.
+ *
+ * @return The next builder stage.
+ */
+ public Stage2 withoutDbName() {
+ return setDbName((byte[]) null);
+ }
+
+ /**
+ * Intermediate builder stage for constructing a {@link Dbi}.
+ *
+ * @param buffer type
+ */
+ public static final class Stage2 {
+
+ private final DbiBuilder dbiBuilder;
+
+ private ComparatorFactory comparatorFactory;
+ private ComparatorType comparatorType;
+
+ private Stage2(final DbiBuilder dbiBuilder) {
+ this.dbiBuilder = dbiBuilder;
+ }
+
+ /**
+ * This is the default choice when it comes to choosing a comparator. If you
+ * are not sure of the implications of the other methods then use this one as it is likely what
+ * you want and also probably the most performant.
+ *
+ * With this option, {@link CursorIterable} will make use of the LmdbJava's default Java-side
+ * comparators when comparing iteration keys to the start/stop keys. LMDB will use its own
+ * comparator for controlling insertion order in the database. The two comparators are
+ * functionally identical.
+ *
+ *
This option may be slightly more performant than when using {@link
+ * Stage2#withNativeComparator()} which calls down to LMDB for ALL comparison operations.
+ *
+ *
If you do not intend to use {@link CursorIterable} then it doesn't matter whether you
+ * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or
+ * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be
+ * used.
+ *
+ * @return The next builder stage.
+ */
+ public Stage3 withDefaultComparator() {
+ this.comparatorType = ComparatorType.DEFAULT;
+ return new Stage3<>(this);
+ }
+
+ /**
+ * With this option, {@link CursorIterable} will call down to LMDB's {@code mdb_cmp} method when
+ * comparing iteration keys to start/stop keys. This ensures LmdbJava is comparing start/stop
+ * keys using the same comparator that is used for insertion order into the db.
+ *
+ * This option may be slightly less performant than when using {@link
+ * Stage2#withDefaultComparator()} as it needs to call down to LMDB to perform the comparisons,
+ * however it guarantees that {@link CursorIterable} key comparison matches LMDB key comparison.
+ *
+ *
If you do not intend to use {@link CursorIterable} then it doesn't matter whether you
+ * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or
+ * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be
+ * used.
+ *
+ * @return The next builder stage.
+ */
+ public Stage3 withNativeComparator() {
+ this.comparatorType = ComparatorType.NATIVE;
+ return new Stage3<>(this);
+ }
+
+ /**
+ * Provide a java-side {@link Comparator} that LMDB will call back to for all
+ * comparison operations. Therefore, it will be called by LMDB to manage database
+ * insertion/iteration order. It will also be used for {@link CursorIterable} start/stop key
+ * comparisons.
+ *
+ * It can be useful if you need to sort your database using some other method, e.g. signed
+ * keys or case-insensitive order. Note, if you need keys stored in reverse order, see {@link
+ * DbiFlags#MDB_REVERSEKEY} and {@link DbiFlags#MDB_REVERSEDUP}.
+ *
+ *
As this requires LMDB to call back to java, this will be less performant than using LMDB's
+ * default comparators, but allows for total control over the order in which entries are stored
+ * in the database.
+ *
+ * @param comparatorFactory A factory to create a comparator. {@link
+ * ComparatorFactory#create(DbiFlagSet)} will be called once during the initialisation of
+ * the {@link Dbi}. It must not return null.
+ * @return The next builder stage.
+ */
+ public Stage3 withCallbackComparator(final ComparatorFactory comparatorFactory) {
+ this.comparatorFactory = Objects.requireNonNull(comparatorFactory);
+ this.comparatorType = ComparatorType.CALLBACK;
+ return new Stage3<>(this);
+ }
+
+ /**
+ * WARNING: Only use this if you fully understand the risks and implications.
+ *
+ *
+ * With this option, {@link CursorIterable} will make use of the passed comparator for
+ * comparing iteration keys to start/stop keys. It has NO bearing on the
+ * insert/iteration order of the database (which is controlled by LMDB's own comparators).
+ *
+ *
It is vital that this comparator is functionally identical to the one
+ * used internally in LMDB for insertion/iteration order, else you will see unexpected behaviour
+ * when using {@link CursorIterable}.
+ *
+ *
If you do not intend to use {@link CursorIterable} then it doesn't matter whether you
+ * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or
+ * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be
+ * used.
+ *
+ * @param comparatorFactory The comparator to use with {@link CursorIterable}. {@link
+ * ComparatorFactory#create(DbiFlagSet)} will be called once during the initialisation of
+ * the {@link Dbi}. It must not return null.
+ * @return The next builder stage.
+ */
+ public Stage3 withIteratorComparator(final ComparatorFactory comparatorFactory) {
+ this.comparatorFactory = Objects.requireNonNull(comparatorFactory);
+ this.comparatorType = ComparatorType.ITERATOR;
+ return new Stage3<>(this);
+ }
+ }
+
+ /**
+ * Final stage builder for constructing a {@link Dbi}.
+ *
+ * @param buffer type
+ */
+ public static final class Stage3 {
+
+ private final Stage2 stage2;
+ private final AbstractFlagSet.Builder flagSetBuilder =
+ DbiFlagSet.builder();
+ private Txn txn = null;
+
+ private Stage3(Stage2 stage2) {
+ this.stage2 = stage2;
+ }
+
+ /**
+ * Apply all the dbi flags supplied in dbiFlags.
+ *
+ * Clears all flags currently set by previous calls to {@link
+ * Stage3#setDbiFlags(Collection)}, {@link Stage3#setDbiFlags(DbiFlags...)} or {@link
+ * Stage3#addDbiFlag(DbiFlags)}.
+ *
+ * @param dbiFlags to open the database with. A null {@link Collection} will just clear all set
+ * flags. Null items are ignored.
+ * @return This builder instance.
+ */
+ public Stage3 setDbiFlags(final Collection dbiFlags) {
+ flagSetBuilder.clear();
+ if (dbiFlags != null) {
+ dbiFlags.stream().filter(Objects::nonNull).forEach(this.flagSetBuilder::addFlag);
+ }
+ return this;
+ }
+
+ /**
+ * Apply all the dbi flags supplied in dbiFlags.
+ *
+ * Clears all flags currently set by previous calls to {@link
+ * Stage3#setDbiFlags(Collection)}, {@link Stage3#setDbiFlags(DbiFlags...)} or {@link
+ * Stage3#addDbiFlag(DbiFlags)}.
+ *
+ * @param dbiFlags to open the database with. A null array will just clear all set flags. Null
+ * items are ignored.
+ * @return This builder instance.
+ */
+ public Stage3 setDbiFlags(final DbiFlags... dbiFlags) {
+ flagSetBuilder.clear();
+ if (dbiFlags != null) {
+ Arrays.stream(dbiFlags).filter(Objects::nonNull).forEach(this.flagSetBuilder::addFlag);
+ }
+ return this;
+ }
+
+ /**
+ * Apply all the dbi flags supplied in dbiFlags.
+ *
+ * Clears all flags currently set by previous calls to {@link
+ * Stage3#setDbiFlags(Collection)}, {@link Stage3#setDbiFlags(DbiFlags...)} or {@link
+ * Stage3#addDbiFlag(DbiFlags)}.
+ *
+ * @param dbiFlagSet to open the database with. A null value will just clear all set flags.
+ * @return This builder instance.
+ */
+ public Stage3 setDbiFlags(final DbiFlagSet dbiFlagSet) {
+ flagSetBuilder.clear();
+ if (dbiFlagSet != null) {
+ this.flagSetBuilder.setFlags(dbiFlagSet.getFlags());
+ }
+ return this;
+ }
+
+ /**
+ * Adds a dbiFlag to those flags already added to this builder by {@link
+ * Stage3#setDbiFlags(DbiFlags...)}, {@link Stage3#setDbiFlags(Collection)} or {@link
+ * Stage3#addDbiFlag(DbiFlags)}.
+ *
+ * @param dbiFlag to add to any existing flags. A null value is a no-op.
+ * @return this builder instance.
+ */
+ public Stage3 addDbiFlag(final DbiFlags dbiFlag) {
+ this.flagSetBuilder.addFlag(dbiFlag);
+ return this;
+ }
+
+ /**
+ * Adds a dbiFlag to those flags already added to this builder by {@link
+ * Stage3#setDbiFlags(DbiFlags...)}, {@link Stage3#setDbiFlags(Collection)} or {@link
+ * Stage3#addDbiFlag(DbiFlags)}.
+ *
+ * @param dbiFlagSet to add to any existing flags. A null value is a no-op.
+ * @return this builder instance.
+ */
+ public Stage3 addDbiFlags(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet != null) {
+ this.flagSetBuilder.addFlags(dbiFlagSet.getFlags());
+ }
+ return this;
+ }
+
+ /**
+ * Use the supplied transaction to open the {@link Dbi}.
+ *
+ * The caller MUST commit the transaction after calling {@link Stage3#open()}, in order to
+ * retain the Dbi in the Env. The caller is also responsible for
+ * closing the transaction.
+ *
+ *
If you don't call this method to supply a {@link Txn}, a {@link Txn} will be opened for
+ * the purpose of creating and opening the {@link Dbi}, then closed. Therefore, if you already
+ * have a transaction open, you should supply that to avoid one blocking the other.
+ *
+ * @param txn transaction to use (required; not closed). If the {@link Env} was opened with the
+ * {@link EnvFlags#MDB_RDONLY_ENV} flag, the {@link Txn} can be read-only, else it needs to
+ * be a read/write {@link Txn}.
+ * @return this builder instance.
+ */
+ public Stage3 setTxn(final Txn txn) {
+ this.txn = Objects.requireNonNull(txn);
+ return this;
+ }
+
+ /**
+ * Construct and open the {@link Dbi}.
+ *
+ * If a {@link Txn} was supplied to the builder, it is the callers responsibility to commit
+ * and close the txn upon return from this method, else the created DB won't be retained.
+ *
+ * @return A newly constructed and opened {@link Dbi}.
+ */
+ public Dbi open() {
+ final DbiBuilder dbiBuilder = stage2.dbiBuilder;
+ if (txn != null) {
+ return openDbi(txn, dbiBuilder);
+ } else {
+ try (final Txn localTxn = getTxn(dbiBuilder)) {
+ final Dbi dbi = openDbi(localTxn, dbiBuilder);
+ // even RO Txns require a commit to retain Dbi in Env
+ localTxn.commit();
+ return dbi;
+ }
+ }
+ }
+
+ private Txn getTxn(final DbiBuilder dbiBuilder) {
+ return dbiBuilder.readOnly ? dbiBuilder.env.txnRead() : dbiBuilder.env.txnWrite();
+ }
+
+ private Comparator getComparator(
+ final DbiBuilder dbiBuilder,
+ final ComparatorType comparatorType,
+ final DbiFlagSet dbiFlagSet) {
+ Comparator comparator = null;
+ switch (comparatorType) {
+ case DEFAULT:
+ // Get the appropriate default CursorIterable comparator based on the DbiFlags,
+ // e.g. MDB_INTEGERKEY may benefit from an optimised comparator.
+ comparator = dbiBuilder.proxy.getComparator(dbiFlagSet);
+ break;
+ case CALLBACK:
+ case ITERATOR:
+ comparator = stage2.comparatorFactory.create(dbiFlagSet);
+ Objects.requireNonNull(comparator, "comparatorFactory returned null");
+ break;
+ case NATIVE:
+ break;
+ default:
+ throw new IllegalStateException("Unexpected comparatorType " + comparatorType);
+ }
+ return comparator;
+ }
+
+ private Dbi openDbi(final Txn txn, final DbiBuilder dbiBuilder) {
+ final DbiFlagSet dbiFlagSet = flagSetBuilder.build();
+ final ComparatorType comparatorType = stage2.comparatorType;
+ final Comparator comparator = getComparator(dbiBuilder, comparatorType, dbiFlagSet);
+ final boolean useNativeCallback = comparatorType == ComparatorType.CALLBACK;
+ return new Dbi<>(
+ dbiBuilder.env,
+ txn,
+ dbiBuilder.name,
+ comparator,
+ useNativeCallback,
+ dbiBuilder.proxy,
+ dbiFlagSet);
+ }
+ }
+
+ private enum ComparatorType {
+ /**
+ * Default Java comparator for {@link CursorIterable} KeyRange testing, LMDB comparator for
+ * insertion/iteration order.
+ */
+ DEFAULT,
+ /** Use LMDB native comparator for everything. */
+ NATIVE,
+ /** Use the supplied custom Java-side comparator for everything. */
+ CALLBACK,
+ /**
+ * Use the supplied custom Java-side comparator for {@link CursorIterable} KeyRange testing,
+ * LMDB comparator for insertion/iteration order.
+ */
+ ITERATOR,
+ ;
+ }
+
+ /**
+ * A factory for creating a {@link Comparator} from a {@link DbiFlagSet}
+ *
+ * @param The type of buffer that will be compared by the created {@link Comparator}.
+ */
+ @FunctionalInterface
+ public interface ComparatorFactory {
+
+ /**
+ * Creates a comparator for the supplied {@link DbiFlagSet}. This will only be called once
+ * during the initialisation of the {@link Dbi}.
+ *
+ * @param dbiFlagSet The flags set on the DB that the returned {@link Comparator} will be used
+ * by. The flags in the set may impact how the returned {@link Comparator} should behave.
+ * @return A {@link Comparator} applicable to the passed DB flags.
+ */
+ Comparator create(final DbiFlagSet dbiFlagSet);
+ }
+}
diff --git a/src/main/java/org/lmdbjava/DbiFlagSet.java b/src/main/java/org/lmdbjava/DbiFlagSet.java
new file mode 100644
index 00000000..82043fe3
--- /dev/null
+++ b/src/main/java/org/lmdbjava/DbiFlagSet.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.util.Collection;
+import java.util.Objects;
+
+/** An immutable set of flags for use when opening a {@link Dbi}. */
+public interface DbiFlagSet extends FlagSet {
+
+ /** An immutable empty {@link DbiFlagSet}. */
+ DbiFlagSet EMPTY = DbiFlagSetImpl.EMPTY;
+
+ /** The set of {@link DbiFlags} that indicate unsigned integer keys are being used. */
+ DbiFlagSet INTEGER_KEY_FLAGS = DbiFlagSet.of(DbiFlags.MDB_INTEGERKEY, DbiFlags.MDB_INTEGERDUP);
+
+ /**
+ * Gets the immutable empty {@link DbiFlagSet} instance.
+ *
+ * @return The immutable empty {@link DbiFlagSet} instance.
+ */
+ static DbiFlagSet empty() {
+ return DbiFlagSetImpl.EMPTY;
+ }
+
+ /**
+ * Creates an immutable {@link DbiFlagSet} containing dbiFlag.
+ *
+ * @param dbiFlag The flag to include in the {@link DbiFlagSet}
+ * @return An immutable {@link DbiFlagSet} containing just dbiFlag.
+ */
+ static DbiFlagSet of(final DbiFlags dbiFlag) {
+ Objects.requireNonNull(dbiFlag);
+ return dbiFlag;
+ }
+
+ /**
+ * Creates an immutable {@link DbiFlagSet} containing dbiFlags.
+ *
+ * @param dbiFlags The flags to include in the {@link DbiFlagSet}.
+ * @return An immutable {@link DbiFlagSet} containing dbiFlags.
+ */
+ static DbiFlagSet of(final DbiFlags... dbiFlags) {
+ return builder().setFlags(dbiFlags).build();
+ }
+
+ /**
+ * Creates an immutable {@link DbiFlagSet} containing dbiFlags.
+ *
+ * @param dbiFlags The flags to include in the {@link DbiFlagSet}.
+ * @return An immutable {@link DbiFlagSet} containing dbiFlags.
+ */
+ static DbiFlagSet of(final Collection dbiFlags) {
+ return builder().setFlags(dbiFlags).build();
+ }
+
+ /**
+ * Create a builder for building an {@link DbiFlagSet}.
+ *
+ * @return A builder instance for building an {@link DbiFlagSet}.
+ */
+ static AbstractFlagSet.Builder builder() {
+ return new AbstractFlagSet.Builder<>(
+ DbiFlags.class, DbiFlagSetImpl::new, dbiFlag -> dbiFlag, () -> DbiFlagSetImpl.EMPTY);
+ }
+}
diff --git a/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java b/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java
new file mode 100644
index 00000000..cff91caf
--- /dev/null
+++ b/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+class DbiFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet
+ implements DbiFlagSet {}
diff --git a/src/main/java/org/lmdbjava/DbiFlagSetImpl.java b/src/main/java/org/lmdbjava/DbiFlagSetImpl.java
new file mode 100644
index 00000000..a43e887f
--- /dev/null
+++ b/src/main/java/org/lmdbjava/DbiFlagSetImpl.java
@@ -0,0 +1,27 @@
+/*
+ * 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 java.util.EnumSet;
+
+class DbiFlagSetImpl extends AbstractFlagSet implements DbiFlagSet {
+
+ static final DbiFlagSet EMPTY = new DbiFlagSetEmpty();
+
+ DbiFlagSetImpl(final EnumSet flags) {
+ super(flags);
+ }
+}
diff --git a/src/main/java/org/lmdbjava/DbiFlags.java b/src/main/java/org/lmdbjava/DbiFlags.java
index 123ec9fd..9426db0f 100644
--- a/src/main/java/org/lmdbjava/DbiFlags.java
+++ b/src/main/java/org/lmdbjava/DbiFlags.java
@@ -15,8 +15,11 @@
*/
package org.lmdbjava;
+import java.util.EnumSet;
+import java.util.Set;
+
/** Flags for use when opening a {@link Dbi}. */
-public enum DbiFlags implements MaskedFlag {
+public enum DbiFlags implements MaskedFlag, DbiFlagSet {
/**
* Use reverse string keys.
@@ -29,13 +32,24 @@ public enum DbiFlags implements MaskedFlag {
* Use sorted duplicates.
*
* Duplicate keys may be used in the database. Or, from another perspective, keys may have
- * multiple data items, stored in sorted order. By default keys must be unique and may have only a
- * single data item.
+ * multiple data items, stored in sorted order. By default, keys must be unique and may have only
+ * a single data item.
*/
MDB_DUPSORT(0x04),
/**
- * Numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the
- * same size.
+ * Numeric keys in native byte order: either unsigned int or size_t. The keys must all be
+ * of the same size.
+ *
+ *
This is an optimisation that is available when your keys are 4 or 8 byte unsigned numeric
+ * values. There are performance benefits for both ordered and un-ordered puts as compared to not
+ * using this flag.
+ *
+ *
When writing the key to the buffer you must write it in native order and subsequently read
+ * any keys retrieved from LMDB (via cursor or get method) also using native order.
+ *
+ *
For more information, see Numeric Keys in the
+ * LmdbJava wiki.
*/
MDB_INTEGERKEY(0x08),
/**
@@ -55,14 +69,6 @@ public enum DbiFlags implements MaskedFlag {
* #MDB_INTEGERKEY} keys.
*/
MDB_INTEGERDUP(0x20),
- /**
- * Compare the numeric keys in native byte order and as unsigned.
- *
- *
This option is applied only to {@link java.nio.ByteBuffer}, {@link org.agrona.DirectBuffer}
- * and byte array keys. {@link io.netty.buffer.ByteBuf} keys are always compared in native byte
- * order and as unsigned.
- */
- MDB_UNSIGNEDKEY(0x30, false),
/**
* With {@link #MDB_DUPSORT}, use reverse string dups.
*
@@ -78,15 +84,9 @@ public enum DbiFlags implements MaskedFlag {
MDB_CREATE(0x4_0000);
private final int mask;
- private final boolean propagatedToLmdb;
-
- DbiFlags(final int mask, final boolean propagatedToLmdb) {
- this.mask = mask;
- this.propagatedToLmdb = propagatedToLmdb;
- }
DbiFlags(final int mask) {
- this(mask, true);
+ this.mask = mask;
}
@Override
@@ -95,7 +95,27 @@ public int getMask() {
}
@Override
- public boolean isPropagatedToLmdb() {
- return propagatedToLmdb;
+ public Set getFlags() {
+ return EnumSet.of(this);
+ }
+
+ @Override
+ public boolean isSet(final DbiFlags flag) {
+ return this == flag;
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return FlagSet.asString(this);
}
}
diff --git a/src/main/java/org/lmdbjava/DirectBufferProxy.java b/src/main/java/org/lmdbjava/DirectBufferProxy.java
index 524b81b8..af918943 100644
--- a/src/main/java/org/lmdbjava/DirectBufferProxy.java
+++ b/src/main/java/org/lmdbjava/DirectBufferProxy.java
@@ -22,6 +22,7 @@
import static org.lmdbjava.UnsafeAccess.UNSAFE;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.Comparator;
import jnr.ffi.Pointer;
@@ -35,14 +36,6 @@
* This class requires {@link UnsafeAccess} and Agrona must be in the classpath.
*/
public final class DirectBufferProxy extends BufferProxy {
- private static final Comparator signedComparator =
- (o1, o2) -> {
- requireNonNull(o1);
- requireNonNull(o2);
-
- return o1.compareTo(o2);
- };
- private static final Comparator unsignedComparator = DirectBufferProxy::compareBuff;
/**
* The {@link MutableDirectBuffer} proxy. Guaranteed to never be null, although a class
@@ -58,6 +51,8 @@ public final class DirectBufferProxy extends BufferProxy {
private static final ThreadLocal> BUFFERS =
withInitial(() -> new ArrayDeque<>(16));
+ private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder();
+
private DirectBufferProxy() {}
/**
@@ -67,7 +62,7 @@ private DirectBufferProxy() {}
* @param o2 right operand (required)
* @return as specified by {@link Comparable} interface
*/
- public static int compareBuff(final DirectBuffer o1, final DirectBuffer o2) {
+ public static int compareLexicographically(final DirectBuffer o1, final DirectBuffer o2) {
requireNonNull(o1);
requireNonNull(o2);
@@ -95,6 +90,47 @@ public static int compareBuff(final DirectBuffer o1, final DirectBuffer o2) {
return o1.capacity() - o2.capacity();
}
+ /**
+ * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when using
+ * MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically.
+ *
+ * Both buffer must have 4 or 8 bytes remaining
+ *
+ * @param o1 left operand (required)
+ * @param o2 right operand (required)
+ * @return as specified by {@link Comparable} interface
+ */
+ public static int compareAsIntegerKeys(final DirectBuffer o1, final DirectBuffer o2) {
+ requireNonNull(o1);
+ requireNonNull(o2);
+ // Both buffers should be same len
+ final int len1 = o1.capacity();
+ final int len2 = o2.capacity();
+ if (len1 != len2) {
+ throw new RuntimeException(
+ "Length mismatch, len1: "
+ + len1
+ + ", len2: "
+ + len2
+ + ". Lengths must be identical and either 4 or 8 bytes.");
+ }
+ if (len1 == 8) {
+ final long lw = o1.getLong(0, NATIVE_ORDER);
+ final long rw = o2.getLong(0, NATIVE_ORDER);
+ return Long.compareUnsigned(lw, rw);
+ } else if (len1 == 4) {
+ final int lw = o1.getInt(0, NATIVE_ORDER);
+ final int rw = o2.getInt(0, NATIVE_ORDER);
+ return Integer.compareUnsigned(lw, rw);
+ } else {
+ // size_t and int are likely to be 8bytes and 4bytes respectively on 64bit.
+ // If 32bit then would be 4/2 respectively.
+ // Short.compareUnsigned is not available in Java8.
+ // For now just fall back to our standard comparator
+ return compareLexicographically(o1, o2);
+ }
+ }
+
@Override
protected DirectBuffer allocate() {
final ArrayDeque q = BUFFERS.get();
@@ -109,13 +145,12 @@ protected DirectBuffer allocate() {
}
@Override
- protected Comparator getSignedComparator() {
- return signedComparator;
- }
-
- @Override
- protected Comparator getUnsignedComparator() {
- return unsignedComparator;
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) {
+ return DirectBufferProxy::compareAsIntegerKeys;
+ } else {
+ return DirectBufferProxy::compareLexicographically;
+ }
}
@Override
diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java
index 3db16119..4bc6cca8 100644
--- a/src/main/java/org/lmdbjava/Env.java
+++ b/src/main/java/org/lmdbjava/Env.java
@@ -16,24 +16,28 @@
package org.lmdbjava;
import static java.lang.Boolean.getBoolean;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL;
import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR;
import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV;
import static org.lmdbjava.Library.LIB;
import static org.lmdbjava.Library.RUNTIME;
-import static org.lmdbjava.MaskedFlag.isSet;
-import static org.lmdbjava.MaskedFlag.mask;
import static org.lmdbjava.ResultCodeMapper.checkRc;
-import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN;
import java.io.File;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
import jnr.ffi.Pointer;
import jnr.ffi.byref.IntByReference;
import jnr.ffi.byref.PointerByReference;
@@ -50,6 +54,12 @@ public final class Env implements AutoCloseable {
/** Java system property name that can be set to disable optional checks. */
public static final String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks";
+ /**
+ * The default {@link Charset} used to convert DB names from a byte[] to a String or to encode a
+ * String as a byte[]. Only used if not explicit {@link Charset} is provided.
+ */
+ public static final Charset DEFAULT_NAME_CHARSET = StandardCharsets.UTF_8;
+
/**
* Indicates whether optional checks should be applied in LmdbJava. Optional checks are only
* disabled in critical paths (see package-level JavaDocs). Non-critical paths have optional
@@ -63,18 +73,24 @@ public final class Env implements AutoCloseable {
private final BufferProxy proxy;
private final Pointer ptr;
private final boolean readOnly;
+ private final Path path;
+ private final EnvFlagSet envFlagSet;
private Env(
final BufferProxy proxy,
final Pointer ptr,
final boolean readOnly,
- final boolean noSubDir) {
+ final boolean noSubDir,
+ final Path path,
+ final EnvFlagSet envFlagSet) {
this.proxy = proxy;
this.readOnly = readOnly;
this.noSubDir = noSubDir;
this.ptr = ptr;
// cache max key size to avoid further JNI calls
this.maxKeySize = LIB.mdb_env_get_maxkeysize(ptr);
+ this.path = path;
+ this.envFlagSet = envFlagSet;
}
/**
@@ -98,16 +114,17 @@ public static Builder create(final BufferProxy proxy) {
}
/**
- * Opens an environment with a single default database in 0664 mode using the {@link
- * ByteBufferProxy#PROXY_OPTIMAL}.
- *
* @param path file system destination
* @param size size in megabytes
* @param flags the flags for this new environment
* @return env the environment (never null)
+ * @deprecated Instead use {@link Env#create()} or {@link Env#create(BufferProxy)}
+ * Opens an environment with a single default database in 0664 mode using the {@link
+ * ByteBufferProxy#PROXY_OPTIMAL}.
*/
+ @Deprecated
public static Env open(final File path, final int size, final EnvFlags... flags) {
- return new Builder<>(PROXY_OPTIMAL).setMapSize(size * 1_024L * 1_024L).open(path, flags);
+ return new Builder<>(PROXY_OPTIMAL).setMapSize(size, ByteUnit.MEBIBYTES).open(path, flags);
}
/**
@@ -141,12 +158,58 @@ public void close() {
*
* @param path writable destination path as described above
* @param flags special options for this copy
+ * @deprecated Use {@link Env#copy(Path, CopyFlagSet)}
*/
+ @Deprecated
public void copy(final File path, final CopyFlags... flags) {
requireNonNull(path);
+ copy(path.toPath(), CopyFlagSet.of(flags));
+ }
+
+ /**
+ * Copies an LMDB environment to the specified destination path.
+ *
+ * This function may be used to make a backup of an existing environment. No lockfile is
+ * created, since it gets recreated at need.
+ *
+ *
If this environment was created using {@link EnvFlags#MDB_NOSUBDIR}, the destination path
+ * must be a directory that exists but contains no files. If {@link EnvFlags#MDB_NOSUBDIR} was
+ * used, the destination path must not exist, but it must be possible to create a file at the
+ * provided path.
+ *
+ *
Note: This call can trigger significant file size growth if run in parallel with write
+ * transactions, because it employs a read-only transaction. See long-lived transactions under
+ * "Caveats" in the LMDB native documentation.
+ *
+ * @param path writable destination path as described above
+ */
+ public void copy(final Path path) {
+ copy(path, CopyFlagSet.EMPTY);
+ }
+
+ /**
+ * Copies an LMDB environment to the specified destination path.
+ *
+ *
This function may be used to make a backup of an existing environment. No lockfile is
+ * created, since it gets recreated at need.
+ *
+ *
If this environment was created using {@link EnvFlags#MDB_NOSUBDIR}, the destination path
+ * must be a directory that exists but contains no files. If {@link EnvFlags#MDB_NOSUBDIR} was
+ * used, the destination path must not exist, but it must be possible to create a file at the
+ * provided path.
+ *
+ *
Note: This call can trigger significant file size growth if run in parallel with write
+ * transactions, because it employs a read-only transaction. See long-lived transactions under
+ * "Caveats" in the LMDB native documentation.
+ *
+ * @param path writable destination path as described above
+ * @param flags special options for this copy
+ */
+ public void copy(final Path path, final CopyFlagSet flags) {
+ requireNonNull(path);
+ requireNonNull(flags);
validatePath(path);
- final int flagsMask = mask(true, flags);
- checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flagsMask));
+ checkRc(LIB.mdb_env_copy2(ptr, path.toAbsolutePath().toString(), flags.getMask()));
}
/**
@@ -162,19 +225,38 @@ public void copy(final File path, final CopyFlags... flags) {
*/
public List getDbiNames() {
final List result = new ArrayList<>();
- final Dbi names = openDbi((byte[]) null);
- try (Txn txn = txnRead();
- Cursor cursor = names.openCursor(txn)) {
- if (!cursor.first()) {
- return Collections.emptyList();
+ // The unnamed DB is special so the names of the named DBs are held as keys in it.
+ try (final Txn readTxn = txnRead()) {
+ final Dbi unnamedDb = new Dbi<>(this, readTxn, null, proxy, DbiFlagSet.EMPTY);
+ try (final Cursor cursor = unnamedDb.openCursor(readTxn)) {
+ if (!cursor.first()) {
+ return Collections.emptyList();
+ }
+ do {
+ final byte[] name = proxy.getBytes(cursor.key());
+ result.add(name);
+ } while (cursor.next());
}
- do {
- final byte[] name = proxy.getBytes(cursor.key());
- result.add(name);
- } while (cursor.next());
}
+ return Collections.unmodifiableList(result);
+ }
- return result;
+ /**
+ * Obtain the DBI names.
+ *
+ * This method is only compatible with {@link Env}s that use named databases. If an unnamed
+ * {@link Dbi} is being used to store data, this method will attempt to return all such keys from
+ * the unnamed database.
+ *
+ *
This method must not be called from concurrent threads.
+ *
+ * @return a list of DBI names (never null)
+ */
+ public List getDbiNames(final Charset charset) {
+ final List dbiNames = getDbiNames();
+ return dbiNames.stream()
+ .map(nameBytes -> Dbi.getNameAsString(nameBytes, charset))
+ .collect(Collectors.toList());
}
/**
@@ -183,9 +265,23 @@ public List getDbiNames() {
* @param mapSize the new size, in bytes
*/
public void setMapSize(final long mapSize) {
+ if (mapSize < 0) {
+ throw new IllegalArgumentException("Negative value; overflow?");
+ }
checkRc(LIB.mdb_env_set_mapsize(ptr, mapSize));
}
+ /**
+ * Set the size of the data memory map.
+ *
+ * @param mapSize new map size in the units of byteUnit.
+ * @param byteUnit The unit that mapSize is in.
+ */
+ public void setMapSize(final long mapSize, final ByteUnit byteUnit) {
+ requireNonNull(byteUnit);
+ setMapSize(byteUnit.toBytes(mapSize));
+ }
+
/**
* Get the maximum size of keys and MDB_DUPSORT data we can write.
*
@@ -242,137 +338,159 @@ public boolean isReadOnly() {
}
/**
- * Convenience method that opens a {@link Dbi} with a UTF-8 database name and default {@link
- * Comparator} that is not invoked from native code.
+ * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}.
+ *
+ * The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if you need to create a
+ * new database before opening it.
*
+ * @return A new builder instance for creating/opening a {@link Dbi}.
+ */
+ public DbiBuilder createDbi() {
+ return new DbiBuilder<>(this, proxy, readOnly);
+ }
+
+ /**
* @param name name of the database (or null if no name is required)
* @param flags to open the database with
* @return a database that is ready to use
+ * @deprecated Instead use {@link Env#createDbi()}
+ * Convenience method that opens a {@link Dbi} with a UTF-8 database name and default
+ * {@link Comparator} that is not invoked from native code.
*/
+ @Deprecated()
public Dbi openDbi(final String name, final DbiFlags... flags) {
- final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8);
- return openDbi(nameBytes, null, false, flags);
+ return openDbi(Dbi.getNameBytes(name), null, false, flags);
}
/**
- * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link
- * Comparator} that is not invoked from native code.
- *
* @param name name of the database (or null if no name is required)
- * @param comparator custom comparator callback (or null to use default)
+ * @param comparator custom comparator for cursor start/stop key comparisons. If null, LMDB's
+ * comparator will be used.
* @param flags to open the database with
* @return a database that is ready to use
+ * @deprecated Instead use {@link Env#createDbi()}
+ * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated
+ * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys.
+ *
It is very important that the passed comparator behaves in the same way as the
+ * comparator LMDB uses for its insertion order (for the type of data that will be stored in
+ * the database), or you fully understand the implications of them behaving differently.
+ * LMDB's comparator is unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is
+ * used.
*/
+ @Deprecated()
public Dbi openDbi(
final String name, final Comparator comparator, final DbiFlags... flags) {
- final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8);
- return openDbi(nameBytes, comparator, false, flags);
+ return openDbi(Dbi.getNameBytes(name), comparator, false, flags);
}
/**
- * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link
- * Comparator} that may be invoked from native code if specified.
- *
* @param name name of the database (or null if no name is required)
- * @param comparator custom comparator callback (or null to use default)
- * @param nativeCb whether native code calls back to the Java comparator
+ * @param comparator custom comparator for cursor start/stop key comparisons and optionally for
+ * LMDB to call back to. If null, LMDB's comparator will be used.
+ * @param nativeCb whether LMDB native code calls back to the Java comparator
* @param flags to open the database with
* @return a database that is ready to use
+ * @deprecated Instead use {@link Env#createDbi()}
+ * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated
+ * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing
+ * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be
+ * called by LMDB to determine insertion/iteration order. Calling back to a java comparator
+ * may significantly impact performance.
*/
+ @Deprecated()
public Dbi openDbi(
final String name,
final Comparator comparator,
final boolean nativeCb,
final DbiFlags... flags) {
- final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8);
- return openDbi(nameBytes, comparator, nativeCb, flags);
+ return openDbi(Dbi.getNameBytes(name), comparator, nativeCb, flags);
}
/**
- * Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is not
- * invoked from native code.
- *
* @param name name of the database (or null if no name is required)
* @param flags to open the database with
* @return a database that is ready to use
+ * @deprecated Instead use {@link Env#createDbi()}
+ * Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is
+ * not invoked from native code.
*/
+ @Deprecated()
public Dbi openDbi(final byte[] name, final DbiFlags... flags) {
return openDbi(name, null, false, flags);
}
/**
- * Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that is not
- * invoked from native code.
- *
* @param name name of the database (or null if no name is required)
- * @param comparator custom comparator callback (or null to use LMDB default)
+ * @param comparator custom iterator comparator (or null to use LMDB default)
* @param flags to open the database with
* @return a database that is ready to use
+ * @deprecated Instead use {@link Env#createDbi()}
+ * Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that
+ * is not invoked from native code.
*/
+ @Deprecated()
public Dbi openDbi(
final byte[] name, final Comparator