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 dc7aaadd..a3c339bf 100644
--- a/src/main/java/org/lmdbjava/BufferProxy.java
+++ b/src/main/java/org/lmdbjava/BufferProxy.java
@@ -1,78 +1,55 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 java.lang.Long.BYTES;
+import java.util.Comparator;
import jnr.ffi.Pointer;
/**
* The strategy for mapping memory address to a given buffer type.
*
- *
- * The proxy is passed to the {@link Env#create(org.lmdbjava.BufferProxy)}
- * method and is subsequently used by every {@link Txn}, {@link Dbi} and
- * {@link Cursor} associated with the {@link Env}.
+ *
The proxy is passed to the {@link Env#create(org.lmdbjava.BufferProxy)} method and is
+ * subsequently used by every {@link Txn}, {@link Dbi} and {@link Cursor} associated with the {@link
+ * Env}.
*
* @param buffer type
*/
-@SuppressWarnings("checkstyle:AbstractClassName")
public abstract class BufferProxy {
- /**
- * Size of a MDB_val pointer in bytes.
- */
+ /** Size of a MDB_val pointer in bytes. */
protected static final int MDB_VAL_STRUCT_SIZE = BYTES * 2;
- /**
- * Offset from a pointer of the MDB_val.mv_data field.
- */
+ /** Offset from a pointer of the MDB_val.mv_data field. */
protected static final int STRUCT_FIELD_OFFSET_DATA = BYTES;
- /**
- * Offset from a pointer of the MDB_val.mv_size field.
- */
+ /** Offset from a pointer of the MDB_val.mv_size field. */
protected static final int STRUCT_FIELD_OFFSET_SIZE = 0;
+ /** Explicitly-defined default constructor to avoid warnings. */
+ protected BufferProxy() {}
+
/**
- * Allocate a new buffer suitable for passing to
- * {@link #out(java.lang.Object, jnr.ffi.Pointer, long)}.
+ * Allocate a new buffer suitable for passing to {@link #out(java.lang.Object, jnr.ffi.Pointer)}.
*
* @return a buffer for passing to the out method
*/
protected abstract T allocate();
- /**
- * Compare the two buffers.
- *
- *
- * Implemented as a protected method to discourage use of the buffer proxy
- * in collections etc (given by design it wraps a temporary value only).
- *
- * @param o1 left operand
- * @param o2 right operand
- * @return as per {@link Comparable}
- */
- protected abstract int compare(T o1, T o2);
-
/**
* Deallocate a buffer that was previously provided by {@link #allocate()}.
*
@@ -89,37 +66,57 @@ public abstract class BufferProxy {
protected abstract byte[] getBytes(T buffer);
/**
- * Called when the MDB_val should be set to reflect the passed
- * buffer. This buffer will have been created by end users, not
- * {@link #allocate()}.
+ * Get a suitable default {@link Comparator} given the provided flags.
*
- * @param buffer the buffer to write to MDB_val
- * @param ptr the pointer to the MDB_val
- * @param ptrAddr the address of the MDB_val pointer
+ * The provided comparator must strictly match the lexicographical order of keys in the native
+ * LMDB database.
+ *
+ * @param dbiFlagSet The {@link DbiFlags} set for the database.
+ * @return a comparator that can be used (never null)
+ */
+ public abstract Comparator getComparator(final DbiFlagSet dbiFlagSet);
+
+ /**
+ * Get a suitable default {@link Comparator}
+ *
+ * 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 void in(T buffer, Pointer ptr, long ptrAddr);
+ public Comparator getComparator() {
+ return getComparator(DbiFlagSet.empty());
+ }
/**
- * Called when the MDB_val should be set to reflect the passed
- * buffer.
+ * Called when the MDB_val should be set to reflect the passed buffer. This buffer
+ * will have been created by end users, not {@link #allocate()}.
*
- * @param buffer the buffer to write to MDB_val
- * @param size the buffer size to write to MDB_val
- * @param ptr the pointer to the MDB_val
- * @param ptrAddr the address of the MDB_val pointer
+ * @param buffer the buffer to write to MDB_val
+ * @param ptr the pointer to the MDB_val
+ * @return a transient pointer that must be kept alive, or null if none
*/
- protected abstract void in(T buffer, int size, Pointer ptr, long ptrAddr);
+ protected abstract Pointer in(T buffer, Pointer ptr);
/**
- * Called when the MDB_val may have changed and the passed buffer
- * should be modified to reflect the new MDB_val.
+ * Called when the MDB_val should be set to reflect the passed buffer.
*
- * @param buffer the buffer to write to MDB_val
- * @param ptr the pointer to the MDB_val
- * @param ptrAddr the address of the MDB_val pointer
+ * @param buffer the buffer to write to MDB_val
+ * @param size the buffer size to write to MDB_val
+ * @param ptr the pointer to the MDB_val
+ * @return a transient pointer that must be kept alive, or null if none
+ */
+ protected abstract Pointer in(T buffer, int size, Pointer ptr);
+
+ /**
+ * Called when the MDB_val may have changed and the passed buffer should be modified
+ * to reflect the new MDB_val.
+ *
+ * @param buffer the buffer to write to MDB_val
+ * @param ptr the pointer to the MDB_val
* @return the buffer for MDB_val
*/
- protected abstract T out(T buffer, Pointer ptr, long ptrAddr);
+ protected abstract T out(T buffer, Pointer ptr);
/**
* Create a new {@link KeyVal} to hold pointers for this buffer proxy.
@@ -130,4 +127,12 @@ 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 0bc05376..82b7721c 100644
--- a/src/main/java/org/lmdbjava/ByteArrayProxy.java
+++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java
@@ -1,49 +1,42 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 java.lang.Math.min;
import static java.util.Objects.requireNonNull;
import static org.lmdbjava.Library.RUNTIME;
import java.util.Arrays;
-
+import java.util.Comparator;
import jnr.ffi.Pointer;
import jnr.ffi.provider.MemoryManager;
/**
* Byte array proxy.
*
- * {@link Env#create(org.lmdbjava.BufferProxy)}.
+ * {@link Env#create(org.lmdbjava.BufferProxy)}.
*/
public final class ByteArrayProxy extends BufferProxy {
- /**
- * The byte array proxy. Guaranteed to never be null.
- */
+ /** The byte array proxy. Guaranteed to never be null. */
public static final BufferProxy PROXY_BA = new ByteArrayProxy();
private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager();
- private ByteArrayProxy() {
- }
+ private ByteArrayProxy() {}
/**
* Lexicographically compare two byte arrays.
@@ -52,13 +45,13 @@ 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) {
return 0;
}
- final int minLength = Math.min(o1.length, o2.length);
+ final int minLength = min(o1.length, o2.length);
for (int i = 0; i < minLength; i++) {
final int lw = Byte.toUnsignedInt(o1[i]);
@@ -77,11 +70,6 @@ protected byte[] allocate() {
return new byte[0];
}
- @Override
- protected int compare(final byte[] o1, final byte[] o2) {
- return compareArrays(o1, o2);
- }
-
@Override
protected void deallocate(final byte[] buff) {
// byte arrays cannot be allocated
@@ -93,23 +81,27 @@ protected byte[] getBytes(final byte[] buffer) {
}
@Override
- protected void in(final byte[] buffer, final Pointer ptr,
- final long ptrAddr) {
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ return ByteArrayProxy::compareLexicographically;
+ }
+
+ @Override
+ protected Pointer in(final byte[] buffer, final Pointer ptr) {
final Pointer pointer = MEM_MGR.allocateDirect(buffer.length);
pointer.put(0, buffer, 0, buffer.length);
ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, buffer.length);
ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, pointer.address());
+ return pointer;
}
@Override
- protected void in(final byte[] buffer, final int size, final Pointer ptr,
- final long ptrAddr) {
+ protected Pointer in(final byte[] buffer, final int size, final Pointer ptr) {
// cannot reserve for byte arrays
+ return null;
}
@Override
- protected byte[] out(final byte[] buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected byte[] out(final byte[] buffer, final Pointer ptr) {
final long addr = ptr.getAddress(STRUCT_FIELD_OFFSET_DATA);
final int size = (int) ptr.getLong(STRUCT_FIELD_OFFSET_SIZE);
final Pointer pointer = MEM_MGR.newPointer(addr, size);
diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java
index 54d8a44b..bcbb6ebf 100644
--- a/src/main/java/org/lmdbjava/ByteBufProxy.java
+++ b/src/main/java/org/lmdbjava/ByteBufProxy.java
@@ -1,48 +1,43 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 io.netty.buffer.PooledByteBufAllocator.DEFAULT;
import static java.lang.Class.forName;
+import static java.util.Objects.requireNonNull;
import static org.lmdbjava.UnsafeAccess.UNSAFE;
-import java.lang.reflect.Field;
-
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;
/**
* A buffer proxy backed by Netty's {@link ByteBuf}.
*
- *
- * This class requires {@link UnsafeAccess} and netty-buffer must be in the
- * classpath.
+ *
This class requires {@link UnsafeAccess} and netty-buffer must be in the classpath.
*/
public final class ByteBufProxy extends BufferProxy {
/**
- * A proxy for using Netty {@link ByteBuf}. Guaranteed to never be null,
- * although a class initialization exception will occur if an attempt is made
- * to access this field when Netty is unavailable.
+ * A proxy for using Netty {@link ByteBuf}. Guaranteed to never be null, although a class
+ * initialization exception will occur if an attempt is made to access this field when Netty is
+ * unavailable.
*/
public static final BufferProxy PROXY_NETTY = new ByteBufProxy();
@@ -52,14 +47,20 @@ public final class ByteBufProxy extends BufferProxy {
private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf";
private final long lengthOffset;
private final long addressOffset;
-
+
private final PooledByteBufAllocator nettyAllocator;
private ByteBufProxy() {
this(DEFAULT);
}
+ /**
+ * Constructs a buffer proxy for use with Netty.
+ *
+ * @param allocator the Netty allocator to obtain the {@link ByteBuf} from
+ */
public ByteBufProxy(final PooledByteBufAllocator allocator) {
+ super();
this.nettyAllocator = allocator;
try {
@@ -74,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 {
@@ -107,8 +173,12 @@ protected ByteBuf allocate() {
}
@Override
- protected int compare(final ByteBuf o1, final ByteBuf o2) {
- return o1.compareTo(o2);
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) {
+ return ByteBufProxy::compareAsIntegerKeys;
+ } else {
+ return ByteBufProxy::compareLexicographically;
+ }
}
@Override
@@ -124,30 +194,31 @@ protected byte[] getBytes(final ByteBuf buffer) {
}
@Override
- protected void in(final ByteBuf buffer, final Pointer ptr, final long ptrAddr) {
- UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE,
- buffer.writerIndex() - buffer.readerIndex());
- UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA,
- buffer.memoryAddress() + buffer.readerIndex());
+ protected Pointer in(final ByteBuf buffer, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
+ UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, buffer.writerIndex() - buffer.readerIndex());
+ UNSAFE.putLong(
+ ptrAddr + STRUCT_FIELD_OFFSET_DATA, buffer.memoryAddress() + buffer.readerIndex());
+ return null;
}
@Override
- protected void in(final ByteBuf buffer, final int size, final Pointer ptr,
- final long ptrAddr) {
- UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE,
- size);
- UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA,
- buffer.memoryAddress() + buffer.readerIndex());
+ protected Pointer in(final ByteBuf buffer, final int size, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
+ UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size);
+ UNSAFE.putLong(
+ ptrAddr + STRUCT_FIELD_OFFSET_DATA, buffer.memoryAddress() + buffer.readerIndex());
+ return null;
}
@Override
- protected ByteBuf out(final ByteBuf buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected ByteBuf out(final ByteBuf buffer, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA);
final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE);
UNSAFE.putLong(buffer, addressOffset, addr);
UNSAFE.putInt(buffer, lengthOffset, (int) size);
- buffer.writerIndex((int) size).readerIndex(0);
+ buffer.clear().writerIndex((int) size);
return buffer;
}
}
diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java
index 2dd337c0..b5dfca0b 100644
--- a/src/main/java/org/lmdbjava/ByteBufferProxy.java
+++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java
@@ -1,23 +1,18 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 java.lang.Long.reverseBytes;
@@ -32,48 +27,45 @@
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;
/**
* {@link ByteBuffer}-based proxy.
*
- *
- * There are two concrete {@link ByteBuffer} proxy implementations available:
+ *
There are two concrete {@link ByteBuffer} proxy implementations available:
+ *
*
- * - A "fast" implementation: {@link UnsafeProxy}
- * - A "safe" implementation: {@link ReflectiveProxy}
+ * - A "fast" implementation: {@link UnsafeProxy}
+ *
- A "safe" implementation: {@link ReflectiveProxy}
*
*
- *
- * Users nominate which implementation they prefer by referencing the
- * {@link #PROXY_OPTIMAL} or {@link #PROXY_SAFE} field when invoking
- * {@link Env#create(org.lmdbjava.BufferProxy)}.
+ *
Users nominate which implementation they prefer by referencing the {@link #PROXY_OPTIMAL} or
+ * {@link #PROXY_SAFE} field when invoking {@link Env#create(org.lmdbjava.BufferProxy)}.
*/
public final class ByteBufferProxy {
/**
- * The fastest {@link ByteBuffer} proxy that is available on this platform.
- * This will always be the same instance as {@link #PROXY_SAFE} if the
- * {@link UnsafeAccess#DISABLE_UNSAFE_PROP} has been set to true
- * and/or {@link UnsafeAccess} is unavailable. Guaranteed to never be null.
+ * The fastest {@link ByteBuffer} proxy that is available on this platform. This will always be
+ * the same instance as {@link #PROXY_SAFE} if the {@link UnsafeAccess#DISABLE_UNSAFE_PROP} has
+ * been set to true and/or {@link UnsafeAccess} is unavailable. Guaranteed to never
+ * be null.
*/
public static final BufferProxy PROXY_OPTIMAL;
- /**
- * The safe, reflective {@link ByteBuffer} proxy for this system. Guaranteed
- * to never be null.
- */
+ /** 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();
}
- private ByteBufferProxy() {
- }
+ private ByteBufferProxy() {}
private static BufferProxy getProxyOptimal() {
try {
@@ -83,24 +75,20 @@ private static BufferProxy getProxyOptimal() {
}
}
- /**
- * The buffer must be a direct buffer (not heap allocated).
- */
+ /** The buffer must be a direct buffer (not heap allocated). */
public static final class BufferMustBeDirectException extends LmdbException {
private static final long serialVersionUID = 1L;
- /**
- * Creates a new instance.
- */
+ /** Creates a new instance. */
public BufferMustBeDirectException() {
super("The buffer must be a direct buffer (not heap allocated");
}
}
/**
- * Provides {@link ByteBuffer} pooling and address resolution for concrete
- * {@link BufferProxy} implementations.
+ * Provides {@link ByteBuffer} pooling and address resolution for concrete {@link BufferProxy}
+ * implementations.
*/
abstract static class AbstractByteBufferProxy extends BufferProxy {
@@ -108,12 +96,11 @@ abstract static class AbstractByteBufferProxy extends BufferProxy {
protected static final String FIELD_NAME_CAPACITY = "capacity";
/**
- * 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.
+ * 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.
*/
- private static final ThreadLocal> BUFFERS
- = withInitial(() -> new ArrayDeque<>(16));
+ private static final ThreadLocal> BUFFERS =
+ withInitial(() -> new ArrayDeque<>(16));
/**
* Lexicographically compare two buffers.
@@ -122,13 +109,10 @@ abstract static class AbstractByteBufferProxy extends BufferProxy {
* @param o2 right operand (required)
* @return as specified by {@link Comparable} interface
*/
- @SuppressWarnings("PMD.CyclomaticComplexity")
- public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) {
+ public static int compareLexicographically(final ByteBuffer o1, final ByteBuffer o2) {
requireNonNull(o1);
requireNonNull(o2);
- if (o1.equals(o2)) {
- return 0;
- }
+
final int minLength = Math.min(o1.limit(), o2.limit());
final int minWords = minLength / Long.BYTES;
@@ -155,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 {
@@ -189,8 +222,12 @@ protected final ByteBuffer allocate() {
}
@Override
- protected final int compare(final ByteBuffer o1, final ByteBuffer o2) {
- return compareBuff(o1, o2);
+ public Comparator getComparator(final DbiFlagSet dbiFlagSet) {
+ if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) {
+ return AbstractByteBufferProxy::compareAsIntegerKeys;
+ } else {
+ return AbstractByteBufferProxy::compareLexicographically;
+ }
}
@Override
@@ -206,12 +243,11 @@ protected byte[] getBytes(final ByteBuffer buffer) {
buffer.get(dest, 0, buffer.limit());
return dest;
}
-
}
/**
- * A proxy that uses Java reflection to modify byte buffer fields, and
- * official JNR-FFF methods to manipulate native pointers.
+ * A proxy that uses Java reflection to modify byte buffer fields, and official JNR-FFF methods to
+ * manipulate native pointers.
*/
private static final class ReflectiveProxy extends AbstractByteBufferProxy {
@@ -224,22 +260,22 @@ private static final class ReflectiveProxy extends AbstractByteBufferProxy {
}
@Override
- protected void in(final ByteBuffer buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected Pointer in(final ByteBuffer buffer, final Pointer ptr) {
ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, address(buffer));
ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, buffer.remaining());
+ return null;
}
@Override
- protected void in(final ByteBuffer buffer, final int size, final Pointer ptr,
- final long ptrAddr) {
+ protected Pointer in(final ByteBuffer buffer, final int size, final Pointer ptr) {
ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, size);
ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, address(buffer));
+ return null;
}
@Override
- protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
final long addr = ptr.getAddress(STRUCT_FIELD_OFFSET_DATA);
final long size = ptr.getLong(STRUCT_FIELD_OFFSET_SIZE);
try {
@@ -251,12 +287,11 @@ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr,
buffer.clear();
return buffer;
}
-
}
/**
- * A proxy that uses Java's "unsafe" class to directly manipulate byte buffer
- * fields and JNR-FFF allocated memory pointers.
+ * A proxy that uses Java's "unsafe" class to directly manipulate byte buffer fields and JNR-FFF
+ * allocated memory pointers.
*/
private static final class UnsafeProxy extends AbstractByteBufferProxy {
@@ -275,22 +310,24 @@ private static final class UnsafeProxy extends AbstractByteBufferProxy {
}
@Override
- protected void in(final ByteBuffer buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected Pointer in(final ByteBuffer buffer, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, buffer.remaining());
UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, address(buffer));
+ return null;
}
@Override
- protected void in(final ByteBuffer buffer, final int size, final Pointer ptr,
- final long ptrAddr) {
+ protected Pointer in(final ByteBuffer buffer, final int size, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size);
UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, address(buffer));
+ return null;
}
@Override
- protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr,
- final long ptrAddr) {
+ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr) {
+ final long ptrAddr = ptr.address();
final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA);
final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE);
UNSAFE.putLong(buffer, ADDRESS_OFFSET, addr);
@@ -299,5 +336,4 @@ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr,
return buffer;
}
}
-
}
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 685df3c3..e3677baf 100644
--- a/src/main/java/org/lmdbjava/CopyFlags.java
+++ b/src/main/java/org/lmdbjava/CopyFlags.java
@@ -1,35 +1,28 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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;
-/**
- * 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.
- */
+ /** Compacting copy: Omit free space from copy, and renumber all pages sequentially. */
MDB_CP_COMPACT(0x01);
private final int mask;
@@ -43,4 +36,28 @@ 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 f165423b..0e320930 100644
--- a/src/main/java/org/lmdbjava/Cursor.java
+++ b/src/main/java/org/lmdbjava/Cursor.java
@@ -1,23 +1,18 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 java.util.Objects.requireNonNull;
@@ -25,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;
@@ -51,46 +44,50 @@ public final class Cursor implements AutoCloseable {
private final KeyVal kv;
private final Pointer ptrCursor;
private Txn txn;
+ private final Env env;
- Cursor(final Pointer ptr, final Txn txn) {
+ Cursor(final Pointer ptr, final Txn txn, final Env env) {
requireNonNull(ptr);
requireNonNull(txn);
this.ptrCursor = ptr;
this.txn = txn;
this.kv = txn.newKeyVal();
+ this.env = env;
}
/**
* Close a cursor handle.
*
- *
- * The cursor handle will be freed and must not be used again after this call.
- * Its transaction must still be live if it is a write-transaction.
+ *
The cursor handle will be freed and must not be used again after this call. Its transaction
+ * must still be live if it is a write-transaction.
*/
@Override
public void close() {
if (closed) {
return;
}
- if (SHOULD_CHECK && !txn.isReadOnly()) {
- txn.checkReady();
+ kv.close();
+ if (SHOULD_CHECK) {
+ env.checkNotClosed();
+ if (!txn.isReadOnly()) {
+ txn.checkReady();
+ }
}
LIB.mdb_cursor_close(ptrCursor);
- kv.close();
closed = true;
}
/**
* Return count of duplicates for current key.
*
- *
- * This call is only valid on databases that support sorted duplicate data
- * items {@link DbiFlags#MDB_DUPSORT}.
+ *
This call is only valid on databases that support sorted duplicate data items {@link
+ * DbiFlags#MDB_DUPSORT}.
*
* @return count of duplicates for current key
*/
public long count() {
if (SHOULD_CHECK) {
+ env.checkNotClosed();
checkNotClosed();
txn.checkReady();
}
@@ -99,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.
+ *
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(f);
- checkRc(LIB.mdb_cursor_del(ptrCursor, flags));
+ final PutFlagSet putFlagSet = flags != null ? flags : PutFlagSet.EMPTY;
+ checkRc(LIB.mdb_cursor_del(ptrCursor, putFlagSet.getMask()));
}
/**
@@ -129,23 +145,23 @@ public boolean first() {
/**
* Reposition the key/value buffers based on the passed key and operation.
*
- * @param key to search for
+ * @param key to search for
* @param data to search for
- * @param op options for this operation
+ * @param op options for this operation
* @return false if key not found
*/
public boolean get(final T key, final T data, final SeekOp op) {
if (SHOULD_CHECK) {
requireNonNull(key);
requireNonNull(op);
+ env.checkNotClosed();
checkNotClosed();
txn.checkReady();
}
- kv.keyIn(key);
- kv.valIn(data);
+ final Pointer transientKey = kv.keyIn(key);
+ final Pointer transientVal = kv.valIn(data);
- final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv
- .pointerVal(), op.getCode());
+ final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode());
if (rc == MDB_NOTFOUND) {
return false;
@@ -154,6 +170,11 @@ public boolean get(final T key, final T data, final SeekOp op) {
checkRc(rc);
kv.keyOut();
kv.valOut();
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(transientVal);
+ ReferenceUtil.reachabilityFence0(kv.key());
+ ReferenceUtil.reachabilityFence0(kv.val());
+ ReferenceUtil.reachabilityFence0(key);
return true;
}
@@ -161,20 +182,20 @@ public boolean get(final T key, final T data, final SeekOp op) {
* Reposition the key/value buffers based on the passed key and operation.
*
* @param key to search for
- * @param op options for this operation
+ * @param op options for this operation
* @return false if key not found
*/
public boolean get(final T key, final GetOp op) {
if (SHOULD_CHECK) {
requireNonNull(key);
requireNonNull(op);
+ env.checkNotClosed();
checkNotClosed();
txn.checkReady();
}
- kv.keyIn(key);
+ final Pointer transientKey = kv.keyIn(key);
- final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv
- .pointerVal(), op.getCode());
+ final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode());
if (rc == MDB_NOTFOUND) {
return false;
@@ -183,6 +204,10 @@ public boolean get(final T key, final GetOp op) {
checkRc(rc);
kv.keyOut();
kv.valOut();
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(kv.key());
+ ReferenceUtil.reachabilityFence0(kv.val());
+ ReferenceUtil.reachabilityFence0(key);
return true;
}
@@ -195,6 +220,10 @@ public T key() {
return kv.key();
}
+ KeyVal keyVal() {
+ return kv;
+ }
+
/**
* Position at last key/data item.
*
@@ -222,95 +251,158 @@ 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.
*
- *
- * This function stores key/data pairs into the database.
+ *
This function stores key/data pairs into the database.
*
* @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.
+ * @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();
txn.checkWritesAllowed();
}
- kv.keyIn(key);
- kv.valIn(val);
- final int mask = mask(op);
- final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(),
- kv.pointerVal(), mask);
+ final Pointer transientKey = kv.keyIn(key);
+ final Pointer transientVal = kv.valIn(val);
+ 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;
}
checkRc(rc);
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(transientVal);
+ ReferenceUtil.reachabilityFence0(key);
+ ReferenceUtil.reachabilityFence0(val);
return true;
}
/**
- * Put multiple values into the database in one MDB_MULTIPLE
- * operation.
+ * @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.
*
- *
- * 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.
+ *
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 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);
requireNonNull(val);
+ env.checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
+ if (!flags.isSet(MDB_MULTIPLE)) {
+ throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag");
+ }
}
- final int mask = mask(op);
- if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) {
- throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag");
- }
- txn.kv().keyIn(key);
+
+ 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);
+ ReferenceUtil.reachabilityFence0(key);
+ ReferenceUtil.reachabilityFence0(val);
}
/**
* Renew a cursor handle.
*
- *
- * A cursor is associated with a specific transaction and database. Cursors
- * that are only used in read-only transactions may be re-used, to avoid
- * unnecessary malloc/free overhead. The cursor may be associated with a new
- * read-only transaction, and referencing the same database handle as it was
- * created with. This may be done whether the previous transaction is live or
- * dead.
+ *
A cursor is associated with a specific transaction and database. Cursors that are only used
+ * in read-only transactions may be re-used, to avoid unnecessary malloc/free overhead. The cursor
+ * may be associated with a new read-only transaction, and referencing the same database handle as
+ * it was created with. This may be done whether the previous transaction is live or dead.
*
* @param newTxn transaction handle
*/
public void renew(final Txn newTxn) {
if (SHOULD_CHECK) {
requireNonNull(newTxn);
+ env.checkNotClosed();
checkNotClosed();
this.txn.checkReadOnly(); // existing
newTxn.checkReadOnly();
@@ -321,34 +413,72 @@ public void renew(final Txn newTxn) {
}
/**
- * 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.
+ * @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 the space
+ * requested.
*
- *
- * This flag must not be specified if the database was opened with MDB_DUPSORT
+ *
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 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();
txn.checkWritesAllowed();
}
- kv.keyIn(key);
- kv.valIn(size);
- final int flags = mask(op) | MDB_RESERVE.getMask();
- checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(),
- flags));
+ final Pointer transientKey = kv.keyIn(key);
+ final Pointer transientVal = kv.valIn(size);
+ // 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);
+ ReferenceUtil.reachabilityFence0(key);
return val();
}
@@ -361,12 +491,12 @@ public T reserve(final T key, final int size, final PutFlags... op) {
public boolean seek(final SeekOp op) {
if (SHOULD_CHECK) {
requireNonNull(op);
+ env.checkNotClosed();
checkNotClosed();
txn.checkReady();
}
- final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv
- .pointerVal(), op.getCode());
+ final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode());
if (rc == MDB_NOTFOUND) {
return false;
@@ -393,24 +523,18 @@ private void checkNotClosed() {
}
}
- /**
- * Cursor has already been closed.
- */
+ /** Cursor has already been closed. */
public static final class ClosedException extends LmdbException {
private static final long serialVersionUID = 1L;
- /**
- * Creates a new instance.
- */
+ /** Creates a new instance. */
public ClosedException() {
super("Cursor has already been closed");
}
}
- /**
- * Cursor stack too deep - internal error.
- */
+ /** Cursor stack too deep - internal error. */
public static final class FullException extends LmdbNativeException {
static final int MDB_CURSOR_FULL = -30_787;
@@ -420,5 +544,4 @@ public static final class FullException extends LmdbNativeException {
super(MDB_CURSOR_FULL, "Cursor stack too deep - internal error");
}
}
-
}
diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java
index 5bd86272..65fc1023 100644
--- a/src/main/java/org/lmdbjava/CursorIterable.java
+++ b/src/main/java/org/lmdbjava/CursorIterable.java
@@ -1,23 +1,18 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 org.lmdbjava.CursorIterable.State.RELEASED;
@@ -26,66 +21,74 @@
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;
/**
- * {@link Iterable} that creates a single {@link Iterator} that will iterate
- * over a {@link Cursor} as specified by a {@link KeyRange}.
- *
- *
- * An instance will create and close its own cursor.
+ * {@link Iterable} that creates a single {@link Iterator} that will iterate over a {@link Cursor}
+ * as specified by a {@link KeyRange}.
*
- *
- * If iterating over keys stored with {@link DbiFlags#MDB_INTEGERKEY} you must
- * provide a Java comparator when constructing the {@link Dbi} or this class. It
- * is more efficient to use a comparator only with this class, as this avoids
- * LMDB calling back into Java code to perform the integer key comparison.
+ *
An instance will create and close its own cursor.
*
* @param buffer type
*/
-public final class CursorIterable implements
- Iterable>, AutoCloseable {
+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;
private final KeyRange range;
private State state = REQUIRES_INITIAL_OP;
- CursorIterable(final Txn txn, final Dbi dbi, final KeyRange range,
- final Comparator comparator) {
+ CursorIterable(
+ 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);
+ }
}
/**
* Obtain an iterator.
*
- *
- * As iteration of the returned iterator will cause movement of the underlying
- * LMDB cursor, an {@link IllegalStateException} is thrown if an attempt is
- * made to obtain the iterator more than once. For advanced cursor control
- * (such as being able to iterate over the same data multiple times etc)
- * please instead refer to {@link Dbi#openCursor(org.lmdbjava.Txn)}.
+ *
As iteration of the returned iterator will cause movement of the underlying LMDB cursor, an
+ * {@link IllegalStateException} is thrown if an attempt is made to obtain the iterator more than
+ * once. For advanced cursor control (such as being able to iterate over the same data multiple
+ * times etc) please instead refer to {@link Dbi#openCursor(org.lmdbjava.Txn)}.
*
* @return an iterator
*/
@Override
- @SuppressWarnings("checkstyle:AnonInnerLength")
public Iterator> iterator() {
if (iteratorReturned) {
throw new IllegalStateException("Iterator can only be returned once");
@@ -112,14 +115,13 @@ public KeyVal next() {
@Override
public void remove() {
- cursor.delete();
+ cursor.delete(PutFlags.EMPTY);
}
};
}
- @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NullAssignment"})
private void executeCursorOp(final CursorOp op) {
- final boolean found;
+ boolean found;
switch (op) {
case FIRST:
found = cursor.first();
@@ -137,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");
@@ -147,9 +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());
@@ -189,10 +213,9 @@ private void update() {
/**
* Holder for a key and value pair.
*
- *
- * The same holder instance will always be returned for a given iterator.
- * The returned keys and values may change or point to different memory
- * locations following changes in the iterator, cursor or transaction.
+ *
The same holder instance will always be returned for a given iterator. The returned keys and
+ * values may change or point to different memory locations following changes in the iterator,
+ * cursor or transaction.
*
* @param buffer type
*/
@@ -201,6 +224,9 @@ public static final class KeyVal {
private T k;
private T v;
+ /** Explicitly-defined default constructor to avoid warnings. */
+ public KeyVal() {}
+
/**
* The key.
*
@@ -226,15 +252,110 @@ void setK(final T key) {
void setV(final T val) {
this.v = val;
}
-
}
- /**
- * Represents the internal {@link CursorIterable} state.
- */
+ /** Represents the internal {@link CursorIterable} state. */
enum State {
- REQUIRES_INITIAL_OP, REQUIRES_NEXT_OP, REQUIRES_ITERATOR_OP, RELEASED,
+ REQUIRES_INITIAL_OP,
+ REQUIRES_NEXT_OP,
+ REQUIRES_ITERATOR_OP,
+ 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 7ccb1940..d2afdf8f 100644
--- a/src/main/java/org/lmdbjava/Dbi.java
+++ b/src/main/java/org/lmdbjava/Dbi.java
@@ -1,23 +1,18 @@
-/*-
- * #%L
- * LmdbJava
- * %%
- * Copyright (C) 2016 - 2021 The LmdbJava Open Source Project
- * %%
+/*
+ * 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
- *
+ *
+ * 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 java.util.Objects.requireNonNull;
@@ -36,11 +31,11 @@
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;
import java.util.List;
-
import jnr.ffi.Pointer;
import jnr.ffi.byref.IntByReference;
import jnr.ffi.byref.PointerByReference;
@@ -54,56 +49,92 @@
*/
public final class Dbi {
- private final ComparatorCallback ccb;
+ @SuppressWarnings("FieldCanBeLocal") // Needs to be instance variable for FFI
+ private final ComparatorCallback callbackComparator;
+
private boolean cleaned;
- private final Comparator compFunc;
+ // Used for CursorIterable KeyRange testing and/or native callbacks
+ private final Comparator comparator;
private final Env env;
private final byte[] name;
- private final BufferProxy proxy;
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,
+ final Txn txn,
+ final byte[] name,
+ final Comparator comparator,
+ final boolean nativeCb,
+ final BufferProxy proxy,
+ final DbiFlagSet dbiFlagSet) {
- Dbi(final Env env, final Txn txn, final byte[] name,
- final Comparator comparator, final DbiFlags... flags) {
+ 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);
- final int flagsMask = mask(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 (comparator == null) {
- proxy = null;
- compFunc = null;
- ccb = null;
+ if (nativeCb) {
+ // LMDB will call back to this comparator for insertion/iteration order
+ this.callbackComparator = createCallbackComparator(proxy);
+ LIB.mdb_set_compare(txn.pointer(), ptr, callbackComparator);
} else {
- this.proxy = txn.getProxy();
- this.compFunc = comparator;
- this.ccb = (keyA, keyB) -> {
- final T compKeyA = proxy.allocate();
- final T compKeyB = proxy.allocate();
- proxy.out(compKeyA, keyA, keyA.address());
- proxy.out(compKeyB, keyB, keyB.address());
- final int result = compFunc.compare(compKeyA, compKeyB);
- proxy.deallocate(compKeyA);
- proxy.deallocate(compKeyB);
- return result;
- };
- LIB.mdb_set_compare(txn.pointer(), ptr, ccb);
+ 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).
*
- *
- * It is very rare that closing a database handle is useful. There are also
- * many warnings/restrictions if closing a database handle (refer to the LMDB
- * C documentation). As such this is non-routine usage and this class does not
- * track the open/closed state of the {@link Dbi}. Advanced users are expected
- * to have specific reasons for using this method and will manage their own
- * state accordingly.
+ *
It is very rare that closing a database handle is useful. There are also many
+ * warnings/restrictions if closing a database handle (refer to the LMDB C documentation). As such
+ * this is non-routine usage and this class does not track the open/closed state of the {@link
+ * Dbi}. Advanced users are expected to have specific reasons for using this method and will
+ * manage their own state accordingly.
*/
public void close() {
clean();
+ if (SHOULD_CHECK) {
+ env.checkNotClosed();
+ }
LIB.mdb_dbi_close(env.pointer(), ptr);
}
@@ -112,7 +143,6 @@ public void close() {
*
* @param key key to delete from the database (not null)
* @return true if the key/data pair was found, false otherwise
- *
* @see #delete(org.lmdbjava.Txn, java.lang.Object, java.lang.Object)
*/
public boolean delete(final T key) {
@@ -129,7 +159,6 @@ public boolean delete(final T key) {
* @param txn transaction handle (not null; not committed; must be R-W)
* @param key key to delete from the database (not null)
* @return true if the key/data pair was found, false otherwise
- *
* @see #delete(org.lmdbjava.Txn, java.lang.Object, java.lang.Object)
*/
public boolean delete(final Txn txn, final T key) {
@@ -139,12 +168,10 @@ public boolean delete(final Txn txn, final T key) {
/**
* Removes key/data pairs from the database.
*
- *
- * If the database does not support sorted duplicate data items
- * ({@link DbiFlags#MDB_DUPSORT}) the value parameter is ignored. If the
- * database supports sorted duplicates and the value parameter is null, all of
- * the duplicate data items for the key will be deleted. Otherwise, if the
- * data parameter is non-null only the matching data item will be deleted.
+ *
If the database does not support sorted duplicate data items ({@link DbiFlags#MDB_DUPSORT})
+ * the value parameter is ignored. If the database supports sorted duplicates and the value
+ * parameter is null, all of the duplicate data items for the key will be deleted. Otherwise, if
+ * the data parameter is non-null only the matching data item will be deleted.
*
* @param txn transaction handle (not null; not committed; must be R-W)
* @param key key to delete from the database (not null)
@@ -159,11 +186,12 @@ public boolean delete(final Txn txn, final T key, final T val) {
txn.checkWritesAllowed();
}
- txn.kv().keyIn(key);
+ final Pointer transientKey = txn.kv().keyIn(key);
Pointer data = null;
+ Pointer transientVal = null;
if (val != null) {
- txn.kv().valIn(val);
+ transientVal = txn.kv().valIn(val);
data = txn.kv().pointerVal();
}
final int rc = LIB.mdb_del(txn.pointer(), ptr, txn.kv().pointerKey(), data);
@@ -171,16 +199,18 @@ public boolean delete(final Txn txn, final T key, final T val) {
return false;
}
checkRc(rc);
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(transientVal);
+ ReferenceUtil.reachabilityFence0(key);
+ ReferenceUtil.reachabilityFence0(val);
return true;
}
/**
* Drops the data in this database, leaving the database open for further use.
*
- *
- * This method slightly differs from the LMDB C API in that it does not
- * provide support for also closing the DB handle. If closing the DB handle is
- * required, please see {@link #close()}.
+ *
This method slightly differs from the LMDB C API in that it does not provide support for
+ * also closing the DB handle. If closing the DB handle is required, please see {@link #close()}.
*
* @param txn transaction handle (not null; not committed; must be R-W)
*/
@@ -189,16 +219,17 @@ public void drop(final Txn txn) {
}
/**
- * Drops the database. If delete is set to true, the database will be deleted
- * and handle will be closed. See {@link #close()} for implication of handle
- * close. Otherwise, only the data in this database will be dropped.
+ * Drops the database. If delete is set to true, the database will be deleted and handle will be
+ * closed. See {@link #close()} for implication of handle close. Otherwise, only the data in this
+ * database will be dropped.
*
- * @param txn transaction handle (not null; not committed; must be R-W)
+ * @param txn transaction handle (not null; not committed; must be R-W)
* @param delete whether database should be deleted.
*/
public void drop(final Txn txn, final boolean delete) {
if (SHOULD_CHECK) {
requireNonNull(txn);
+ env.checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
}
@@ -212,12 +243,10 @@ public void drop(final Txn txn, final boolean delete) {
/**
* Get items from a database, moving the {@link Txn#val()} to the value.
*
- *
- * This function retrieves key/data pairs from the database. The address and
- * length of the data associated with the specified \b key are returned in the
- * structure to which \b data refers. If the database supports duplicate keys
- * ({@link org.lmdbjava.DbiFlags#MDB_DUPSORT}) then the first data item for
- * the key will be returned. Retrieval of other items requires the use of
+ *
This function retrieves key/data pairs from the database. The address and length of the data
+ * associated with the specified \b key are returned in the structure to which \b data refers. If
+ * the database supports duplicate keys ({@link org.lmdbjava.DbiFlags#MDB_DUPSORT}) then the first
+ * data item for the key will be returned. Retrieval of other items requires the use of
* #mdb_cursor_get().
*
* @param txn transaction handle (not null; not committed)
@@ -228,91 +257,113 @@ public T get(final Txn txn, final T key) {
if (SHOULD_CHECK) {
requireNonNull(txn);
requireNonNull(key);
+ env.checkNotClosed();
txn.checkReady();
}
- txn.kv().keyIn(key);
- final int rc = LIB.mdb_get(txn.pointer(), ptr, txn.kv().pointerKey(), txn
- .kv().pointerVal());
+ final Pointer transientKey = txn.kv().keyIn(key);
+ final int rc = LIB.mdb_get(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal());
if (rc == MDB_NOTFOUND) {
return null;
}
checkRc(rc);
- return txn.kv().valOut(); // marked as out in LMDB C docs
+ final T result = txn.kv().valOut(); // marked as out in LMDB C docs
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(key);
+ return result;
}
/**
* 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);
}
/**
- * Iterate the database from the first item and forwards.
+ * Convert the passed name into bytes using the default {@link Charset}.
*
- * @param txn transaction handle (not null; not committed)
- * @return iterator
+ * @param name The name to convert.
+ * @return The name as a byte[] or null if name is null.
*/
- public CursorIterable iterate(final Txn txn) {
- return iterate(txn, all());
+ public static byte[] getNameBytes(final String name) {
+ return name == null ? null : name.getBytes(Env.DEFAULT_NAME_CHARSET);
}
/**
- * Iterate the database in accordance with the provided {@link KeyRange} and
- * default {@link Comparator}.
+ * Obtains the name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}.
*
- * @param txn transaction handle (not null; not committed)
- * @param range range of acceptable keys (not null)
- * @return iterator (never null)
+ * @return The name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}.
*/
- public CursorIterable iterate(final Txn txn, final KeyRange range) {
- return iterate(txn, range, null);
+ public String getNameAsString() {
+ return getNameAsString(Env.DEFAULT_NAME_CHARSET);
}
/**
- * Iterate the database in accordance with the provided {@link KeyRange} and
- * {@link Comparator}.
+ * Obtains the name of this database, using the supplied {@link Charset}.
*
- *
- * If a comparator is provided, it must reflect the same ordering as LMDB uses
- * for cursor operations (eg first, next, last, previous etc).
+ * @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.
*
- *
- * If a null comparator is provided, any comparator provided when opening the
- * database is used. If no database comparator was specified, the buffer's
- * default comparator is used. Such buffer comparators reflect LMDB's default
- * lexicographical order.
+ * @param txn transaction handle (not null; not committed)
+ * @return iterator
+ */
+ public CursorIterable iterate(final Txn txn) {
+ return iterate(txn, all());
+ }
+
+ /**
+ * Iterate the database in accordance with the provided {@link KeyRange}.
*
- * @param txn transaction handle (not null; not committed)
- * @param range range of acceptable keys (not null)
- * @param comparator custom comparator for keys (may be null)
+ * @param txn transaction handle (not null; not committed)
+ * @param range range of acceptable keys (not null)
* @return iterator (never null)
*/
- public CursorIterable iterate(final Txn txn, final KeyRange range,
- final Comparator comparator) {
+ public CursorIterable iterate(final Txn txn, final KeyRange range) {
if (SHOULD_CHECK) {
requireNonNull(txn);
requireNonNull(range);
+ env.checkNotClosed();
txn.checkReady();
}
- final Comparator useComp;
- if (comparator == null) {
- useComp = compFunc == null ? txn.comparator() : compFunc;
- } else {
- useComp = comparator;
- }
- return new CursorIterable<>(txn, this, range, useComp);
+ return new CursorIterable<>(txn, this, range, comparator, proxy);
}
- /*
- * Return DbiFlags for this Dbi.
- *
- * @param txn transaction handle (not null; not committed)
- * @return the list of flags this Dbi was created with
+ /**
+ * Return DbiFlags for this Dbi.
+ *
+ * @param txn transaction handle (not null; not committed)
+ * @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();
+ }
final IntByReference resultPtr = new IntByReference();
checkRc(LIB.mdb_dbi_flags(txn.pointer(), ptr, resultPtr));
@@ -332,15 +383,13 @@ public List listFlags(final Txn txn) {
/**
* Create a cursor handle.
*
- *
- * A cursor is associated with a specific transaction and database. A cursor
- * cannot be used when its database handle is closed. Nor when its transaction
- * has ended, except with {@link Cursor#renew(org.lmdbjava.Txn)}. It can be
- * discarded with {@link Cursor#close()}. A cursor in a write-transaction can
- * be closed before its transaction ends, and will otherwise be closed when
- * its transaction ends. A cursor in a read-only transaction must be closed
- * explicitly, before or after its transaction ends. It can be reused with
- * {@link Cursor#renew(org.lmdbjava.Txn)} before finally closing it.
+ *
A cursor is associated with a specific transaction and database. A cursor cannot be used
+ * when its database handle is closed. Nor when its transaction has ended, except with {@link
+ * Cursor#renew(org.lmdbjava.Txn)}. It can be discarded with {@link Cursor#close()}. A cursor in a
+ * write-transaction can be closed before its transaction ends, and will otherwise be closed when
+ * its transaction ends. A cursor in a read-only transaction must be closed explicitly, before or
+ * after its transaction ends. It can be reused with {@link Cursor#renew(org.lmdbjava.Txn)} before
+ * finally closing it.
*
* @param txn transaction handle (not null; not committed)
* @return cursor handle
@@ -348,11 +397,12 @@ public List listFlags(final Txn txn) {
public Cursor openCursor(final Txn txn) {
if (SHOULD_CHECK) {
requireNonNull(txn);
+ env.checkNotClosed();
txn.checkReady();
}
final PointerByReference cursorPtr = new PointerByReference();
checkRc(LIB.mdb_cursor_open(txn.pointer(), ptr, cursorPtr));
- return new Cursor<>(cursorPtr.getValue(), txn);
+ return new Cursor<>(cursorPtr.getValue(), txn, env);
}
/**
@@ -360,89 +410,124 @@ 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.
*
- *
- * 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}).
+ *
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}).
*
- * @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.
+ * @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.
*/
- 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();
}
- txn.kv().keyIn(key);
- txn.kv().valIn(val);
- final int mask = mask(flags);
- final int rc = LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn
- .kv().pointerVal(), mask);
+ final Pointer transientKey = txn.kv().keyIn(key);
+ final Pointer transientVal = txn.kv().valIn(val);
+ final int rc =
+ 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;
}
checkRc(rc);
+ ReferenceUtil.reachabilityFence0(transientKey);
+ ReferenceUtil.reachabilityFence0(transientVal);
+ ReferenceUtil.reachabilityFence0(key);
+ ReferenceUtil.reachabilityFence0(val);
return true;
}
/**
- * 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
+ * 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
+ *
This flag must not be specified if the database was opened with MDB_DUPSORT
*
- * @param txn transaction handle (not null; not committed; must be R-W)
- * @param key key to store in the database (not null)
+ * @param txn transaction handle (not null; not committed; must be R-W)
+ * @param key key to store in the database (not null)
* @param size size of the value to be stored in the database
- * @param op options for this operation
+ * @param op options for this operation
* @return a buffer that can be used to modify the value
*/
- public T reserve(final Txn txn, final T key, final int size,
- final PutFlags... op) {
+ public T reserve(final Txn txn, final T key, final int size, final PutFlags... op) {
if (SHOULD_CHECK) {
requireNonNull(txn);
requireNonNull(key);
+ env.checkNotClosed();
txn.checkReady();
txn.checkWritesAllowed();
}
- txn.kv().keyIn(key);
- txn.kv().valIn(size);
+ final Pointer transientKey = txn.kv().keyIn(key);
+ final Pointer transientVal = txn.kv().valIn(size);
final int flags = mask(op) | MDB_RESERVE.getMask();
- checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv()
- .pointerVal(), flags));
+ 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);
+ ReferenceUtil.reachabilityFence0(transientVal);
+ ReferenceUtil.reachabilityFence0(key);
return txn.val();
}
@@ -455,6 +540,7 @@ public T reserve(final Txn txn, final T key, final int size,
public Stat stat(final Txn txn) {
if (SHOULD_CHECK) {
requireNonNull(txn);
+ env.checkNotClosed();
txn.checkReady();
}
final MDB_stat stat = new MDB_stat(RUNTIME);
@@ -475,9 +561,18 @@ private void clean() {
cleaned = true;
}
- /**
- * The specified DBI was changed unexpectedly.
- */
+ @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 {
static final int MDB_BAD_DBI = -30_780;
@@ -488,23 +583,18 @@ public static final class BadDbiException extends LmdbNativeException {
}
}
- /**
- * Unsupported size of key/DB name/data, or wrong DUPFIXED size.
- */
+ /** Unsupported size of key/DB name/data, or wrong DUPFIXED size. */
public static final class BadValueSizeException extends LmdbNativeException {
static final int MDB_BAD_VALSIZE = -30_781;
private static final long serialVersionUID = 1L;
BadValueSizeException() {
- super(MDB_BAD_VALSIZE,
- "Unsupported size of key/DB name/data, or wrong DUPFIXED size");
+ super(MDB_BAD_VALSIZE, "Unsupported size of key/DB name/data, or wrong DUPFIXED size");
}
}
- /**
- * Environment maxdbs reached.
- */
+ /** Environment maxdbs reached. */
public static final class DbFullException extends LmdbNativeException {
static final int MDB_DBS_FULL = -30_791;
@@ -518,14 +608,13 @@ public static final class DbFullException extends LmdbNativeException {
/**
* Operation and DB incompatible, or DB type changed.
*
- *
- * This can mean:
+ *
This can mean:
+ *
*
- * - The operation expects an MDB_DUPSORT / MDB_DUPFIXED database.
- * - Opening a named DB when the unnamed DB has MDB_DUPSORT /
- * MDB_INTEGERKEY.
- * - Accessing a data record as a database, or vice versa.
- * - The database was dropped and recreated with different flags.
+ * - The operation expects an MDB_DUPSORT / MDB_DUPFIXED database.
+ *
- Opening a named DB when the unnamed DB has MDB_DUPSORT / MDB_INTEGERKEY.
+ *
- Accessing a data record as a database, or vice versa.
+ *
- The database was dropped and recreated with different flags.
*
*/
public static final class IncompatibleException extends LmdbNativeException {
@@ -534,14 +623,11 @@ public static final class IncompatibleException extends LmdbNativeException {
private static final long serialVersionUID = 1L;
IncompatibleException() {
- super(MDB_INCOMPATIBLE,
- "Operation and DB incompatible, or DB type changed");
+ super(MDB_INCOMPATIBLE, "Operation and DB incompatible, or DB type changed");
}
}
- /**
- * Key/data pair already exists.
- */
+ /** Key/data pair already exists. */
public static final class KeyExistsException extends LmdbNativeException {
static final int MDB_KEYEXIST = -30_799;
@@ -552,9 +638,7 @@ public static final class KeyExistsException extends LmdbNativeException {
}
}
- /**
- * Key/data pair not found (EOF).
- */
+ /** Key/data pair not found (EOF). */
public static final class KeyNotFoundException extends LmdbNativeException {
static final int MDB_NOTFOUND = -30_798;
@@ -565,9 +649,7 @@ public static final class KeyNotFoundException extends LmdbNativeException {
}
}
- /**
- * Database contents grew beyond environment mapsize.
- */
+ /** Database contents grew beyond environment mapsize. */
public static final class MapResizedException extends LmdbNativeException {
static final int MDB_MAP_RESIZED = -30_785;
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