diff --git a/src/main/java/org/lmdbjava/AbstractFlagSet.java b/src/main/java/org/lmdbjava/AbstractFlagSet.java new file mode 100644 index 00000000..058969de --- /dev/null +++ b/src/main/java/org/lmdbjava/AbstractFlagSet.java @@ -0,0 +1,365 @@ +/* + * 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.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Encapsulates an immutable set of flags and the associated bit mask for the flags in the set. + */ +public abstract class AbstractFlagSet & MaskedFlag> implements FlagSet { + + private final Set flags; + private final int mask; + + protected AbstractFlagSet(final EnumSet flags) { + Objects.requireNonNull(flags); + this.mask = MaskedFlag.mask(flags); + this.flags = Collections.unmodifiableSet(Objects.requireNonNull(flags)); + } + + /** + * @return THe combined bit mask for all flags in the set. + */ + @Override + public int getMask() { + return mask; + } + + /** + * @return All flags in the set. + */ + @Override + public Set getFlags() { + return flags; + } + + /** + * @return True if flag has been set, i.e. is contained in this set. + */ + @Override + public boolean isSet(final T flag) { + // Probably cheaper to compare the masks than to use EnumSet.contains() + return flag != null + && MaskedFlag.isSet(mask, flag); + } + + /** + * @return The number of flags in this set. + */ + @Override + public int size() { + return flags.size(); + } + + /** + * @return True if this set is empty. + */ + @Override + public boolean isEmpty() { + return flags.isEmpty(); + } + + /** + * @return The {@link Iterator} for this set. + */ + @Override + public Iterator iterator() { + return flags.iterator(); + } + + @Override + public boolean equals(Object object) { + return FlagSet.equals(this, object); + } + + @Override + public int hashCode() { + return Objects.hash(flags, mask); + } + + @Override + public String toString() { + return FlagSet.asString(this); + } + + + // -------------------------------------------------------------------------------- + + + static abstract class AbstractSingleFlagSet & MaskedFlag> implements FlagSet { + + private final T flag; + // Only holding this for iterator() and getFlags() so make it lazy. + private EnumSet enumSet; + + public AbstractSingleFlagSet(final T flag) { + this.flag = Objects.requireNonNull(flag); + } + + @Override + public int getMask() { + return flag.getMask(); + } + + @Override + public Set getFlags() { + if (enumSet == null) { + return initSet(); + } else { + return this.enumSet; + } + } + + @Override + public boolean isSet(final T flag) { + return this.flag == flag; + } + + @Override + public boolean areAnySet(FlagSet flags) { + if (flags == null) { + return false; + } else { + return flags.isSet(this.flag); + } + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public Iterator iterator() { + if (enumSet == null) { + return initSet().iterator(); + } else { + return this.enumSet.iterator(); + } + } + + @Override + public String toString() { + return FlagSet.asString(this); + } + + @Override + public boolean equals(Object object) { + return FlagSet.equals(this, object); + } + + @Override + public int hashCode() { + return Objects.hash(flag, getFlags()); + } + + private Set initSet() { + final EnumSet set = EnumSet.of(this.flag); + this.enumSet = set; + return set; + } + } + + + // -------------------------------------------------------------------------------- + + + static class AbstractEmptyFlagSet implements FlagSet { + + @Override + public int getMask() { + return MaskedFlag.EMPTY_MASK; + } + + @Override + public Set getFlags() { + return Collections.emptySet(); + } + + @Override + public boolean isSet(final T flag) { + return false; + } + + @Override + public boolean areAnySet(final FlagSet flags) { + return false; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public String toString() { + return FlagSet.asString(this); + } + + @Override + public boolean equals(Object object) { + return FlagSet.equals(this, object); + } + + @Override + public int hashCode() { + return Objects.hash(getMask(), getFlags()); + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * A builder for creating a {@link AbstractFlagSet}. + * + * @param The type of flag to be held in the {@link AbstractFlagSet} + * @param The type of the {@link AbstractFlagSet} implementation. + */ + public static class Builder & MaskedFlag, S extends FlagSet> { + + final Class type; + final EnumSet enumSet; + final Function, S> constructor; + final Function singletonSetConstructor; + final Supplier emptySetSupplier; + + protected Builder(final Class type, + final Function, S> constructor, + final Function singletonSetConstructor, + final Supplier emptySetSupplier) { + this.type = type; + this.enumSet = EnumSet.noneOf(type); + this.constructor = Objects.requireNonNull(constructor); + this.singletonSetConstructor = Objects.requireNonNull(singletonSetConstructor); + this.emptySetSupplier = Objects.requireNonNull(emptySetSupplier); + } + + /** + * Replaces any flags already set in the builder with the contents of the passed flags {@link Collection} + * + * @param flags The flags to set in the builder. + * @return this builder instance. + */ + public Builder withFlags(final Collection flags) { + clear(); + if (flags != null) { + for (E flag : flags) { + if (flag != null) { + enumSet.add(flag); + } + } + } + return this; + } + + /** + * @param flags The flags to set in the builder. + * @return this builder instance. + */ + @SafeVarargs + public final Builder withFlags(final E... flags) { + clear(); + if (flags != null) { + for (E flag : flags) { + if (flag != null) { + if (!type.equals(flag.getClass())) { + throw new IllegalArgumentException("Unexpected type " + flag.getClass()); + } + enumSet.add(flag); + } + } + } + return this; + } + + /** + * Sets a single flag in the builder. + * + * @param flag The flag to set in the builder. + * @return this builder instance. + */ + public Builder setFlag(final E flag) { + if (flag != null) { + enumSet.add(flag); + } + return this; + } + + /** + * Sets multiple flag in the builder. + * + * @param flags The flags to set in the builder. + * @return this builder instance. + */ + public Builder setFlags(final Collection flags) { + if (flags != null) { + enumSet.addAll(flags); + } + return this; + } + + /** + * Clears any flags already set in this {@link Builder} + * + * @return this builder instance. + */ + public Builder clear() { + enumSet.clear(); + return this; + } + + /** + * Build the {@link DbiFlagSet} + * + * @return A + */ + public S build() { + final int size = enumSet.size(); + if (size == 0) { + return emptySetSupplier.get(); + } else if (size == 1) { + return singletonSetConstructor.apply(enumSet.stream().findFirst().get()); + } else { + return constructor.apply(enumSet); + } + } + } +} diff --git a/src/main/java/org/lmdbjava/BufferProxy.java b/src/main/java/org/lmdbjava/BufferProxy.java index d4503731..f857ade7 100644 --- a/src/main/java/org/lmdbjava/BufferProxy.java +++ b/src/main/java/org/lmdbjava/BufferProxy.java @@ -16,10 +16,6 @@ package org.lmdbjava; import static java.lang.Long.BYTES; -import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; -import static org.lmdbjava.DbiFlags.MDB_UNSIGNEDKEY; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import java.util.Comparator; import jnr.ffi.Pointer; @@ -44,6 +40,7 @@ public abstract class BufferProxy { /** 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() {} @@ -75,30 +72,22 @@ protected BufferProxy() {} *

The provided comparator must strictly match the lexicographical order of keys in the native * LMDB database. * - * @param flags for the database + * @param dbiFlagSet The {@link DbiFlags} set for the database. * @return a comparator that can be used (never null) */ - protected Comparator getComparator(DbiFlags... flags) { - final int intFlag = mask(flags); - - return isSet(intFlag, MDB_INTEGERKEY) || isSet(intFlag, MDB_UNSIGNEDKEY) - ? getUnsignedComparator() - : getSignedComparator(); - } + public abstract Comparator getComparator(final DbiFlagSet dbiFlagSet); /** - * Get a suitable default {@link Comparator} to compare numeric key values as signed. + * Get a suitable default {@link Comparator} * - * @return a comparator that can be used (never null) - */ - protected abstract Comparator getSignedComparator(); - - /** - * Get a suitable default {@link Comparator} to compare numeric key values as unsigned. + *

The provided comparator must strictly match the lexicographical order of keys in the native + * LMDB database. * * @return a comparator that can be used (never null) */ - protected abstract Comparator getUnsignedComparator(); + public Comparator getComparator() { + return getComparator(DbiFlagSet.empty()); + } /** * Called when the MDB_val should be set to reflect the passed buffer. This buffer @@ -138,4 +127,13 @@ protected Comparator getComparator(DbiFlags... flags) { final KeyVal keyVal() { return new KeyVal<>(this); } + + /** + * Create a new {@link Key} to hold pointers for this buffer proxy. + * + * @return a non-null key holder + */ + final Key key() { + return new Key<>(this); + } } diff --git a/src/main/java/org/lmdbjava/ByteArrayProxy.java b/src/main/java/org/lmdbjava/ByteArrayProxy.java index 853521e0..82b7721c 100644 --- a/src/main/java/org/lmdbjava/ByteArrayProxy.java +++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java @@ -36,9 +36,6 @@ public final class ByteArrayProxy extends BufferProxy { private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); - private static final Comparator signedComparator = ByteArrayProxy::compareArraysSigned; - private static final Comparator unsignedComparator = ByteArrayProxy::compareArrays; - private ByteArrayProxy() {} /** @@ -48,7 +45,7 @@ private ByteArrayProxy() {} * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - public static int compareArrays(final byte[] o1, final byte[] o2) { + public static int compareLexicographically(final byte[] o1, final byte[] o2) { requireNonNull(o1); requireNonNull(o2); if (o1 == o2) { @@ -68,26 +65,6 @@ public static int compareArrays(final byte[] o1, final byte[] o2) { return o1.length - o2.length; } - /** - * Compare two byte arrays. - * - * @param b1 left operand (required) - * @param b2 right operand (required) - * @return as specified by {@link Comparable} interface - */ - public static int compareArraysSigned(final byte[] b1, final byte[] b2) { - requireNonNull(b1); - requireNonNull(b2); - - if (b1 == b2) return 0; - - for (int i = 0; i < min(b1.length, b2.length); ++i) { - if (b1[i] != b2[i]) return b1[i] - b2[i]; - } - - return b1.length - b2.length; - } - @Override protected byte[] allocate() { return new byte[0]; @@ -104,13 +81,8 @@ protected byte[] getBytes(final byte[] buffer) { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + return ByteArrayProxy::compareLexicographically; } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java index 2866e874..19d94392 100644 --- a/src/main/java/org/lmdbjava/ByteBufProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufProxy.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import java.lang.reflect.Field; +import java.nio.ByteOrder; import java.util.Comparator; import jnr.ffi.Pointer; @@ -44,13 +45,6 @@ public final class ByteBufProxy extends BufferProxy { private static final String FIELD_NAME_ADDRESS = "memoryAddress"; private static final String FIELD_NAME_LENGTH = "length"; private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf"; - private static final Comparator comparator = - (o1, o2) -> { - requireNonNull(o1); - requireNonNull(o2); - - return o1.compareTo(o2); - }; private final long lengthOffset; private final long addressOffset; @@ -81,6 +75,66 @@ public ByteBufProxy(final PooledByteBufAllocator allocator) { } } + /** + * Lexicographically compare two buffers. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareLexicographically(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + return o1.compareTo(o2); + } + + /** + * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, + * i.e. when using MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same length according to LMDB API. + // From the LMDB docs for MDB_INTEGER_KEY + // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the same size. + final int len1 = o1.readableBytes(); + final int len2 = o2.readableBytes(); + if (len1 != len2) { + throw new RuntimeException("Length mismatch, len1: " + len1 + ", len2: " + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + if (len1 == 8) { + final long lw; + final long rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readLongLE(); + rw = o2.readLongLE(); + } else { + lw = o1.readLong(); + rw = o2.readLong(); + } + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw; + final int rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readIntLE(); + rw = o2.readIntLE(); + } else { + lw = o1.readInt(); + rw = o2.readInt(); + } + return Integer.compareUnsigned(lw, rw); + } else { + return compareLexicographically(o1, o2); + } + } + static Field findField(final String c, final String name) { Class clazz; try { @@ -114,13 +168,12 @@ protected ByteBuf allocate() { } @Override - protected Comparator getSignedComparator() { - return comparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return comparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return ByteBufProxy::compareAsIntegerKeys; + } else { + return ByteBufProxy::compareLexicographically; + } } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java index 3c80b995..1f82e953 100644 --- a/src/main/java/org/lmdbjava/ByteBufferProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java @@ -27,6 +27,7 @@ import java.lang.reflect.Field; import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.Comparator; import jnr.ffi.Pointer; @@ -54,15 +55,20 @@ public final class ByteBufferProxy { */ 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 { @@ -72,17 +78,25 @@ 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. @@ -92,16 +106,6 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { protected static final String FIELD_NAME_ADDRESS = "address"; protected static final String FIELD_NAME_CAPACITY = "capacity"; - private static final Comparator signedComparator = - (o1, o2) -> { - requireNonNull(o1); - requireNonNull(o2); - - return o1.compareTo(o2); - }; - private static final Comparator unsignedComparator = - AbstractByteBufferProxy::compareBuff; - /** * A thread-safe pool for a given length. If the buffer found is valid (ie not of a negative * length) then that buffer is used. If no valid buffer is found, a new buffer is created. @@ -116,7 +120,7 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) { + public static int compareLexicographically(final ByteBuffer o1, final ByteBuffer o2) { requireNonNull(o1); requireNonNull(o2); final int minLength = Math.min(o1.limit(), o2.limit()); @@ -145,6 +149,50 @@ 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 { @@ -179,13 +227,12 @@ protected final ByteBuffer allocate() { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return AbstractByteBufferProxy::compareAsIntegerKeys; + } else { + return AbstractByteBufferProxy::compareLexicographically; + } } @Override @@ -203,6 +250,10 @@ protected byte[] getBytes(final ByteBuffer buffer) { } } + + // -------------------------------------------------------------------------------- + + /** * A proxy that uses Java reflection to modify byte buffer fields, and official JNR-FFF methods to * manipulate native pointers. @@ -247,6 +298,10 @@ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr) { } } + + // -------------------------------------------------------------------------------- + + /** * A proxy that uses Java's "unsafe" class to directly manipulate byte buffer fields and JNR-FFF * allocated memory pointers. diff --git a/src/main/java/org/lmdbjava/CopyFlagSet.java b/src/main/java/org/lmdbjava/CopyFlagSet.java new file mode 100644 index 00000000..5f7901de --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSet.java @@ -0,0 +1,74 @@ +/* + * 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.EnumSet; +import java.util.Objects; + +public interface CopyFlagSet extends FlagSet { + + static CopyFlagSet EMPTY = CopyFlagSetImpl.EMPTY; + + static CopyFlagSet empty() { + return CopyFlagSetImpl.EMPTY; + } + + static CopyFlagSet of(final CopyFlags dbiFlag) { + Objects.requireNonNull(dbiFlag); + return dbiFlag; + } + + static CopyFlagSet of(final CopyFlags... CopyFlags) { + return builder() + .withFlags(CopyFlags) + .build(); + } + + static CopyFlagSet of(final Collection CopyFlags) { + return builder() + .withFlags(CopyFlags) + .build(); + } + + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + CopyFlags.class, + CopyFlagSetImpl::new, + copyFlag -> copyFlag, + () -> CopyFlagSetImpl.EMPTY); + } + + + // -------------------------------------------------------------------------------- + + + class CopyFlagSetImpl extends AbstractFlagSet implements CopyFlagSet { + + static final CopyFlagSet EMPTY = new EmptyCopyFlagSet(); + + private CopyFlagSetImpl(final EnumSet flags) { + super(flags); + } + } + + + // -------------------------------------------------------------------------------- + + + class EmptyCopyFlagSet extends AbstractFlagSet.AbstractEmptyFlagSet implements CopyFlagSet { + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlags.java b/src/main/java/org/lmdbjava/CopyFlags.java index 4365563c..b45dc87c 100644 --- a/src/main/java/org/lmdbjava/CopyFlags.java +++ b/src/main/java/org/lmdbjava/CopyFlags.java @@ -15,8 +15,12 @@ */ package org.lmdbjava; -/** Flags for use when performing a {@link Env#copy(java.io.File, org.lmdbjava.CopyFlags...)}. */ -public enum CopyFlags implements MaskedFlag { +import java.io.File; +import java.util.EnumSet; +import java.util.Set; + +/** Flags for use when performing a {@link Env#copy(File, CopyFlagSet)}. */ +public enum CopyFlags implements MaskedFlag, CopyFlagSet { /** Compacting copy: Omit free space from copy, and renumber all pages sequentially. */ MDB_CP_COMPACT(0x01); @@ -31,4 +35,29 @@ public enum CopyFlags implements MaskedFlag { public int getMask() { return mask; } + + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final CopyFlags flag) { + return flag != null && mask == flag.getMask(); + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index f7fcbc41..127fffc0 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -20,8 +20,6 @@ import static org.lmdbjava.Dbi.KeyNotFoundException.MDB_NOTFOUND; import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; @@ -97,23 +95,49 @@ public long count() { checkRc(LIB.mdb_cursor_count(ptrCursor, longByReference)); 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)); + } /** + * @deprecated Instead use {@link Cursor#delete(PutFlagSet)}. + *


* 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. * - * @param f flags (either null or {@link PutFlags#MDB_NODUPDATA} + *

This function deletes the key/data pair to which the cursor refers. + * + * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA} */ - public void delete(final PutFlags... f) { + public void delete(final PutFlagSet flags) { if (SHOULD_CHECK) { env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } - final int flags = mask(true, f); - checkRc(LIB.mdb_cursor_del(ptrCursor, flags)); + final PutFlagSet putFlagSet = flags != null + ? flags + : PutFlagSet.EMPTY; + checkRc(LIB.mdb_cursor_del(ptrCursor, putFlagSet.getMask())); } /** @@ -203,6 +227,10 @@ public T key() { return kv.key(); } + KeyVal keyVal() { + return kv; + } + /** * Position at last key/data item. * @@ -231,17 +259,49 @@ public boolean 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 op options for this operation + * @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 PutFlags... op) { + @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. + * + * @param key key to store + * @param val data to store + * @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) { + 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); @@ -252,12 +312,14 @@ public boolean put(final T key, final T val, final PutFlags... op) { } final Pointer transientKey = kv.keyIn(key); final Pointer transientVal = kv.valIn(val); - final int mask = mask(true, op); - final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), mask); + final PutFlagSet putFlagSet = flags != null + ? flags + : PutFlagSet.EMPTY; + final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), putFlagSet.getMask()); if (rc == MDB_KEYEXIST) { - if (isSet(mask, MDB_NOOVERWRITE)) { + if (putFlagSet.isSet(MDB_NOOVERWRITE)) { kv.valOut(); // marked as in,out in LMDB C docs - } else if (!isSet(mask, MDB_NODUPDATA)) { + } else if (!putFlagSet.isSet(MDB_NODUPDATA)) { checkRc(rc); } return false; @@ -271,6 +333,8 @@ public boolean put(final T key, final T val, final PutFlags... op) { } /** + * @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 @@ -281,9 +345,44 @@ public boolean put(final T key, final T val, final PutFlags... op) { * @param key key to store in the database (not null) * @param val value to store in the database (not null) * @param elements number of elements contained in the passed value buffer - * @param op options for operation (must set MDB_MULTIPLE) + * @param flags options for operation (must set MDB_MULTIPLE) */ - public void putMultiple(final T key, final T val, final int elements, final PutFlags... op) { + @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. + * + * @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) + * Either a {@link PutFlagSet} or a single {@link PutFlags}. + */ + public void putMultiple(final T key, final T val, final int elements, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); @@ -292,13 +391,15 @@ public void putMultiple(final T key, final T val, final int elements, final PutF txn.checkReady(); txn.checkWritesAllowed(); } - final int mask = mask(true, op); - if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) { + final PutFlagSet putFlagSet = flags != null + ? flags + : PutFlagSet.EMPTY; + if (SHOULD_CHECK && !putFlagSet.isSet(MDB_MULTIPLE)) { throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); } final Pointer transientKey = txn.kv().keyIn(key); final Pointer dataPtr = txn.kv().valInMulti(val, elements); - final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, mask); + final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, putFlagSet.getMask()); checkRc(rc); ReferenceUtil.reachabilityFence0(transientKey); ReferenceUtil.reachabilityFence0(dataPtr); @@ -330,6 +431,8 @@ public void renew(final Txn newTxn) { } /** + * @deprecated Use {@link Cursor#reserve(Object, int, PutFlagSet)} instead. + *


* Reserve space for data of the given size, but don't copy the given val. Instead, return a * pointer to the reserved space, which the caller can fill in later - before the next update * operation or the transaction ends. This saves an extra memcpy if the data is being generated @@ -340,10 +443,46 @@ public void renew(final Txn newTxn) { * * @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 + */ + @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 + * + * @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 */ - 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); env.checkNotClosed(); @@ -353,8 +492,12 @@ public T reserve(final T key, final int size, final PutFlags... op) { } final Pointer transientKey = kv.keyIn(key); final Pointer transientVal = kv.valIn(size); - final int flags = mask(true, op) | MDB_RESERVE.getMask(); - checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags)); + final PutFlagSet putFlagSet = flags != null + ? flags + : PutFlagSet.EMPTY; + // This is inconsistent with putMultiple which require MDB_MULTIPLE to be in the set. + final int flagsMask = putFlagSet.getMaskWith(MDB_RESERVE); + checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flagsMask)); kv.valOut(); ReferenceUtil.reachabilityFence0(transientKey); ReferenceUtil.reachabilityFence0(transientVal); diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index 6a03bd90..69d43fcd 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -21,10 +21,14 @@ import static org.lmdbjava.CursorIterable.State.REQUIRES_NEXT_OP; import static org.lmdbjava.CursorIterable.State.TERMINATED; import static org.lmdbjava.GetOp.MDB_SET_RANGE; +import static org.lmdbjava.Library.LIB; import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Supplier; +import jnr.ffi.Pointer; import org.lmdbjava.KeyRangeType.CursorOp; import org.lmdbjava.KeyRangeType.IteratorOp; @@ -38,7 +42,7 @@ */ public final class CursorIterable implements Iterable>, AutoCloseable { - private final Comparator comparator; + private final RangeComparator rangeComparator; private final Cursor cursor; private final KeyVal entry; private boolean iteratorReturned; @@ -46,16 +50,32 @@ public final class CursorIterable implements Iterable txn, final Dbi dbi, final KeyRange range, final Comparator comparator) { + final Txn txn, + final Dbi dbi, + final KeyRange range, + final Comparator comparator, + final BufferProxy proxy) { this.cursor = dbi.openCursor(txn); this.range = range; - this.comparator = comparator; this.entry = new KeyVal<>(); + + if (comparator != null) { + // User supplied Java-side comparator so use that + this.rangeComparator = new JavaRangeComparator<>(range, comparator, entry::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); + } } /** @@ -129,8 +149,7 @@ private void executeCursorOp(final CursorOp op) { } private void executeIteratorOp() { - final IteratorOp op = - range.getType().iteratorOp(range.getStart(), range.getStop(), entry.key(), comparator); + final IteratorOp op = range.getType().iteratorOp(entry.key(), rangeComparator); switch (op) { case CALL_NEXT_OP: executeCursorOp(range.getType().nextOp()); @@ -219,4 +238,108 @@ enum State { RELEASED, TERMINATED } + + + // -------------------------------------------------------------------------------- + + + static class JavaRangeComparator implements RangeComparator { + + private final Comparator comparator; + private final Supplier currentKeySupplier; + private final T start; + private final T stop; + + JavaRangeComparator( + final KeyRange range, + final Comparator comparator, + final Supplier currentKeySupplier) { + this.comparator = comparator; + this.currentKeySupplier = currentKeySupplier; + this.start = range.getStart(); + this.stop = range.getStop(); + } + + @Override + public int compareToStartKey() { + return comparator.compare(currentKeySupplier.get(), start); + } + + @Override + public int compareToStopKey() { + return comparator.compare(currentKeySupplier.get(), stop); + } + + @Override + public void close() throws Exception { + // Nothing to close + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * Calls down to mdb_cmp to make use of the comparator that LMDB uses for insertion order. Has a + * very slight overhead as compared to {@link JavaRangeComparator}. + */ + private static class LmdbRangeComparator implements RangeComparator { + + private final Pointer txnPointer; + private final Pointer dbiPointer; + private final Pointer cursorKeyPointer; + private final Key startKey; + private final Key stopKey; + private final Pointer startKeyPointer; + private final Pointer stopKeyPointer; + + public LmdbRangeComparator( + final Txn txn, + final Dbi dbi, + final Cursor cursor, + final KeyRange range, + final BufferProxy proxy) { + txnPointer = Objects.requireNonNull(txn).pointer(); + dbiPointer = Objects.requireNonNull(dbi).pointer(); + cursorKeyPointer = Objects.requireNonNull(cursor).keyVal().pointerKey(); + // Allocate buffers for use with the start/stop keys if required. + // Saves us copying bytes on each comparison + Objects.requireNonNull(range); + startKey = createKey(range.getStart(), proxy); + stopKey = createKey(range.getStop(), proxy); + startKeyPointer = startKey != null ? startKey.pointer() : null; + stopKeyPointer = stopKey != null ? stopKey.pointer() : null; + } + + @Override + public int compareToStartKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, startKeyPointer); + } + + @Override + public int compareToStopKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, stopKeyPointer); + } + + @Override + public void close() { + if (startKey != null) { + startKey.close(); + } + if (stopKey != null) { + stopKey.close(); + } + } + + private Key createKey(final T keyBuffer, final BufferProxy proxy) { + if (keyBuffer != null) { + final Key key = proxy.key(); + key.keyIn(keyBuffer); + return key; + } else { + return null; + } + } + } } diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index 5449c172..5e8fa2f2 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -31,6 +31,7 @@ import static org.lmdbjava.PutFlags.MDB_RESERVE; import static org.lmdbjava.ResultCodeMapper.checkRc; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -48,12 +49,15 @@ */ public final class Dbi { - private final ComparatorCallback ccb; + private final ComparatorCallback callbackComparator; private boolean cleaned; + // Used for CursorIterable KeyRange testing and/or native callbacks private final Comparator comparator; private final Env env; private final byte[] name; private final Pointer ptr; + private final BufferProxy proxy; + private final DbiFlagSet dbiFlagSet; Dbi( final Env env, @@ -62,38 +66,53 @@ public final class Dbi { final Comparator comparator, final boolean nativeCb, final BufferProxy proxy, - final DbiFlags... flags) { + final DbiFlagSet dbiFlagSet) { if (SHOULD_CHECK) { requireNonNull(txn); txn.checkReady(); } this.env = env; this.name = name == null ? null : Arrays.copyOf(name, name.length); - if (comparator == null) { - this.comparator = proxy.getComparator(flags); - } else { - this.comparator = comparator; - } - final int flagsMask = mask(true, flags); + this.proxy = proxy; + this.comparator = comparator; + this.dbiFlagSet = dbiFlagSet; final Pointer dbiPtr = allocateDirect(RUNTIME, ADDRESS); - checkRc(LIB.mdb_dbi_open(txn.pointer(), name, flagsMask, dbiPtr)); + checkRc(LIB.mdb_dbi_open(txn.pointer(), name, dbiFlagSet.getMask(), dbiPtr)); ptr = dbiPtr.getPointer(0); if (nativeCb) { - this.ccb = - (keyA, keyB) -> { - final T compKeyA = proxy.out(proxy.allocate(), keyA); - final T compKeyB = proxy.out(proxy.allocate(), keyB); - final int result = this.comparator.compare(compKeyA, compKeyB); - proxy.deallocate(compKeyA); - proxy.deallocate(compKeyB); - return result; - }; - LIB.mdb_set_compare(txn.pointer(), ptr, ccb); + requireNonNull(comparator, "comparator cannot be null if nativeCb is set"); + // LMDB will call back to this comparator for insertion/iteration order +// if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { +// this.callbackComparator = +// (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; +// }; +// } else { + this.callbackComparator = + (keyA, keyB) -> { + final T compKeyA = proxy.out(proxy.allocate(), keyA); + final T compKeyB = proxy.out(proxy.allocate(), keyB); + final int result = this.comparator.compare(compKeyA, compKeyB); + proxy.deallocate(compKeyA); + proxy.deallocate(compKeyB); + return result; + }; +// } + LIB.mdb_set_compare(txn.pointer(), ptr, callbackComparator); } else { - ccb = null; + callbackComparator = null; } } + Pointer pointer() { + return ptr; + } + /** * Close the database handle (normally unnecessary; use with caution). * @@ -254,6 +273,31 @@ public byte[] getName() { return name == null ? null : Arrays.copyOf(name, name.length); } + public String getNameAsString() { + return getNameAsString(Env.DEFAULT_NAME_CHARSET); + } + + + /** + * Obtains the name of this database, using the supplied {@link Charset}. + * + * @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) { + if (name == null) { + return ""; + } else { + // Assume a UTF8 encoding as we don't know, thus swallow if it fails + try { + return new String(name, requireNonNull(charset)); + } catch (Exception e) { + throw new RuntimeException("Unable to decode database name using charset " + charset); + } + } + } + /** * Iterate the database from the first item and forwards. * @@ -278,7 +322,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) { env.checkNotClosed(); txn.checkReady(); } - return new CursorIterable<>(txn, this, range, comparator); + return new CursorIterable<>(txn, this, range, comparator, proxy); } /** @@ -288,6 +332,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) { * @return the list of flags this Dbi was created with */ public List listFlags(final Txn txn) { + // TODO we could just return what is in dbiFlagSet, rather than hitting LMDB. if (SHOULD_CHECK) { env.checkNotClosed(); } @@ -337,18 +382,22 @@ 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(); } } /** + * @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}). @@ -360,7 +409,40 @@ public void put(final T key, final T val) { * @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 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}). + * + * @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 PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); @@ -369,15 +451,14 @@ public boolean put(final Txn txn, final T key, final T val, final PutFlags... txn.checkReady(); txn.checkWritesAllowed(); } + final PutFlagSet flagSet = flags != null ? flags : PutFlagSet.empty(); final Pointer transientKey = txn.kv().keyIn(key); final Pointer transientVal = txn.kv().valIn(val); - final int mask = mask(true, flags); - final int rc = - LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), mask); + final int rc = LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flagSet.getMask()); if (rc == MDB_KEYEXIST) { - if (isSet(mask, MDB_NOOVERWRITE)) { + if (flagSet.isSet(MDB_NOOVERWRITE)) { txn.kv().valOut(); // marked as in,out in LMDB C docs - } else if (!isSet(mask, MDB_NODUPDATA)) { + } else if (!flagSet.isSet(MDB_NODUPDATA)) { checkRc(rc); } return false; @@ -415,7 +496,7 @@ public T reserve(final Txn txn, final T key, final int size, final PutFlags.. } final Pointer transientKey = txn.kv().keyIn(key); final Pointer transientVal = txn.kv().valIn(size); - final int flags = mask(true, op) | MDB_RESERVE.getMask(); + final int flags = mask(op) | MDB_RESERVE.getMask(); checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags)); txn.kv().valOut(); // marked as in,out in LMDB C docs ReferenceUtil.reachabilityFence0(transientKey); @@ -454,6 +535,20 @@ private void clean() { cleaned = true; } + @Override + public String toString() { + String name; + try { + name = getNameAsString(); + } catch (Exception e) { + name = "?"; + } + return "Dbi{" + + "name='" + name + + "', dbiFlagSet=" + dbiFlagSet + + '}'; + } + /** The specified DBI was changed unexpectedly. */ public static final class BadDbiException extends LmdbNativeException { diff --git a/src/main/java/org/lmdbjava/DbiBuilder.java b/src/main/java/org/lmdbjava/DbiBuilder.java new file mode 100644 index 00000000..9cb85616 --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiBuilder.java @@ -0,0 +1,488 @@ +/* + * 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 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 DbiBuilderStage2 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 DbiBuilderStage2 setDbName(final byte[] name) { + // Null name is allowed so no null check + this.name = name; + return new DbiBuilderStage2<>(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 DbiBuilderStage2 withoutDbName() { + return setDbName((byte[]) null); + } + + + // -------------------------------------------------------------------------------- + + + /** + * Intermediate builder stage for constructing a {@link Dbi}. + * + * @param buffer type + */ + public static class DbiBuilderStage2 { + + private final DbiBuilder dbiBuilder; + + private ComparatorFactory comparatorFactory; + private ComparatorType comparatorType; + + private DbiBuilderStage2(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 DbiBuilderStage2#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 DbiBuilderStage2#withNativeComparator()}, + * {@link DbiBuilderStage2#withDefaultComparator()} or + * {@link DbiBuilderStage2#withIteratorComparator(ComparatorFactory)} as these comparators will + * never be used. + *

+ * + * @return The next builder stage. + */ + public DbiBuilderStage3 withDefaultComparator() { + this.comparatorType = ComparatorType.DEFAULT; + return new DbiBuilderStage3<>(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 DbiBuilderStage2#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 DbiBuilderStage2#withNativeComparator()}, + * {@link DbiBuilderStage2#withDefaultComparator()} or + * {@link DbiBuilderStage2#withIteratorComparator(ComparatorFactory)} as these comparators will + * never be used. + *

+ * + * @return The next builder stage. + */ + public DbiBuilderStage3 withNativeComparator() { + this.comparatorType = ComparatorType.NATIVE; + return new DbiBuilderStage3<>(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 DbiBuilderStage3 withCallbackComparator(final ComparatorFactory comparatorFactory) { + this.comparatorFactory = Objects.requireNonNull(comparatorFactory); + this.comparatorType = ComparatorType.CALLBACK; + return new DbiBuilderStage3<>(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 DbiBuilderStage2#withNativeComparator()}, + * {@link DbiBuilderStage2#withDefaultComparator()} or + * {@link DbiBuilderStage2#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 DbiBuilderStage3 withIteratorComparator(final ComparatorFactory comparatorFactory) { + this.comparatorFactory = Objects.requireNonNull(comparatorFactory); + this.comparatorType = ComparatorType.ITERATOR; + return new DbiBuilderStage3<>(this); + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * Final stage builder for constructing a {@link Dbi}. + * + * @param buffer type + */ + public static class DbiBuilderStage3 { + + private final DbiBuilderStage2 dbiBuilderStage2; + private final AbstractFlagSet.Builder flagSetBuilder = DbiFlagSet.builder(); + private Txn txn = null; + + private DbiBuilderStage3(DbiBuilderStage2 dbiBuilderStage2) { + this.dbiBuilderStage2 = dbiBuilderStage2; + } + + /** + *

+ * Apply all the dbi flags supplied in dbiFlags. + *

+ *

+ * Clears all flags currently set by previous calls to + * {@link DbiBuilderStage3#setDbiFlags(Collection)}, + * {@link DbiBuilderStage3#setDbiFlags(DbiFlags...)} + * or {@link DbiBuilderStage3#addDbiFlag(DbiFlags)}. + *

+ * + * @param dbiFlags to open the database with. + * A null {@link Collection} will just clear all set flags. + * Null items are ignored. + */ + public DbiBuilderStage3 setDbiFlags(final Collection dbiFlags) { + flagSetBuilder.clear(); + if (dbiFlags != null) { + dbiFlags.stream() + .filter(Objects::nonNull) + .forEach(dbiFlags::add); + } + return this; + } + + /** + *

+ * Apply all the dbi flags supplied in dbiFlags. + *

+ *

+ * Clears all flags currently set by previous calls to + * {@link DbiBuilderStage3#setDbiFlags(Collection)}, + * {@link DbiBuilderStage3#setDbiFlags(DbiFlags...)} + * or {@link DbiBuilderStage3#addDbiFlag(DbiFlags)}. + *

+ * + * @param dbiFlags to open the database with. + * A null array will just clear all set flags. + * Null items are ignored. + */ + public DbiBuilderStage3 setDbiFlags(final DbiFlags... dbiFlags) { + flagSetBuilder.clear(); + if (dbiFlags != null) { + Arrays.stream(dbiFlags) + .filter(Objects::nonNull) + .forEach(this.flagSetBuilder::setFlag); + } + return this; + } + + /** + *

+ * Apply all the dbi flags supplied in dbiFlags. + *

+ *

+ * Clears all flags currently set by previous calls to + * {@link DbiBuilderStage3#setDbiFlags(Collection)}, + * {@link DbiBuilderStage3#setDbiFlags(DbiFlags...)} + * or {@link DbiBuilderStage3#addDbiFlag(DbiFlags)}. + *

+ * + * @param dbiFlagSet to open the database with. + * A null value will just clear all set flags. + */ + public DbiBuilderStage3 setDbiFlags(final DbiFlagSet dbiFlagSet) { + flagSetBuilder.clear(); + if (dbiFlagSet != null) { + this.flagSetBuilder.withFlags(dbiFlagSet.getFlags()); + } + return this; + } + + /** + * Adds a dbiFlag to those flags already added to this builder by + * {@link DbiBuilderStage3#setDbiFlags(DbiFlags...)}, + * {@link DbiBuilderStage3#setDbiFlags(Collection)} + * or {@link DbiBuilderStage3#addDbiFlag(DbiFlags)}. + * + * @param dbiFlag to add to any existing flags. A null value is a no-op. + * @return this builder instance. + */ + public DbiBuilderStage3 addDbiFlag(final DbiFlags dbiFlag) { + this.flagSetBuilder.setFlag(dbiFlag); + return this; + } + + /** + * Adds a dbiFlag to those flags already added to this builder by + * {@link DbiBuilderStage3#setDbiFlags(DbiFlags...)}, + * {@link DbiBuilderStage3#setDbiFlags(Collection)} + * or {@link DbiBuilderStage3#addDbiFlag(DbiFlags)}. + * + * @param dbiFlagSet to add to any existing flags. A null value is a no-op. + * @return this builder instance. + */ + public DbiBuilderStage3 addDbiFlags(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet != null) { + flagSetBuilder.setFlags(dbiFlagSet.getFlags()); + } + return this; + } + + /** + * Use the supplied transaction to open the {@link Dbi}. + *

+ * The caller MUST commit the transaction after calling {@link DbiBuilderStage3#open()}, + * in order to retain the Dbi in the Env. + *

+ *

+ * 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 DbiBuilderStage3 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 = dbiBuilderStage2.dbiBuilder; + if (txn != null) { + return open(txn, dbiBuilder); + } else { + try (final Txn txn = getTxn(dbiBuilder)) { + final Dbi dbi = open(txn, dbiBuilder); + // even RO Txns require a commit to retain Dbi in Env + txn.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 = Objects.requireNonNull( + dbiBuilderStage2.comparatorFactory.create(dbiFlagSet), + () -> "comparatorFactory returned null"); + break; + case NATIVE: + break; + default: + throw new IllegalStateException("Unexpected comparatorType " + comparatorType); + } + return comparator; + } + + private Dbi open(final Txn txn, + final DbiBuilder dbiBuilder) { + final DbiFlagSet dbiFlagSet = flagSetBuilder.build(); + final ComparatorType comparatorType = dbiBuilderStage2.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, + ; + } + + + // -------------------------------------------------------------------------------- + + + @FunctionalInterface + public interface ComparatorFactory { + + 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..5a0bc83e --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiFlagSet.java @@ -0,0 +1,80 @@ +/* + * 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.EnumSet; +import java.util.Objects; + +public interface DbiFlagSet extends FlagSet { + + /** An immutable empty {@link DbiFlagSet}. */ + DbiFlagSet EMPTY = DbiFlagSetImpl.EMPTY; + + /** The set of {@link DbiFlags} that indicate unsigned integer keys are being used. */ + DbiFlagSet INTEGER_KEY_FLAGS = DbiFlagSet.of( + DbiFlags.MDB_INTEGERKEY, + DbiFlags.MDB_INTEGERDUP); + + static DbiFlagSet empty() { + return DbiFlagSetImpl.EMPTY; + } + + static DbiFlagSet of(final DbiFlags dbiFlag) { + Objects.requireNonNull(dbiFlag); + return dbiFlag; + } + + static DbiFlagSet of(final DbiFlags... DbiFlags) { + return builder() + .withFlags(DbiFlags) + .build(); + } + + static DbiFlagSet of(final Collection DbiFlags) { + return builder() + .withFlags(DbiFlags) + .build(); + } + + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + DbiFlags.class, + DbiFlagSetImpl::new, + dbiFlag -> dbiFlag, + () -> DbiFlagSetImpl.EMPTY); + } + + + // -------------------------------------------------------------------------------- + + + class DbiFlagSetImpl extends AbstractFlagSet implements DbiFlagSet { + + static final DbiFlagSet EMPTY = new EmptyDbiFlagSet(); + + private DbiFlagSetImpl(final EnumSet flags) { + super(flags); + } + } + + + // -------------------------------------------------------------------------------- + + + class EmptyDbiFlagSet extends AbstractFlagSet.AbstractEmptyFlagSet implements DbiFlagSet { + } +} diff --git a/src/main/java/org/lmdbjava/DbiFlags.java b/src/main/java/org/lmdbjava/DbiFlags.java index 123ec9fd..7c4b6794 100644 --- a/src/main/java/org/lmdbjava/DbiFlags.java +++ b/src/main/java/org/lmdbjava/DbiFlags.java @@ -15,8 +15,11 @@ */ package org.lmdbjava; +import java.util.EnumSet; +import java.util.Set; + /** Flags for use when opening a {@link Dbi}. */ -public enum DbiFlags implements MaskedFlag { +public enum DbiFlags implements MaskedFlag, DbiFlagSet { /** * Use reverse string keys. @@ -29,13 +32,30 @@ public enum DbiFlags implements MaskedFlag { * Use sorted duplicates. * *

Duplicate keys may be used in the database. Or, from another perspective, keys may have - * multiple data items, stored in sorted order. By default keys must be unique and may have only a + * multiple data items, stored in sorted order. By default, keys must be unique and may have only a * single data item. + *

+ * + *

*/ MDB_DUPSORT(0x04), /** - * Numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the - * same size. + * Numeric keys in native byte order: either unsigned int or size_t. + * The keys must all be of the same size. + *

+ * This is an optimisation that is available when your keys are 4 or 8 byte unsigned numeric values. + * There are performance benefits for both ordered and un-ordered puts as compared to not using + * this flag. + *

+ *

+ * When writing the key to the buffer you must write it in native order and subsequently read any + * keys retrieved from LMDB (via cursor or get method) also using native order. + *

+ *

+ * For more information, see + * Numeric Keys + * in the LmdbJava wiki. + *

*/ MDB_INTEGERKEY(0x08), /** @@ -55,14 +75,6 @@ public enum DbiFlags implements MaskedFlag { * #MDB_INTEGERKEY} keys. */ MDB_INTEGERDUP(0x20), - /** - * Compare the numeric keys in native byte order and as unsigned. - * - *

This option is applied only to {@link java.nio.ByteBuffer}, {@link org.agrona.DirectBuffer} - * and byte array keys. {@link io.netty.buffer.ByteBuf} keys are always compared in native byte - * order and as unsigned. - */ - MDB_UNSIGNEDKEY(0x30, false), /** * With {@link #MDB_DUPSORT}, use reverse string dups. * @@ -78,15 +90,9 @@ public enum DbiFlags implements MaskedFlag { MDB_CREATE(0x4_0000); private final int mask; - private final boolean propagatedToLmdb; - - DbiFlags(final int mask, final boolean propagatedToLmdb) { - this.mask = mask; - this.propagatedToLmdb = propagatedToLmdb; - } DbiFlags(final int mask) { - this(mask, true); + this.mask = mask; } @Override @@ -95,7 +101,27 @@ public int getMask() { } @Override - public boolean isPropagatedToLmdb() { - return propagatedToLmdb; + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final DbiFlags flag) { + return flag != null && mask == flag.getMask(); + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); } } diff --git a/src/main/java/org/lmdbjava/DirectBufferProxy.java b/src/main/java/org/lmdbjava/DirectBufferProxy.java index 524b81b8..3ddda467 100644 --- a/src/main/java/org/lmdbjava/DirectBufferProxy.java +++ b/src/main/java/org/lmdbjava/DirectBufferProxy.java @@ -22,6 +22,7 @@ import static org.lmdbjava.UnsafeAccess.UNSAFE; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.Comparator; import jnr.ffi.Pointer; @@ -35,14 +36,6 @@ *

This class requires {@link UnsafeAccess} and Agrona must be in the classpath. */ public final class DirectBufferProxy extends BufferProxy { - private static final Comparator signedComparator = - (o1, o2) -> { - requireNonNull(o1); - requireNonNull(o2); - - return o1.compareTo(o2); - }; - private static final Comparator unsignedComparator = DirectBufferProxy::compareBuff; /** * The {@link MutableDirectBuffer} proxy. Guaranteed to never be null, although a class @@ -58,6 +51,8 @@ public final class DirectBufferProxy extends BufferProxy { private static final ThreadLocal> BUFFERS = withInitial(() -> new ArrayDeque<>(16)); + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + private DirectBufferProxy() {} /** @@ -67,7 +62,7 @@ private DirectBufferProxy() {} * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - public static int compareBuff(final DirectBuffer o1, final DirectBuffer o2) { + public static int compareLexicographically(final DirectBuffer o1, final DirectBuffer o2) { requireNonNull(o1); requireNonNull(o2); @@ -95,6 +90,43 @@ public static int compareBuff(final DirectBuffer o1, final DirectBuffer o2) { return o1.capacity() - o2.capacity(); } + /** + * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, + * i.e. when using MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically. + *

+ * Both buffer must have 4 or 8 bytes remaining + *

+ * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final DirectBuffer o1, final DirectBuffer o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same len + final int len1 = o1.capacity(); + final int len2 = o2.capacity(); + if (len1 != len2) { + throw new RuntimeException("Length mismatch, len1: " + len1 + ", len2: " + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + if (len1 == 8) { + final long lw = o1.getLong(0, NATIVE_ORDER); + final long rw = o2.getLong(0, NATIVE_ORDER); + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw = o1.getInt(0, NATIVE_ORDER); + final int rw = o2.getInt(0, NATIVE_ORDER); + return Integer.compareUnsigned(lw, rw); + } else { + // size_t and int are likely to be 8bytes and 4bytes respectively on 64bit. + // If 32bit then would be 4/2 respectively. + // Short.compareUnsigned is not available in Java8. + // For now just fall back to our standard comparator + return compareLexicographically(o1, o2); + } + } + @Override protected DirectBuffer allocate() { final ArrayDeque q = BUFFERS.get(); @@ -109,13 +141,12 @@ protected DirectBuffer allocate() { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return DirectBufferProxy::compareAsIntegerKeys; + } else { + return DirectBufferProxy::compareLexicographically; + } } @Override diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 3db16119..b8003cbb 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -23,17 +23,20 @@ import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.Library.RUNTIME; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.ResultCodeMapper.checkRc; -import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; import java.io.File; import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; import jnr.ffi.Pointer; import jnr.ffi.byref.IntByReference; import jnr.ffi.byref.PointerByReference; @@ -47,8 +50,11 @@ */ public final class Env implements AutoCloseable { - /** Java system property name that can be set to disable optional checks. */ + /** + * Java system property name that can be set to disable optional checks. + */ public static final String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks"; + public static final Charset DEFAULT_NAME_CHARSET = StandardCharsets.UTF_8; /** * Indicates whether optional checks should be applied in LmdbJava. Optional checks are only @@ -89,7 +95,7 @@ public static Builder create() { /** * Create an {@link Env} using the passed {@link BufferProxy}. * - * @param buffer type + * @param buffer type * @param proxy the proxy to use (required) * @return the environment (never null) */ @@ -98,16 +104,21 @@ public static Builder create(final BufferProxy proxy) { } /** + * @deprecated Instead use {@link Env#create()} or {@link Env#create(BufferProxy)} + *

* Opens an environment with a single default database in 0664 mode using the {@link * ByteBufferProxy#PROXY_OPTIMAL}. * - * @param path file system destination - * @param size size in megabytes + * @param path file system destination + * @param size size in megabytes * @param flags the flags for this new environment * @return env the environment (never null) */ + @Deprecated public static Env open(final File path, final int size, final EnvFlags... flags) { - return new Builder<>(PROXY_OPTIMAL).setMapSize(size * 1_024L * 1_024L).open(path, flags); + return new Builder<>(PROXY_OPTIMAL) + .setMapSize(size * 1_024L * 1_024L) + .open(path, flags); } /** @@ -140,13 +151,33 @@ public void close() { * "Caveats" in the LMDB native documentation. * * @param path writable destination path as described above + */ + public void copy(final File path) { + copy(path, CopyFlagSet.EMPTY); + } + + /** + * Copies an LMDB environment to the specified destination path. + * + *

This function may be used to make a backup of an existing environment. No lockfile is + * created, since it gets recreated at need. + * + *

If this environment was created using {@link EnvFlags#MDB_NOSUBDIR}, the destination path + * must be a directory that exists but contains no files. If {@link EnvFlags#MDB_NOSUBDIR} was + * used, the destination path must not exist, but it must be possible to create a file at the + * provided path. + * + *

Note: This call can trigger significant file size growth if run in parallel with write + * transactions, because it employs a read-only transaction. See long-lived transactions under + * "Caveats" in the LMDB native documentation. + * + * @param path writable destination path as described above * @param flags special options for this copy */ - public void copy(final File path, final CopyFlags... flags) { + public void copy(final File path, final CopyFlagSet flags) { requireNonNull(path); validatePath(path); - final int flagsMask = mask(true, flags); - checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flagsMask)); + checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flags.getMask())); } /** @@ -164,7 +195,7 @@ public List getDbiNames() { final List result = new ArrayList<>(); final Dbi names = openDbi((byte[]) null); try (Txn txn = txnRead(); - Cursor cursor = names.openCursor(txn)) { + Cursor cursor = names.openCursor(txn)) { if (!cursor.first()) { return Collections.emptyList(); } @@ -242,91 +273,122 @@ public boolean isReadOnly() { } /** - * Convenience method that opens a {@link Dbi} with a UTF-8 database name and default {@link - * Comparator} that is not invoked from native code. + * Open (and optionally creates, if {@link DbiFlags#MDB_CREATE} is set) + * a {@link Dbi} using a builder. * - * @param name name of the database (or null if no name is required) + * @return A new builder instance for creating/opening a {@link Dbi}. + */ + public DbiBuilder buildDbi() { + return new DbiBuilder<>(this, proxy, readOnly); + } + + /** + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} + * Convenience method that opens a {@link Dbi} with a UTF-8 database name and default {@link + * Comparator} that is not invoked from native code. */ + @Deprecated() public Dbi openDbi(final String name, final DbiFlags... flags) { final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); return openDbi(nameBytes, null, false, flags); } /** + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator for cursor start/stop key comparisons. If null, LMDB's + * comparator will be used. + * @param flags to open the database with + * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link - * Comparator} that is not invoked from native code. + * Comparator} for use by {@link CursorIterable} when comparing start/stop keys. * - * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use default) - * @param flags to open the database with - * @return a database that is ready to use + *

It is very important that the passed comparator behaves in the same way as the comparator + * LMDB uses for its insertion order (for the type of data that will be stored in the database), + * or you fully understand the implications of them behaving differently. LMDB's comparator is + * unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is used. */ - public Dbi openDbi( - final String name, final Comparator comparator, final DbiFlags... flags) { + @Deprecated() + public Dbi openDbi(final String name, + final Comparator comparator, + final DbiFlags... flags) { final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); return openDbi(nameBytes, comparator, false, flags); } /** - * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link - * Comparator} that may be invoked from native code if specified. - * - * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use default) - * @param nativeCb whether native code calls back to the Java comparator - * @param flags to open the database with + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator for cursor start/stop key comparisons and optionally for + * LMDB to call back to. If null, LMDB's comparator will be used. + * @param nativeCb whether LMDB native code calls back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} + * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link + * Comparator}. The comparator will be used by {@link CursorIterable} when comparing start/stop + * keys as a minimum. If nativeCb is {@code true}, this comparator will also be called by LMDB to + * determine insertion/iteration order. Calling back to a java comparator may significantly impact + * performance. */ - public Dbi openDbi( - final String name, - final Comparator comparator, - final boolean nativeCb, - final DbiFlags... flags) { + @Deprecated() + public Dbi openDbi(final String name, + final Comparator comparator, + final boolean nativeCb, + final DbiFlags... flags) { final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); return openDbi(nameBytes, comparator, nativeCb, flags); } /** - * Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is not - * invoked from native code. - * - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} + *


+ * Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is not + * invoked from native code. */ - public Dbi openDbi(final byte[] name, final DbiFlags... flags) { + @Deprecated() + public Dbi openDbi(final byte[] name, + final DbiFlags... flags) { return openDbi(name, null, false, flags); } /** - * Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that is not - * invoked from native code. - * - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param flags to open the database with + * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} + *
+ * Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that is not + * invoked from native code. */ - public Dbi openDbi( - final byte[] name, final Comparator comparator, final DbiFlags... flags) { + @Deprecated() + public Dbi openDbi(final byte[] name, + final Comparator comparator, + final DbiFlags... flags) { return openDbi(name, comparator, false, flags); } /** + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator callback (or null to use LMDB default) + * @param nativeCb whether native code calls back to the Java comparator + * @param flags to open the database with + * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} + *
* Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that may be * invoked from native code if specified. * *

This method will automatically commit the private transaction before returning. This ensures * the Dbi is available in the Env. - * - * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native code calls back to the Java comparator - * @param flags to open the database with - * @return a database that is ready to use */ + @Deprecated() public Dbi openDbi( final byte[] name, final Comparator comparator, @@ -340,6 +402,13 @@ public Dbi openDbi( } /** + * @param txn transaction to use (required; not closed) + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator callback (or null to use LMDB default) + * @param nativeCb whether native LMDB code should call back to the Java comparator + * @param flags to open the database with + * @return a database that is ready to use + * @deprecated Instead use {@link Env#buildDbi()} * Open the {@link Dbi} using the passed {@link Txn}. * *

The caller must commit the transaction after this method returns in order to retain the @@ -358,21 +427,19 @@ public Dbi openDbi( * *

This method (and its overloaded convenience variants) must not be called from concurrent * threads. - * - * @param txn transaction to use (required; not closed) - * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native code should call back to the comparator - * @param flags to open the database with - * @return a database that is ready to use */ + @Deprecated() public Dbi openDbi( final Txn txn, final byte[] name, final Comparator comparator, final boolean nativeCb, final DbiFlags... flags) { - return new Dbi<>(this, txn, name, comparator, nativeCb, proxy, flags); + + if (nativeCb && comparator == null) { + throw new IllegalArgumentException("Is nativeCb is true, you must supply a comparator"); + } + return new Dbi<>(this, txn, name, comparator, nativeCb, proxy, DbiFlagSet.of(flags)); } /** @@ -399,7 +466,7 @@ public Stat stat() { * Flushes the data buffers to disk. * * @param force force a synchronous flush (otherwise if the environment has the MDB_NOSYNC flag - * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) + * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) */ public void sync(final boolean force) { if (closed) { @@ -409,17 +476,42 @@ public void sync(final boolean force) { checkRc(LIB.mdb_env_sync(ptr, f)); } + /** + * @param parent parent transaction (may be null if no parent) + * @param flags applicable flags (eg for a reusable, read-only transaction) + * @return a transaction (never null) + * @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)} + *

+ * Obtain a transaction with the requested parent and flags. + */ + @Deprecated + public Txn txn(final Txn parent, final TxnFlags... flags) { + checkNotClosed(); + return new Txn<>(this, parent, proxy, TxnFlagSet.of(flags)); + } + /** * Obtain a transaction with the requested parent and flags. * * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (eg for a reusable, read-only transaction) * @return a transaction (never null) */ - public Txn txn(final Txn parent, final TxnFlags... flags) { - if (closed) { - throw new AlreadyClosedException(); - } + public Txn txn(final Txn parent) { + checkNotClosed(); + return new Txn<>(this, parent, proxy, TxnFlagSet.EMPTY); + } + + /** + * Obtain a transaction with the requested parent and flags. + * + * @param parent parent transaction (may be null if no parent) + * @param flags applicable flags (e.g. for a reusable, read-only transaction). + * If the set of flags is used frequently it is recommended to hold + * a static instance of the {@link TxnFlagSet} for re-use. + * @return a transaction (never null) + */ + public Txn txn(final Txn parent, final TxnFlagSet flags) { + checkNotClosed(); return new Txn<>(this, parent, proxy, flags); } @@ -429,7 +521,8 @@ public Txn txn(final Txn parent, final TxnFlags... flags) { * @return a read-only transaction */ public Txn txnRead() { - return txn(null, MDB_RDONLY_TXN); + checkNotClosed(); + return new Txn<>(this, null, proxy, TxnFlags.MDB_RDONLY_TXN); } /** @@ -438,7 +531,8 @@ public Txn txnRead() { * @return a read-write transaction */ public Txn txnWrite() { - return txn(null); + checkNotClosed(); + return new Txn<>(this, null, proxy, TxnFlagSet.EMPTY); } Pointer pointer() { @@ -485,28 +579,40 @@ public int readerCheck() { return resultPtr.intValue(); } - /** Object has already been closed and the operation is therefore prohibited. */ + /** + * Object has already been closed and the operation is therefore prohibited. + */ public static final class AlreadyClosedException extends LmdbException { private static final long serialVersionUID = 1L; - /** Creates a new instance. */ + /** + * Creates a new instance. + */ public AlreadyClosedException() { super("Environment has already been closed"); } } - /** Object has already been opened and the operation is therefore prohibited. */ + /** + * Object has already been opened and the operation is therefore prohibited. + */ public static final class AlreadyOpenException extends LmdbException { private static final long serialVersionUID = 1L; - /** Creates a new instance. */ + /** + * Creates a new instance. + */ public AlreadyOpenException() { super("Environment has already been opened"); } } + + // -------------------------------------------------------------------------------- + + /** * Builder for configuring and opening Env. * @@ -520,6 +626,8 @@ public static final class Builder { private int maxReaders = MAX_READERS_DEFAULT; private boolean opened; private final BufferProxy proxy; + private int mode = 0664; + private AbstractFlagSet.Builder flagSetBuilder = EnvFlagSet.builder(); Builder(final BufferProxy proxy) { requireNonNull(proxy); @@ -529,12 +637,53 @@ public static final class Builder { /** * Opens the environment. * - * @param path file system destination - * @param mode Unix permissions to set on created files and semaphores + * @param path file system destination + * @param mode Unix permissions to set on created files and semaphores * @param flags the flags for this new environment * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)}, {@link Builder#setFilePermissions(int)} + * and {@link Builder#setEnvFlags(EnvFlags...)}. */ + @Deprecated public Env open(final File path, final int mode, final EnvFlags... flags) { + setFilePermissions(mode); + setEnvFlags(flags); + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment. + * + * @param path file system destination + * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)} + */ + @Deprecated + public Env open(final File path) { + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment with 0664 mode. + * + * @param path file system destination + * @param flags the flags for this new environment + * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)} and {@link Builder#setEnvFlags(EnvFlags...)}. + */ + @Deprecated + public Env open(final File path, final EnvFlags... flags) { + setEnvFlags(flags); + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment. + * + * @param path file system destination + * @return an environment ready for use + */ + public Env open(final Path path) { requireNonNull(path); if (opened) { throw new AlreadyOpenException(); @@ -547,10 +696,10 @@ public Env open(final File path, final int mode, final EnvFlags... flags) { checkRc(LIB.mdb_env_set_mapsize(ptr, mapSize)); checkRc(LIB.mdb_env_set_maxdbs(ptr, maxDbs)); checkRc(LIB.mdb_env_set_maxreaders(ptr, maxReaders)); - final int flagsMask = mask(true, flags); - final boolean readOnly = isSet(flagsMask, MDB_RDONLY_ENV); - final boolean noSubDir = isSet(flagsMask, MDB_NOSUBDIR); - checkRc(LIB.mdb_env_open(ptr, path.getAbsolutePath(), flagsMask, mode)); + final EnvFlagSet flags = flagSetBuilder.build(); + final boolean readOnly = flags.isSet(MDB_RDONLY_ENV); + final boolean noSubDir = flags.isSet(MDB_NOSUBDIR); + checkRc(LIB.mdb_env_open(ptr, path.toAbsolutePath().toString(), flags.getMask(), mode)); return new Env<>(proxy, ptr, readOnly, noSubDir); } catch (final LmdbNativeException e) { LIB.mdb_env_close(ptr); @@ -559,18 +708,7 @@ public Env open(final File path, final int mode, final EnvFlags... flags) { } /** - * Opens the environment with 0664 mode. - * - * @param path file system destination - * @param flags the flags for this new environment - * @return an environment ready for use - */ - public Env open(final File path, final EnvFlags... flags) { - return open(path, 0664, flags); - } - - /** - * Sets the map size. + * Sets the map size in bytes. * * @param mapSize new limit in bytes * @return the builder @@ -613,9 +751,104 @@ public Builder setMaxReaders(final int readers) { this.maxReaders = readers; return this; } + + /** + * Sets the Unix file permissions to use on created files and semaphores, e.g. {@code 0664}. + * If this method is not called, the default of {@code 0664} will be used. + * + * @param mode Unix permissions to set on created files and semaphores + * @return the builder + */ + public Builder setFilePermissions(final int mode) { + if (opened) { + throw new AlreadyOpenException(); + } + this.mode = mode; + return this; + } + + /** + * Sets all the flags used to open this {@link Env}. + * + * @param envFlags The flags to use. + * Clears any existing flags. + * A null value results in no flags being set. + * @return this builder instance. + */ + public Builder setEnvFlags(final Collection envFlags) { + flagSetBuilder.clear(); + if (envFlags != null) { + envFlags.stream() + .filter(Objects::nonNull) + .forEach(envFlags::add); + } + return this; + } + + /** + * Sets all the flags used to open this {@link Env}. + * + * @param envFlags The flags to use. + * Clears any existing flags. + * A null value results in no flags being set. + * @return this builder instance. + */ + public Builder setEnvFlags(final EnvFlags... envFlags) { + flagSetBuilder.clear(); + if (envFlags != null) { + Arrays.stream(envFlags) + .filter(Objects::nonNull) + .forEach(this.flagSetBuilder::setFlag); + } + return this; + } + + /** + * Sets all the flags used to open this {@link Env}. + * + * @param envFlagSet The flags to use. + * Clears any existing flags. + * A null value results in no flags being set. + * @return this builder instance. + */ + public Builder setEnvFlags(final EnvFlagSet envFlagSet) { + flagSetBuilder.clear(); + if (envFlagSet != null) { + this.flagSetBuilder.withFlags(envFlagSet.getFlags()); + } + return this; + } + + /** + * Adds a single {@link EnvFlags} to any existing flags. + * + * @param dbiFlag The flag to add to any existing flags. + * A null value is a no-op. + * @return this builder instance. + */ + public Builder addEnvFlag(final EnvFlags dbiFlag) { + this.flagSetBuilder.setFlag(dbiFlag); + return this; + } + + /** + * Adds a set of {@link EnvFlags} to any existing flags. + * + * @param dbiFlagSet The set of flags to add to any existing flags. + * A null value is a no-op. + * @return this builder instance. + */ + public Builder addEnvFlags(final EnvFlagSet dbiFlagSet) { + if (dbiFlagSet != null) { + flagSetBuilder.setFlags(dbiFlagSet.getFlags()); + } + return this; + } } - /** File is not a valid LMDB file. */ + /** + * File is not a valid LMDB file. + */ public static final class FileInvalidException extends LmdbNativeException { static final int MDB_INVALID = -30_793; @@ -626,7 +859,9 @@ public static final class FileInvalidException extends LmdbNativeException { } } - /** The specified copy destination is invalid. */ + /** + * The specified copy destination is invalid. + */ public static final class InvalidCopyDestination extends LmdbException { private static final long serialVersionUID = 1L; @@ -641,7 +876,9 @@ public InvalidCopyDestination(final String message) { } } - /** Environment mapsize reached. */ + /** + * Environment mapsize reached. + */ public static final class MapFullException extends LmdbNativeException { static final int MDB_MAP_FULL = -30_792; @@ -652,7 +889,9 @@ public static final class MapFullException extends LmdbNativeException { } } - /** Environment maxreaders reached. */ + /** + * Environment maxreaders reached. + */ public static final class ReadersFullException extends LmdbNativeException { static final int MDB_READERS_FULL = -30_790; @@ -663,7 +902,9 @@ public static final class ReadersFullException extends LmdbNativeException { } } - /** Environment version mismatch. */ + /** + * Environment version mismatch. + */ public static final class VersionMismatchException extends LmdbNativeException { static final int MDB_VERSION_MISMATCH = -30_794; diff --git a/src/main/java/org/lmdbjava/EnvFlagSet.java b/src/main/java/org/lmdbjava/EnvFlagSet.java new file mode 100644 index 00000000..a3c8d1fa --- /dev/null +++ b/src/main/java/org/lmdbjava/EnvFlagSet.java @@ -0,0 +1,74 @@ +/* + * 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.EnumSet; +import java.util.Objects; + +public interface EnvFlagSet extends FlagSet { + + EnvFlagSet EMPTY = EnvFlagSetImpl.EMPTY; + + static EnvFlagSet empty() { + return EnvFlagSetImpl.EMPTY; + } + + static EnvFlagSet of(final EnvFlags envFlag) { + Objects.requireNonNull(envFlag); + return envFlag; + } + + static EnvFlagSet of(final EnvFlags... EnvFlags) { + return builder() + .withFlags(EnvFlags) + .build(); + } + + static EnvFlagSet of(final Collection EnvFlags) { + return builder() + .withFlags(EnvFlags) + .build(); + } + + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + EnvFlags.class, + EnvFlagSetImpl::new, + envFlag -> envFlag, + () -> EnvFlagSetImpl.EMPTY); + } + + + // -------------------------------------------------------------------------------- + + + class EnvFlagSetImpl extends AbstractFlagSet implements EnvFlagSet { + + static final EnvFlagSet EMPTY = new EmptyEnvFlagSet(); + + private EnvFlagSetImpl(final EnumSet flags) { + super(flags); + } + } + + + // -------------------------------------------------------------------------------- + + + class EmptyEnvFlagSet extends AbstractFlagSet.AbstractEmptyFlagSet implements EnvFlagSet { + } +} diff --git a/src/main/java/org/lmdbjava/EnvFlags.java b/src/main/java/org/lmdbjava/EnvFlags.java index 4ce555a8..7fb4a29b 100644 --- a/src/main/java/org/lmdbjava/EnvFlags.java +++ b/src/main/java/org/lmdbjava/EnvFlags.java @@ -15,8 +15,11 @@ */ package org.lmdbjava; +import java.util.EnumSet; +import java.util.Set; + /** Flags for use when opening the {@link Env}. */ -public enum EnvFlags implements MaskedFlag { +public enum EnvFlags implements MaskedFlag, EnvFlagSet { /** * Mmap at a fixed address (experimental). @@ -144,4 +147,29 @@ public enum EnvFlags implements MaskedFlag { public int getMask() { return mask; } + + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final EnvFlags flag) { + return flag != null && mask == flag.getMask(); + } + + @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/FlagSet.java b/src/main/java/org/lmdbjava/FlagSet.java new file mode 100644 index 00000000..27513fcd --- /dev/null +++ b/src/main/java/org/lmdbjava/FlagSet.java @@ -0,0 +1,128 @@ +/* + * 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.Comparator; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A set of flags, each with a bit mask value. + * Flags can be combined in a set such that the set has a combined bit mask value. + * @param + */ +public interface FlagSet extends Iterable { + + /** + * @return The combined mask for this flagSet. + */ + int getMask(); + + /** + * @return The result of combining the mask of this {@link FlagSet} + * with the mask of the other {@link FlagSet}. + */ + default int getMaskWith(final FlagSet other) { + if (other != null) { + return MaskedFlag.mask(getMask(), other.getMask()); + } else { + return getMask(); + } + } + + /** + * @return The set of flags in this {@link FlagSet}. + */ + Set getFlags(); + + /** + * @return True if flag is non-null and included in this {@link FlagSet}. + */ + boolean isSet(T flag); + + /** + * @return True if at least one of flags are included in thie {@link FlagSet} + */ + default boolean areAnySet(final FlagSet flags) { + if (flags == null) { + return false; + } else { + for (final T flag : flags) { + if (isSet(flag)) { + return true; + } + } + } + return false; + } + + /** + * @return The size of this {@link FlagSet} + */ + default int size() { + return getFlags().size(); + } + + /** + * @return True if this {@link FlagSet} is empty. + */ + default boolean isEmpty() { + return getFlags().isEmpty(); + } + + /** + * @return The {@link Iterator} (in no particular order) for the flags in this {@link FlagSet}. + */ + default Iterator iterator() { + return getFlags().iterator(); + } + + /** + * Convert this {@link FlagSet} to a string for use in toString methods. + */ + static String asString(final FlagSet flagSet) { + Objects.requireNonNull(flagSet); + final String flagsStr = flagSet.getFlags() + .stream() + .sorted(Comparator.comparing(MaskedFlag::getMask)) + .map(MaskedFlag::name) + .collect(Collectors.joining(", ")); + return "FlagSet{" + + "flags=[" + flagsStr + + "], mask=" + flagSet.getMask() + + '}'; + } + + static boolean equals(final FlagSet flagSet, + final Object other) { + if (other instanceof FlagSet) { + final FlagSet flagSet2 = (FlagSet) other; + if (flagSet == flagSet2) { + return true; + } else if (flagSet == null) { + return false; + } else { + return flagSet.getMask() == flagSet2.getMask() + && Objects.equals(flagSet.getFlags(), flagSet2.getFlags()); + } + } else { + return false; + } + } + +} diff --git a/src/main/java/org/lmdbjava/Key.java b/src/main/java/org/lmdbjava/Key.java new file mode 100644 index 00000000..ac25f65d --- /dev/null +++ b/src/main/java/org/lmdbjava/Key.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static java.util.Objects.requireNonNull; +import static org.lmdbjava.BufferProxy.MDB_VAL_STRUCT_SIZE; +import static org.lmdbjava.Library.RUNTIME; + +import jnr.ffi.Pointer; +import jnr.ffi.provider.MemoryManager; + +/** + * Represents off-heap memory holding a key only. + * + * @param buffer type + */ +final class Key implements AutoCloseable { + + private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); + private boolean closed; + private T k; + private final BufferProxy proxy; + private final Pointer ptrKey; + private final long ptrKeyAddr; + + Key(final BufferProxy proxy) { + requireNonNull(proxy); + this.proxy = proxy; + this.k = proxy.allocate(); + ptrKey = MEM_MGR.allocateTemporary(MDB_VAL_STRUCT_SIZE, false); + ptrKeyAddr = ptrKey.address(); + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + proxy.deallocate(k); + } + + T key() { + return k; + } + + void keyIn(final T key) { + proxy.in(key, ptrKey); + } + + T keyOut() { + k = proxy.out(k, ptrKey); + return k; + } + + Pointer pointer() { + return ptrKey; + } +} diff --git a/src/main/java/org/lmdbjava/KeyRangeType.java b/src/main/java/org/lmdbjava/KeyRangeType.java index ad67286d..26f09636 100644 --- a/src/main/java/org/lmdbjava/KeyRangeType.java +++ b/src/main/java/org/lmdbjava/KeyRangeType.java @@ -319,15 +319,13 @@ CursorOp initialOp() { * * @param buffer type * @param comparator for the buffers - * @param start start buffer - * @param stop stop buffer * @param buffer current key returned by LMDB (may be null) - * @param c comparator (required) + * @param rangeComparator comparator (required) * @return response to this key */ > IteratorOp iteratorOp( - final T start, final T stop, final T buffer, final C c) { - requireNonNull(c, "Comparator required"); + final T buffer, final RangeComparator rangeComparator) { + requireNonNull(rangeComparator, "Comparator required"); if (buffer == null) { return TERMINATE; } @@ -337,55 +335,55 @@ > IteratorOp iteratorOp( case FORWARD_AT_LEAST: return RELEASE; case FORWARD_AT_MOST: - return c.compare(buffer, stop) > 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() > 0 ? TERMINATE : RELEASE; case FORWARD_CLOSED: - return c.compare(buffer, stop) > 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() > 0 ? TERMINATE : RELEASE; case FORWARD_CLOSED_OPEN: - return c.compare(buffer, stop) >= 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() >= 0 ? TERMINATE : RELEASE; case FORWARD_GREATER_THAN: - return c.compare(buffer, start) == 0 ? CALL_NEXT_OP : RELEASE; + return rangeComparator.compareToStartKey() == 0 ? CALL_NEXT_OP : RELEASE; case FORWARD_LESS_THAN: - return c.compare(buffer, stop) >= 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() >= 0 ? TERMINATE : RELEASE; case FORWARD_OPEN: - if (c.compare(buffer, start) == 0) { + if (rangeComparator.compareToStartKey() == 0) { return CALL_NEXT_OP; } - return c.compare(buffer, stop) >= 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() >= 0 ? TERMINATE : RELEASE; case FORWARD_OPEN_CLOSED: - if (c.compare(buffer, start) == 0) { + if (rangeComparator.compareToStartKey() == 0) { return CALL_NEXT_OP; } - return c.compare(buffer, stop) > 0 ? TERMINATE : RELEASE; + return rangeComparator.compareToStopKey() > 0 ? TERMINATE : RELEASE; case BACKWARD_ALL: return RELEASE; case BACKWARD_AT_LEAST: - return c.compare(buffer, start) > 0 ? CALL_NEXT_OP : RELEASE; // rewind + return rangeComparator.compareToStartKey() > 0 ? CALL_NEXT_OP : RELEASE; // rewind case BACKWARD_AT_MOST: - return c.compare(buffer, stop) >= 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() >= 0 ? RELEASE : TERMINATE; case BACKWARD_CLOSED: - if (c.compare(buffer, start) > 0) { + if (rangeComparator.compareToStartKey() > 0) { return CALL_NEXT_OP; // rewind } - return c.compare(buffer, stop) >= 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() >= 0 ? RELEASE : TERMINATE; case BACKWARD_CLOSED_OPEN: - if (c.compare(buffer, start) > 0) { + if (rangeComparator.compareToStartKey() > 0) { return CALL_NEXT_OP; // rewind } - return c.compare(buffer, stop) > 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() > 0 ? RELEASE : TERMINATE; case BACKWARD_GREATER_THAN: - return c.compare(buffer, start) >= 0 ? CALL_NEXT_OP : RELEASE; + return rangeComparator.compareToStartKey() >= 0 ? CALL_NEXT_OP : RELEASE; case BACKWARD_LESS_THAN: - return c.compare(buffer, stop) > 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() > 0 ? RELEASE : TERMINATE; case BACKWARD_OPEN: - if (c.compare(buffer, start) >= 0) { + if (rangeComparator.compareToStartKey() >= 0) { return CALL_NEXT_OP; // rewind } - return c.compare(buffer, stop) > 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() > 0 ? RELEASE : TERMINATE; case BACKWARD_OPEN_CLOSED: - if (c.compare(buffer, start) >= 0) { + if (rangeComparator.compareToStartKey() >= 0) { return CALL_NEXT_OP; // rewind } - return c.compare(buffer, stop) >= 0 ? RELEASE : TERMINATE; + return rangeComparator.compareToStopKey() >= 0 ? RELEASE : TERMINATE; default: throw new IllegalStateException("Invalid type"); } diff --git a/src/main/java/org/lmdbjava/Library.java b/src/main/java/org/lmdbjava/Library.java index ef9b9b35..6d8122d2 100644 --- a/src/main/java/org/lmdbjava/Library.java +++ b/src/main/java/org/lmdbjava/Library.java @@ -235,6 +235,8 @@ public interface Lmdb { void mdb_txn_reset(@In Pointer txn); + int mdb_cmp(@In Pointer txn, @In Pointer dbi, @In Pointer key1, @In Pointer key2); + Pointer mdb_version(IntByReference major, IntByReference minor, IntByReference patch); } } diff --git a/src/main/java/org/lmdbjava/MaskedFlag.java b/src/main/java/org/lmdbjava/MaskedFlag.java index 58d67d8c..1c531ac8 100644 --- a/src/main/java/org/lmdbjava/MaskedFlag.java +++ b/src/main/java/org/lmdbjava/MaskedFlag.java @@ -17,14 +17,13 @@ import static java.util.Objects.requireNonNull; -import java.util.Arrays; -import java.util.Objects; -import java.util.function.Predicate; -import java.util.stream.Stream; +import java.util.Collection; /** Indicates an enum that can provide integers for each of its values. */ public interface MaskedFlag { + int EMPTY_MASK = 0; + /** * Obtains the integer value for this enum which can be included in a mask. * @@ -33,13 +32,9 @@ public interface MaskedFlag { int getMask(); /** - * Indicates if the flag must be propagated to the underlying C code of LMDB or not. - * - * @return the boolean value indicating the propagation + * @return The name of the flag. */ - default boolean isPropagatedToLmdb() { - return true; - } + String name(); /** * Fetch the integer mask for all presented flags. @@ -50,67 +45,47 @@ default boolean isPropagatedToLmdb() { */ @SafeVarargs static int mask(final M... flags) { - return mask(false, flags); + if (flags == null || flags.length == 0) { + return EMPTY_MASK; + } else { + int result = EMPTY_MASK; + for (MaskedFlag flag : flags) { + if (flag == null) { + continue; + } + result |= flag.getMask(); + } + return result; + } } /** - * Fetch the integer mask for all presented flags. - * - * @param flag type - * @param flags to mask (null or empty returns zero) - * @return the integer mask for use in C + * Combine the two masks into a single mask value, i.e. when combining two {@link FlagSet}s. */ - static int mask(final Stream flags) { - return mask(false, flags); + static int mask(final int mask1, final int mask2) { + return mask1 | mask2; } /** * Fetch the integer mask for the presented flags. * * @param flag type - * @param onlyPropagatedToLmdb if to include only the flags which are also propagate to the C code - * or all of them * @param flags to mask (null or empty returns zero) * @return the integer mask for use in C */ - @SafeVarargs - static int mask(final boolean onlyPropagatedToLmdb, final M... flags) { - if (flags == null || flags.length == 0) { - return 0; - } - - int result = 0; - for (final M flag : flags) { - if (flag == null) { - continue; - } - if (!onlyPropagatedToLmdb || flag.isPropagatedToLmdb()) { + static int mask(final Collection flags) { + if (flags == null || flags.isEmpty()) { + return EMPTY_MASK; + } else { + int result = EMPTY_MASK; + for (MaskedFlag flag : flags) { + if (flag == null) { + continue; + } result |= flag.getMask(); } + return result; } - return result; - } - - /** - * Fetch the integer mask for all presented flags. - * - * @param flag type - * @param onlyPropagatedToLmdb if to include only the flags which are also propagate to the C code - * or all of them - * @param flags to mask - * @return the integer mask for use in C - */ - static int mask( - final boolean onlyPropagatedToLmdb, final Stream flags) { - final Predicate filter = onlyPropagatedToLmdb ? MaskedFlag::isPropagatedToLmdb : f -> true; - - return flags == null - ? 0 - : flags - .filter(Objects::nonNull) - .filter(filter) - .map(M::getMask) - .reduce(0, (f1, f2) -> f1 | f2); } /** diff --git a/src/main/java/org/lmdbjava/PutFlagSet.java b/src/main/java/org/lmdbjava/PutFlagSet.java new file mode 100644 index 00000000..9d3a7288 --- /dev/null +++ b/src/main/java/org/lmdbjava/PutFlagSet.java @@ -0,0 +1,74 @@ +/* + * 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.EnumSet; +import java.util.Objects; + +public interface PutFlagSet extends FlagSet { + + PutFlagSet EMPTY = PutFlagSetImpl.EMPTY; + + static PutFlagSet empty() { + return PutFlagSetImpl.EMPTY; + } + + static PutFlagSet of(final PutFlags putFlag) { + Objects.requireNonNull(putFlag); + return putFlag; + } + + static PutFlagSet of(final PutFlags... putFlags) { + return builder() + .withFlags(putFlags) + .build(); + } + + static PutFlagSet of(final Collection putFlags) { + return builder() + .withFlags(putFlags) + .build(); + } + + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + PutFlags.class, + PutFlagSetImpl::new, + putFlag -> putFlag, + EmptyPutFlagSet::new); + } + + + // -------------------------------------------------------------------------------- + + + class PutFlagSetImpl extends AbstractFlagSet implements PutFlagSet { + + public static final PutFlagSet EMPTY = new EmptyPutFlagSet(); + + private PutFlagSetImpl(final EnumSet flags) { + super(flags); + } + } + + + // -------------------------------------------------------------------------------- + + + class EmptyPutFlagSet extends AbstractFlagSet.AbstractEmptyFlagSet implements PutFlagSet { + } +} diff --git a/src/main/java/org/lmdbjava/PutFlags.java b/src/main/java/org/lmdbjava/PutFlags.java index 809103de..03fa916a 100644 --- a/src/main/java/org/lmdbjava/PutFlags.java +++ b/src/main/java/org/lmdbjava/PutFlags.java @@ -15,8 +15,11 @@ */ package org.lmdbjava; +import java.util.EnumSet; +import java.util.Set; + /** Flags for use when performing a "put". */ -public enum PutFlags implements MaskedFlag { +public enum PutFlags implements MaskedFlag, PutFlagSet { /** For put: Don't write if the key already exists. */ MDB_NOOVERWRITE(0x10), @@ -49,4 +52,29 @@ public enum PutFlags implements MaskedFlag { public int getMask() { return mask; } + + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(PutFlags flag) { + return flag != null && mask == flag.getMask(); + } + + @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/RangeComparator.java b/src/main/java/org/lmdbjava/RangeComparator.java new file mode 100644 index 00000000..f2626a59 --- /dev/null +++ b/src/main/java/org/lmdbjava/RangeComparator.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** For comparing a cursor's current key against a {@link KeyRange}'s start/stop key. */ +interface RangeComparator extends AutoCloseable { + + /** + * Compare the cursor's current key to the range start key. Equivalent to compareTo(currentKey, + * startKey) + */ + int compareToStartKey(); + + /** + * Compare the cursor's current key to the range stop key. Equivalent to compareTo(currentKey, + * stopKey) + */ + int compareToStopKey(); +} diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 05e8ce06..99439bf7 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -20,8 +20,6 @@ import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.Library.RUNTIME; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.ResultCodeMapper.checkRc; import static org.lmdbjava.Txn.State.DONE; import static org.lmdbjava.Txn.State.READY; @@ -44,13 +42,16 @@ public final class Txn implements AutoCloseable { private final Pointer ptr; private final boolean readOnly; private final Env env; + private final TxnFlagSet flags; private State state; - Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlags... flags) { + Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlagSet flags) { + this.flags = flags != null + ? flags + : TxnFlagSet.EMPTY; this.proxy = proxy; this.keyVal = proxy.keyVal(); - final int flagsMask = mask(true, flags); - this.readOnly = isSet(flagsMask, MDB_RDONLY_TXN); + this.readOnly = this.flags.isSet(MDB_RDONLY_TXN); if (env.isReadOnly() && !this.readOnly) { throw new EnvIsReadOnly(); } @@ -61,7 +62,7 @@ public final class Txn implements AutoCloseable { } final Pointer txnPtr = allocateDirect(RUNTIME, ADDRESS); final Pointer txnParentPtr = parent == null ? null : parent.ptr; - checkRc(LIB.mdb_txn_begin(env.pointer(), txnParentPtr, flagsMask, txnPtr)); + checkRc(LIB.mdb_txn_begin(env.pointer(), txnParentPtr, this.flags.getMask(), txnPtr)); ptr = txnPtr.getPointer(0); state = READY; @@ -164,7 +165,7 @@ public void renew() { } /** - * Aborts this read-only transaction and resets the transaction handle so it can be reused upon + * Aborts this read-only transaction and resets the transaction handle, so it can be reused upon * calling {@link #renew()}. */ public void reset() { diff --git a/src/main/java/org/lmdbjava/TxnFlagSet.java b/src/main/java/org/lmdbjava/TxnFlagSet.java new file mode 100644 index 00000000..8e6310b3 --- /dev/null +++ b/src/main/java/org/lmdbjava/TxnFlagSet.java @@ -0,0 +1,78 @@ +/* + * 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; +import java.util.Objects; + +public interface TxnFlagSet extends FlagSet { + + TxnFlagSet EMPTY = TxnFlagSetImpl.EMPTY; + + static TxnFlagSet empty() { + return TxnFlagSetImpl.EMPTY; + } + + static TxnFlagSet of(final TxnFlags putFlag) { + Objects.requireNonNull(putFlag); + return new SingleTxnFlagSet(putFlag); + } + + static TxnFlagSet of(final TxnFlags... TxnFlags) { + return builder() + .withFlags(TxnFlags) + .build(); + } + + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + TxnFlags.class, + TxnFlagSetImpl::new, + SingleTxnFlagSet::new, + () -> TxnFlagSetImpl.EMPTY); + } + + + // -------------------------------------------------------------------------------- + + + class TxnFlagSetImpl extends AbstractFlagSet implements TxnFlagSet { + + static final TxnFlagSet EMPTY = new EmptyTxnFlagSet(); + + private TxnFlagSetImpl(final EnumSet flags) { + super(flags); + } + } + + + // -------------------------------------------------------------------------------- + + + class SingleTxnFlagSet extends AbstractFlagSet.AbstractSingleFlagSet implements TxnFlagSet { + + SingleTxnFlagSet(final TxnFlags flag) { + super(flag); + } + } + + + // -------------------------------------------------------------------------------- + + + class EmptyTxnFlagSet extends AbstractFlagSet.AbstractEmptyFlagSet implements TxnFlagSet { + } +} diff --git a/src/main/java/org/lmdbjava/TxnFlags.java b/src/main/java/org/lmdbjava/TxnFlags.java index 26caf6f1..94112957 100644 --- a/src/main/java/org/lmdbjava/TxnFlags.java +++ b/src/main/java/org/lmdbjava/TxnFlags.java @@ -15,8 +15,12 @@ */ package org.lmdbjava; +import java.util.EnumSet; +import java.util.Set; + /** Flags for use when creating a {@link Txn}. */ -public enum TxnFlags implements MaskedFlag { +public enum TxnFlags implements MaskedFlag, TxnFlagSet { + /** Read only. */ MDB_RDONLY_TXN(0x2_0000); @@ -30,4 +34,29 @@ public enum TxnFlags implements MaskedFlag { public int getMask() { return mask; } + + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final TxnFlags flag) { + return flag != null && mask == flag.getMask(); + } + + @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/resources/org/lmdbjava/aarch64-linux-gnu.so b/src/main/resources/org/lmdbjava/aarch64-linux-gnu.so new file mode 100755 index 00000000..7e911756 Binary files /dev/null and b/src/main/resources/org/lmdbjava/aarch64-linux-gnu.so differ diff --git a/src/main/resources/org/lmdbjava/aarch64-macos-none.so b/src/main/resources/org/lmdbjava/aarch64-macos-none.so new file mode 100755 index 00000000..99858e49 Binary files /dev/null and b/src/main/resources/org/lmdbjava/aarch64-macos-none.so differ diff --git a/src/main/resources/org/lmdbjava/x86_64-linux-gnu.so b/src/main/resources/org/lmdbjava/x86_64-linux-gnu.so new file mode 100755 index 00000000..ee2e17bf Binary files /dev/null and b/src/main/resources/org/lmdbjava/x86_64-linux-gnu.so differ diff --git a/src/main/resources/org/lmdbjava/x86_64-macos-none.so b/src/main/resources/org/lmdbjava/x86_64-macos-none.so new file mode 100755 index 00000000..dfceffe6 Binary files /dev/null and b/src/main/resources/org/lmdbjava/x86_64-macos-none.so differ diff --git a/src/main/resources/org/lmdbjava/x86_64-windows-gnu.dll b/src/main/resources/org/lmdbjava/x86_64-windows-gnu.dll new file mode 100755 index 00000000..05e7b2c1 Binary files /dev/null and b/src/main/resources/org/lmdbjava/x86_64-windows-gnu.dll differ diff --git a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java index 82c0abce..dc034f7f 100644 --- a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java +++ b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java @@ -36,13 +36,24 @@ import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Random; +import java.util.Set; import jnr.ffi.Pointer; import jnr.ffi.provider.MemoryManager; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.lmdbjava.ByteBufferProxy.BufferMustBeDirectException; import org.lmdbjava.Env.ReadersFullException; -/** Test {@link ByteBufferProxy}. */ +/** + * Test {@link ByteBufferProxy}. + */ public final class ByteBufferProxyTest { static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); @@ -50,19 +61,25 @@ public final class ByteBufferProxyTest { @Test void buffersMustBeDirect() { assertThatThrownBy( - () -> { - FileUtil.useTempDir( - dir -> { - try (Env env = create().setMaxReaders(1).open(dir.toFile())) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - final ByteBuffer key = allocate(100); - key.putInt(1).flip(); - final ByteBuffer val = allocate(100); - val.putInt(1).flip(); - db.put(key, val); // error - } - }); - }) + () -> { + FileUtil.useTempDir( + dir -> { + try (Env env = create() + .setMaxReaders(1) + .open(dir)) { + final Dbi db = env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + final ByteBuffer key = allocate(100); + key.putInt(1).flip(); + final ByteBuffer val = allocate(100); + val.putInt(1).flip(); + db.put(key, val); // error + } + }); + }) .isInstanceOf(BufferMustBeDirectException.class); } @@ -87,9 +104,9 @@ void coverPrivateConstructor() { @Test void fieldNeverFound() { assertThatThrownBy( - () -> { - findField(Exception.class, "notARealField"); - }) + () -> { + findField(Exception.class, "notARealField"); + }) .isInstanceOf(LmdbException.class); } @@ -131,6 +148,105 @@ void unsafeIsDefault() { assertThat(v.getClass().getSimpleName()).startsWith("Unsafe"); } + /** + * For 100 rounds of 5,000,000 comparisons + * compareAsIntegerKeys: PT1.600525631S + * compareLexicographically: PT3.381935001S + */ + @Test + public void comparatorPerformance() { + final Random random = new Random(); + final ByteBuffer buffer1 = ByteBuffer.allocateDirect(Long.BYTES); + final ByteBuffer buffer2 = ByteBuffer.allocateDirect(Long.BYTES); + buffer1.limit(Long.BYTES); + buffer2.limit(Long.BYTES); + final long[] values = random.longs(5_000_000).toArray(); + + Instant time = Instant.now(); + int x = 0; + for (int rounds = 0; rounds < 100; rounds++) { + for (int i = 1; i < values.length; i++) { + buffer1.order(ByteOrder.nativeOrder()) + .putLong(0, values[i - 1]); + buffer2.order(ByteOrder.nativeOrder()) + .putLong(0, values[i]); + final int result = ByteBufferProxy.AbstractByteBufferProxy.compareAsIntegerKeys(buffer1, buffer2); + x += result; + } + } + System.out.println("compareAsIntegerKeys: " + Duration.between(time, Instant.now())); + + time = Instant.now(); + x = 0; + for (int rounds = 0; rounds < 100; rounds++) { + for (int i = 1; i < values.length; i++) { + buffer1.order(BIG_ENDIAN) + .putLong(0, values[i - 1]); + buffer2.order(BIG_ENDIAN) + .putLong(0, values[i]); + final int result = ByteBufferProxy.AbstractByteBufferProxy.compareLexicographically(buffer1, buffer2); + x += result; + } + } + System.out.println("compareLexicographically: " + Duration.between(time, Instant.now())); + } + + @Test + public void verifyComparators() { + final Random random = new Random(203948); + final ByteBuffer buffer1native = ByteBuffer.allocateDirect(Long.BYTES).order(ByteOrder.nativeOrder()); + final ByteBuffer buffer2native = ByteBuffer.allocateDirect(Long.BYTES).order(ByteOrder.nativeOrder()); + final ByteBuffer buffer1be = ByteBuffer.allocateDirect(Long.BYTES).order(BIG_ENDIAN); + final ByteBuffer buffer2be = ByteBuffer.allocateDirect(Long.BYTES).order(BIG_ENDIAN); + buffer1native.limit(Long.BYTES); + buffer2native.limit(Long.BYTES); + buffer1be.limit(Long.BYTES); + buffer2be.limit(Long.BYTES); + final long[] values = random.longs() + .filter(i -> i >= 0) + .limit(5_000_000) + .toArray(); +// System.out.println("stats: " + Arrays.stream(values) +// .summaryStatistics() +// .toString()); + + final LinkedHashMap> comparators = new LinkedHashMap<>(); + comparators.put("compareAsIntegerKeys", ByteBufferProxy.AbstractByteBufferProxy::compareAsIntegerKeys); + comparators.put("compareLexicographically", ByteBufferProxy.AbstractByteBufferProxy::compareLexicographically); + + final LinkedHashMap results = new LinkedHashMap<>(comparators.size()); + final Set uniqueResults = new HashSet<>(comparators.size()); + + for (int i = 1; i < values.length; i++) { + final long val1 = values[i - 1]; + final long val2 = values[i]; + buffer1native.putLong(0, val1); + buffer2native.putLong(0, val2); + buffer1be.putLong(0, val1); + buffer2be.putLong(0, val2); + uniqueResults.clear(); + + // Make sure all comparators give the same result for the same inputs + comparators.forEach((name, comparator) -> { + final int result; + // IntegerKey comparator expects keys to have been written in native order so need different buffers. + if (name.equals("compareAsIntegerKeys")) { + result = comparator.compare(buffer1native, buffer2native); + } else { + result = comparator.compare(buffer1be, buffer2be); + } + results.put(name, result); + uniqueResults.add(result); + }); + + if (uniqueResults.size() != 1) { + Assertions.fail("Comparator mismatch for values: " + + val1 + " and " + + val2 + ". Results: " + results); + } + } + } + private void checkInOut(final BufferProxy v) { // allocate a buffer larger than max key size final ByteBuffer b = allocateDirect(1_000); diff --git a/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java b/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java new file mode 100644 index 00000000..32e0c8c5 --- /dev/null +++ b/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java @@ -0,0 +1,352 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lmdbjava; + +import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.lmdbjava.ByteBufProxy.PROXY_NETTY; +import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; +import static org.lmdbjava.ComparatorTest.ComparatorResult.EQUAL_TO; +import static org.lmdbjava.ComparatorTest.ComparatorResult.GREATER_THAN; +import static org.lmdbjava.ComparatorTest.ComparatorResult.LESS_THAN; +import static org.lmdbjava.ComparatorTest.ComparatorResult.get; +import static org.lmdbjava.DirectBufferProxy.PROXY_DB; + +import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Comparator; +import java.util.Random; +import java.util.stream.Stream; +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests comparator functions are consistent across buffers. + */ +public final class ComparatorIntegerKeyTest { + + static Stream comparatorProvider() { + return Stream.of( + Arguments.argumentSet("LongRunner", new DirectBufferRunner()), + Arguments.argumentSet("DirectBufferRunner", new DirectBufferRunner()), + Arguments.argumentSet("ByteBufferRunner", new ByteBufferRunner()), + Arguments.argumentSet("NettyRunner", new NettyRunner())); + } + + private static byte[] buffer(final int... bytes) { + final byte[] array = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + array[i] = (byte) bytes[i]; + } + return array; + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testLong(final ComparatorRunner comparator) { + + assertThat(get(comparator.compare(0L, 0L))).isEqualTo(EQUAL_TO); + assertThat(get(comparator.compare(Long.MAX_VALUE, Long.MAX_VALUE))).isEqualTo(EQUAL_TO); + + assertThat(get(comparator.compare(0L, 1L))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(0L, Long.MAX_VALUE))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(0L, 10L))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10L, 100L))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10L, 100L))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10L, 1000L))).isEqualTo(LESS_THAN); + + assertThat(get(comparator.compare(1L, 0L))).isEqualTo(GREATER_THAN); + assertThat(get(comparator.compare(Long.MAX_VALUE, 0L))).isEqualTo(GREATER_THAN); + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testInt(final ComparatorRunner comparator) { + + assertThat(get(comparator.compare(0, 0))).isEqualTo(EQUAL_TO); + assertThat(get(comparator.compare(Integer.MAX_VALUE, Integer.MAX_VALUE))).isEqualTo(EQUAL_TO); + + assertThat(get(comparator.compare(0, 1))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(0, Integer.MAX_VALUE))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(0, 10))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10, 100))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10, 100))).isEqualTo(LESS_THAN); + assertThat(get(comparator.compare(10, 1000))).isEqualTo(LESS_THAN); + + assertThat(get(comparator.compare(1, 0))).isEqualTo(GREATER_THAN); + assertThat(get(comparator.compare(Integer.MAX_VALUE, 0))).isEqualTo(GREATER_THAN); + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testRandomLong(final ComparatorRunner runner) { + final Random random = new Random(3239480); + + // 5mil random longs to compare + final long[] values = random.longs() + .filter(i -> i >= 0) + .limit(5_000_000) + .toArray(); + + for (int i = 1; i < values.length; i++) { + final long long1 = values[i - 1]; + final long long2 = values[i]; + // Make sure the comparator under test gives the same outcome as just comparing two longs + final ComparatorTest.ComparatorResult result = get(runner.compare(long1, long2)); + final ComparatorTest.ComparatorResult expectedResult = get(Long.compare(long1, long2)); + + assertThat(result) + .withFailMessage(() -> "Compare mismatch - long1: " + long1 + + ", long2: " + long2 + + ", expected: " + expectedResult + + ", actual: " + result) + .isEqualTo(expectedResult); + + final ComparatorTest.ComparatorResult result2 = get(runner.compare(long2, long1)); + final ComparatorTest.ComparatorResult expectedResult2 = expectedResult.opposite(); + + assertThat(result) + .withFailMessage(() -> "Compare mismatch for - long2: " + long2 + + ", long1: " + long1 + + ", expected2: " + expectedResult2 + + ", actual2: " + result2) + .isEqualTo(expectedResult); + } + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testRandomInt(final ComparatorRunner runner) { + final Random random = new Random(3239480); + + // 5mil random ints to compare + final int[] values = random.ints() + .filter(i -> i >= 0) + .limit(5_000_000) + .toArray(); + + for (int i = 1; i < values.length; i++) { + final int int1 = values[i - 1]; + final int int2 = values[i]; + // Make sure the comparator under test gives the same outcome as just comparing two ints + final ComparatorTest.ComparatorResult result = get(runner.compare(int1, int2)); + final ComparatorTest.ComparatorResult expectedResult = get(Integer.compare(int1, int2)); + + assertThat(result) + .withFailMessage(() -> "Compare mismatch for - int1: " + int1 + + ", int2: " + int2 + + ", expected: " + expectedResult + + ", actual: " + result) + .isEqualTo(expectedResult); + + final ComparatorTest.ComparatorResult result2 = get(runner.compare(int2, int1)); + final ComparatorTest.ComparatorResult expectedResult2 = expectedResult.opposite(); + + assertThat(result) + .withFailMessage(() -> "Compare mismatch for - int2: " + int2 + + ", int1: " + int1 + + ", expected2: " + expectedResult2 + + ", actual2: " + result2) + .isEqualTo(expectedResult); + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * Tests {@link ByteBufferProxy}. + */ + private static final class ByteBufferRunner implements ComparatorRunner { + + private static final Comparator COMPARATOR = PROXY_OPTIMAL.getComparator(DbiFlags.MDB_INTEGERKEY); + + @Override + public int compare(long long1, long long2) { + // Convert arrays to buffers that are larger than the array, with + // limit set at the array length. One buffer bigger than the other. + ByteBuffer o1b = longToBuffer(long1, Long.BYTES * 3); + ByteBuffer o2b = longToBuffer(long2, Long.BYTES * 2); + final int result = COMPARATOR.compare(o1b, o2b); + + // Now swap which buffer is bigger + o1b = longToBuffer(long1, Long.BYTES * 2); + o2b = longToBuffer(long2, Long.BYTES * 3); + final int result2 = COMPARATOR.compare(o1b, o2b); + + assertThat(result2).isEqualTo(result); + + // Now try with buffers sized to the array. + o1b = longToBuffer(long1, Long.BYTES); + o2b = longToBuffer(long2, Long.BYTES); + final int result3 = COMPARATOR.compare(o1b, o2b); + + assertThat(result3).isEqualTo(result); + return result; + } + + @Override + public int compare(int int1, int int2) { + // Convert arrays to buffers that are larger than the array, with + // limit set at the array length. One buffer bigger than the other. + ByteBuffer o1b = intToBuffer(int1, Integer.BYTES * 3); + ByteBuffer o2b = intToBuffer(int2, Integer.BYTES * 2); + final int result = COMPARATOR.compare(o1b, o2b); + + // Now swap which buffer is bigger + o1b = intToBuffer(int1, Integer.BYTES * 2); + o2b = intToBuffer(int2, Integer.BYTES * 3); + final int result2 = COMPARATOR.compare(o1b, o2b); + + assertThat(result2).isEqualTo(result); + + // Now try with buffers sized to the array. + o1b = intToBuffer(int1, Integer.BYTES); + o2b = intToBuffer(int2, Integer.BYTES); + final int result3 = COMPARATOR.compare(o1b, o2b); + + assertThat(result3).isEqualTo(result); + return result; + } + + private ByteBuffer longToBuffer(final long val, final int bufferCapacity) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(bufferCapacity); + byteBuffer.order(ByteOrder.nativeOrder()); + byteBuffer.putLong(0, val); + byteBuffer.limit(Long.BYTES); + byteBuffer.position(0); + return byteBuffer; + } + + private ByteBuffer intToBuffer(final int val, final int bufferCapacity) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(bufferCapacity); + byteBuffer.order(ByteOrder.nativeOrder()); + byteBuffer.putInt(0, val); + byteBuffer.limit(Integer.BYTES); + byteBuffer.position(0); + return byteBuffer; + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * Tests {@link DirectBufferProxy}. + */ + private static final class DirectBufferRunner implements ComparatorRunner { + private static final Comparator COMPARATOR = PROXY_DB.getComparator(DbiFlags.MDB_INTEGERKEY); + + @Override + public int compare(long long1, long long2) { + final UnsafeBuffer o1b = new UnsafeBuffer(new byte[Long.BYTES]); + final UnsafeBuffer o2b = new UnsafeBuffer(new byte[Long.BYTES]); + o1b.putLong(0, long1, ByteOrder.nativeOrder()); + o2b.putLong(0, long2, ByteOrder.nativeOrder()); + return COMPARATOR.compare(o1b, o2b); + } + + @Override + public int compare(int int1, int int2) { + final UnsafeBuffer o1b = new UnsafeBuffer(new byte[Integer.BYTES]); + final UnsafeBuffer o2b = new UnsafeBuffer(new byte[Integer.BYTES]); + o1b.putInt(0, int1, ByteOrder.nativeOrder()); + o2b.putInt(0, int2, ByteOrder.nativeOrder()); + return COMPARATOR.compare(o1b, o2b); + } + } + + /** + * Tests {@link ByteBufProxy}. + */ + private static final class NettyRunner implements ComparatorRunner { + + private static final Comparator COMPARATOR = PROXY_NETTY.getComparator(DbiFlags.MDB_INTEGERKEY); + + @Override + public int compare(long long1, long long2) { + final ByteBuf o1b = DEFAULT.directBuffer(Long.BYTES); + final ByteBuf o2b = DEFAULT.directBuffer(Long.BYTES); + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + o1b.writeLongLE(long1); + o2b.writeLongLE(long2); + } else { + o1b.writeLong(long1); + o2b.writeLong(long2); + } + o1b.resetReaderIndex(); + o2b.resetReaderIndex(); + final int res = COMPARATOR.compare(o1b, o2b); + o1b.release(); + o2b.release(); + return res; + } + + @Override + public int compare(int int1, int int2) { + final ByteBuf o1b = DEFAULT.directBuffer(Integer.BYTES); + final ByteBuf o2b = DEFAULT.directBuffer(Integer.BYTES); + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + o1b.writeIntLE(int1); + o2b.writeIntLE(int2); + } else { + o1b.writeInt(int1); + o2b.writeInt(int2); + } + o1b.resetReaderIndex(); + o2b.resetReaderIndex(); + final int res = COMPARATOR.compare(o1b, o2b); + o1b.release(); + o2b.release(); + return res; + } + } + + + // -------------------------------------------------------------------------------- + + + /** + * Interface that can test a {@link BufferProxy} compare method. + */ + private interface ComparatorRunner { + + /** + * Write the two longs to a buffer using native order and compare the resulting buffers. + * + * @param long1 lhs value + * @param long2 rhs value + * @return as per {@link Comparable} + */ + int compare(final long long1, final long long2); + + /** + * Write the two int to a buffer using native order and compare the resulting buffers. + * + * @param int1 lhs value + * @param int2 rhs value + * @return as per {@link Comparable} + */ + int compare(final int int1, final int int2); + } +} diff --git a/src/test/java/org/lmdbjava/ComparatorTest.java b/src/test/java/org/lmdbjava/ComparatorTest.java index a5e010ab..1d4ed4c8 100644 --- a/src/test/java/org/lmdbjava/ComparatorTest.java +++ b/src/test/java/org/lmdbjava/ComparatorTest.java @@ -140,7 +140,7 @@ private static final class UnsignedByteArrayRunner implements ComparatorRunner { @Override public int compare(final byte[] o1, final byte[] o2) { - final Comparator c = PROXY_BA.getUnsignedComparator(); + final Comparator c = PROXY_BA.getComparator(); return c.compare(o1, o2); } } @@ -259,6 +259,16 @@ static ComparatorResult get(final int comparatorResult) { } return comparatorResult < 0 ? LESS_THAN : GREATER_THAN; } + + ComparatorResult opposite() { + if (this == LESS_THAN) { + return GREATER_THAN; + } else if (this == GREATER_THAN) { + return LESS_THAN; + } else { + return EQUAL_TO; + } + } } /** Interface that can test a {@link BufferProxy} compare method. */ diff --git a/src/test/java/org/lmdbjava/CopyFlagSetTest.java b/src/test/java/org/lmdbjava/CopyFlagSetTest.java new file mode 100644 index 00000000..2782fe81 --- /dev/null +++ b/src/test/java/org/lmdbjava/CopyFlagSetTest.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 static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.HashSet; +import org.junit.jupiter.api.Test; + +public class CopyFlagSetTest { + + @Test + public void testEmpty() { + final CopyFlagSet copyFlagSet = CopyFlagSet.empty(); + assertThat(copyFlagSet.getMask()).isEqualTo(0); + assertThat(copyFlagSet.size()).isEqualTo(0); + assertThat(copyFlagSet.isEmpty()).isEqualTo(true); + assertThat(copyFlagSet.isSet(CopyFlags.MDB_CP_COMPACT)).isEqualTo(false); + final CopyFlagSet copyFlagSet2 = CopyFlagSet.builder() + .build(); + assertThat(copyFlagSet).isEqualTo(copyFlagSet2); + assertThat(copyFlagSet).isNotEqualTo(CopyFlagSet.of(CopyFlags.MDB_CP_COMPACT)); + assertThat(copyFlagSet).isNotEqualTo(CopyFlagSet.builder() + .setFlag(CopyFlags.MDB_CP_COMPACT) + .build()); + } + + @Test + public void testOf() { + final CopyFlags copyFlag = CopyFlags.MDB_CP_COMPACT; + final CopyFlagSet copyFlagSet = CopyFlagSet.of(copyFlag); + assertThat(copyFlagSet.getMask()).isEqualTo(MaskedFlag.mask(copyFlag)); + assertThat(copyFlagSet.size()).isEqualTo(1); + for (CopyFlags flag : copyFlagSet) { + assertThat(copyFlagSet.isSet(flag)).isEqualTo(true); + } + + final CopyFlagSet copyFlagSet2 = CopyFlagSet.builder() + .setFlag(copyFlag) + .build(); + assertThat(copyFlagSet).isEqualTo(copyFlagSet2); + } + + @Test + public void testBuilder() { + final CopyFlags copyFlag1 = CopyFlags.MDB_CP_COMPACT; + final CopyFlagSet copyFlagSet = CopyFlagSet.builder() + .setFlag(copyFlag1) + .build(); + assertThat(copyFlagSet.getMask()).isEqualTo(MaskedFlag.mask(copyFlag1)); + assertThat(copyFlagSet.size()).isEqualTo(1); + assertThat(copyFlagSet.isSet(CopyFlags.MDB_CP_COMPACT)).isEqualTo(true); + for (CopyFlags flag : copyFlagSet) { + assertThat(copyFlagSet.isSet(flag)).isEqualTo(true); + } + final CopyFlagSet copyFlagSet2 = CopyFlagSet.builder() + .withFlags(copyFlag1) + .build(); + final CopyFlagSet copyFlagSet3 = CopyFlagSet.builder() + .withFlags(new HashSet<>(Collections.singletonList(copyFlag1))) + .build(); + assertThat(copyFlagSet).isEqualTo(copyFlagSet2); + assertThat(copyFlagSet).isEqualTo(copyFlagSet3); + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterableIntegerDupTest.java b/src/test/java/org/lmdbjava/CursorIterableIntegerDupTest.java new file mode 100644 index 00000000..775ac4bc --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterableIntegerDupTest.java @@ -0,0 +1,589 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.DbiFlags.MDB_DUPSORT; +import static org.lmdbjava.DbiFlags.MDB_INTEGERDUP; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.KeyRange.all; +import static org.lmdbjava.KeyRange.allBackward; +import static org.lmdbjava.KeyRange.atLeast; +import static org.lmdbjava.KeyRange.atLeastBackward; +import static org.lmdbjava.KeyRange.atMost; +import static org.lmdbjava.KeyRange.atMostBackward; +import static org.lmdbjava.KeyRange.closed; +import static org.lmdbjava.KeyRange.closedBackward; +import static org.lmdbjava.KeyRange.closedOpen; +import static org.lmdbjava.KeyRange.closedOpenBackward; +import static org.lmdbjava.KeyRange.greaterThan; +import static org.lmdbjava.KeyRange.greaterThanBackward; +import static org.lmdbjava.KeyRange.lessThan; +import static org.lmdbjava.KeyRange.lessThanBackward; +import static org.lmdbjava.KeyRange.open; +import static org.lmdbjava.KeyRange.openBackward; +import static org.lmdbjava.KeyRange.openClosed; +import static org.lmdbjava.KeyRange.openClosedBackward; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.DB_2; +import static org.lmdbjava.TestUtils.DB_3; +import static org.lmdbjava.TestUtils.DB_4; +import static org.lmdbjava.TestUtils.POSIX_MODE; +import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.bbNative; +import static org.lmdbjava.TestUtils.getNativeInt; +import static org.lmdbjava.TestUtils.getNativeIntOrLong; + +import com.google.common.primitives.UnsignedBytes; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.lmdbjava.CursorIterable.KeyVal; + +/** + * Test {@link CursorIterable} using {@link DbiFlags#MDB_INTEGERKEY} to ensure that + * comparators work with native order integer keys. + */ +@Disabled // Waiting for the merge of stroomdev66's cursor tests +@ParameterizedClass(name = "{index}: dbi: {0}") +@ArgumentsSource(CursorIterableTest.MyArgumentProvider.class) +public final class CursorIterableIntegerDupTest { + + private static final DbiFlagSet DBI_FLAGS = DbiFlagSet.of( + MDB_CREATE, + MDB_INTEGERDUP, + MDB_DUPSORT); + private static final BufferProxy BUFFER_PROXY = ByteBufferProxy.PROXY_OPTIMAL; + private static final List> INPUT_DATA; + + static { + // 2 => 21 + // 2 => 22 + // 3 => 31 + // ... + // 9 => 92 + INPUT_DATA = new ArrayList<>(); + for (int i = 2; i <= 9; i++) { + final int val1 = (i * 10) + 1; + final int val2 = (i * 10) + 2; + INPUT_DATA.add(new AbstractMap.SimpleEntry<>(i, val1)); + INPUT_DATA.add(new AbstractMap.SimpleEntry<>(i, val2)); + } + } + + private Path file; + private Env env; + private Deque> expectedEntriesDeque; + + @Parameter + public DbiFactory dbiFactory; + + @BeforeEach + public void before() throws IOException { + file = FileUtil.createTempFile(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = + create(bufferProxy) + .setMapSize(KIBIBYTES.toBytes(256)) + .setMaxReaders(1) + .setMaxDbs(3) + .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); + + populateExpectedEntriesDeque(); + } + + @AfterEach + public void after() { + env.close(); + FileUtil.delete(file); + } + + @Test + public void allBackwardTest() { + verify(allBackward(), 8, 6, 4, 2); + } + + @Test + public void allTest() { + verify(all(), 2, 4, 6, 8); + } + + @Test + public void atLeastBackwardTest() { + verify(atLeastBackward(bbNative(5)), 4, 2); + verify(atLeastBackward(bbNative(6)), 6, 4, 2); + verify(atLeastBackward(bbNative(9)), 8, 6, 4, 2); + } + + @Test + public void atLeastTest() { + verify(atLeast(bbNative(5)), 6, 8); + verify(atLeast(bbNative(6)), 6, 8); + } + + @Test + public void atMostBackwardTest() { + verify(atMostBackward(bbNative(5)), 8, 6); + verify(atMostBackward(bbNative(6)), 8, 6); + } + + @Test + public void atMostTest() { + verify(atMost(bbNative(5)), 2, 4); + verify(atMost(bbNative(6)), 2, 4, 6); + } + + private void populateExpectedEntriesDeque() { + expectedEntriesDeque = new LinkedList<>(); + expectedEntriesDeque.addAll(INPUT_DATA); + } + + private void populateDatabase(final Dbi dbi) { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + for (Map.Entry entry : INPUT_DATA) { + c.put(bbNative(entry.getKey()), bb(entry.getValue())); + } + txn.commit(); + } + + try (Txn txn = env.txnRead(); + CursorIterable c = dbi.iterate(txn)) { + +// for (final KeyVal kv : c) { +// System.out.print(getNativeInt(kv.key()) + " => " + kv.val().getInt()); +// System.out.print(", "); +// } +// System.out.println(); + } + } + + private int[] rangeInc(final int fromInc, final int toInc) { + int idx = 0; + if (fromInc <= toInc) { + // Forwards + final int[] arr = new int[toInc - fromInc + 1]; + for (int i = fromInc; i <= toInc; i++) { + arr[idx++] = i; + } + return arr; + } else { + // Backwards + final int[] arr = new int[fromInc - toInc + 1]; + for (int i = fromInc; i >= toInc; i--) { + arr[idx++] = i; + } + return arr; + } + } + + @Test + public void closedBackwardTest() { + verify(closedBackward(bbNative(7), bbNative(3)), rangeInc(7, 3)); + verify(closedBackward(bbNative(6), bbNative(2)), rangeInc(6, 2)); + verify(closedBackward(bbNative(9), bbNative(3)), rangeInc(9, 3)); + } + + @Test + public void closedOpenBackwardTest() { + verify(closedOpenBackward(bbNative(8), bbNative(3)), rangeInc(8, 4)); + verify(closedOpenBackward(bbNative(7), bbNative(2)), rangeInc(7, 3)); + verify(closedOpenBackward(bbNative(9), bbNative(3)), rangeInc(9, 4)); + } + + @Test + public void closedOpenTest() { + verify(closedOpen(bbNative(3), bbNative(8)), rangeInc(3, 7)); + verify(closedOpen(bbNative(2), bbNative(6)), rangeInc(2, 5)); + } + + @Test + public void closedTest() { + verify(closed(bbNative(3), bbNative(7)), rangeInc(3, 7)); + verify(closed(bbNative(2), bbNative(6)), rangeInc(2, 6)); + verify(closed(bbNative(1), bbNative(7)), rangeInc(2, 7)); + } + + @Test + public void greaterThanBackwardTest() { + verify(greaterThanBackward(bbNative(6)), rangeInc(5, 2)); + verify(greaterThanBackward(bbNative(7)), rangeInc(6, 2)); + verify(greaterThanBackward(bbNative(9)), rangeInc(8, 2)); + } + + @Test + public void greaterThanTest() { + verify(greaterThan(bbNative(4)), rangeInc(5, 9)); + verify(greaterThan(bbNative(3)), rangeInc(4, 9)); + } + + public void iterableOnlyReturnedOnce() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }).isInstanceOf(IllegalStateException.class); + } + + @Test + public void iterate() { + populateExpectedEntriesDeque(); + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + + for (final KeyVal kv : c) { + final Map.Entry entry = expectedEntriesDeque.pollFirst(); +// System.out.println(entry.getKey() + " => " + entry.getValue()); + assertThat(getNativeInt(kv.key())).isEqualTo(entry.getKey()); + assertThat(kv.val().getInt()).isEqualTo(entry.getValue()); + } + } + } + + public void iteratorOnlyReturnedOnce() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }).isInstanceOf(IllegalStateException.class); + } + + @Test + public void lessThanBackwardTest() { + verify(lessThanBackward(bbNative(5)), 8, 6); + verify(lessThanBackward(bbNative(2)), 8, 6, 4); + } + + @Test + public void lessThanTest() { + verify(lessThan(bbNative(5)), 2, 4); + verify(lessThan(bbNative(8)), 2, 4, 6); + } + + public void nextThrowsNoSuchElementExceptionIfNoMoreElements() { + Assertions.assertThatThrownBy(() -> { + populateExpectedEntriesDeque(); + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + final Iterator> i = c.iterator(); + while (i.hasNext()) { + final KeyVal kv = i.next(); + assertThat(getNativeInt(kv.key())).isEqualTo(expectedEntriesDeque.pollFirst().getKey()); + assertThat(kv.val().getInt()).isEqualTo(expectedEntriesDeque.pollFirst().getValue()); + } + assertThat(i.hasNext()).isEqualTo(false); + i.next(); + } + }).isInstanceOf(NoSuchElementException.class); + } + + @Test + public void openBackwardTest() { + verify(openBackward(bbNative(7), bbNative(2)), 6, 4); + verify(openBackward(bbNative(8), bbNative(1)), 6, 4, 2); + verify(openBackward(bbNative(9), bbNative(4)), 8, 6); + } + + @Test + public void openClosedBackwardTest() { + verify(openClosedBackward(bbNative(7), bbNative(2)), 6, 4, 2); + verify(openClosedBackward(bbNative(8), bbNative(4)), 6, 4); + verify(openClosedBackward(bbNative(9), bbNative(4)), 8, 6, 4); + } + + @Test + public void openClosedBackwardTestWithGuava() { + final Comparator guava = UnsignedBytes.lexicographicalComparator(); + final Comparator comparator = + (bb1, bb2) -> { + final byte[] array1 = new byte[bb1.remaining()]; + final byte[] array2 = new byte[bb2.remaining()]; + bb1.mark(); + bb2.mark(); + bb1.get(array1); + bb2.get(array2); + bb1.reset(); + bb2.reset(); + return guava.compare(array1, array2); + }; + final Dbi guavaDbi = env.openDbi(DB_1, comparator, MDB_CREATE); + populateDatabase(guavaDbi); + verify(openClosedBackward(bbNative(7), bbNative(2)), guavaDbi, 6, 4, 2); + verify(openClosedBackward(bbNative(8), bbNative(4)), guavaDbi, 6, 4); + } + + @Test + public void openClosedTest() { + verify(openClosed(bbNative(3), bbNative(8)), 4, 6, 8); + verify(openClosed(bbNative(2), bbNative(6)), 4, 6); + } + + @Test + public void openTest() { + verify(open(bbNative(3), bbNative(7)), 4, 6); + verify(open(bbNative(2), bbNative(8)), 4, 6); + } + + @Test + public void removeOddElements() { + final Dbi db = getDb(); + verify(db, all(), 2, 4, 6, 8); + int idx = -1; + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn)) { + final Iterator> c = ci.iterator(); + while (c.hasNext()) { + c.next(); + idx++; + if (idx % 2 == 0) { + c.remove(); + } + } + } + txn.commit(); + } + verify(db, all(), 4, 8); + } + + public void nextWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.next(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void removeWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + final KeyVal keyVal = c.next(); + assertThat(keyVal).isNotNull(); + env.close(); + c.remove(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void hasNextWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.hasNext(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void forEachRemainingWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.forEachRemaining(keyVal -> { + }); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + private void verify(final KeyRange range, final int... expectedKeys) { + // Verify using all comparator types + final Dbi db = getDb(); + verify(range, db, expectedKeys); + } + + private void verify(final Dbi dbi, + final KeyRange range, + final int... expectedKeys) { + verify(range, dbi, expectedKeys); + } + + private void verify(final KeyRange range, + final Dbi dbi, + final int... expectedKeys) { + final boolean isForward = range.getType().isDirectionForward(); + + final List expectedValues = Arrays.stream(expectedKeys) + .boxed() + .flatMap(key -> { + final int base = key * 10; + return isForward + ? Stream.of(base + 1, base + 2) + : Stream.of(base + 2, base + 1); + }) + .collect(Collectors.toList()); + + final List results = new ArrayList<>(); +// System.out.println(rangeToString(range) + ", expected: " + expectedValues); + + try (Txn txn = env.txnRead(); + CursorIterable c = dbi.iterate(txn, range)) { + for (final KeyVal kv : c) { + final int key = getNativeInt(kv.key()); + final int val = kv.val().getInt(); +// System.out.println(key + " => " + val); + results.add(val); + assertThat(val).satisfiesAnyOf( + v -> assertThat(v).isEqualTo((key * 10) + 1), + v -> assertThat(v).isEqualTo((key * 10) + 2)); + } + } + + assertThat(results).hasSize(expectedValues.size()); + for (int idx = 0; idx < results.size(); idx++) { + assertThat(results.get(idx)).isEqualTo(expectedValues.get(idx)); + } + } + + private String rangeToString(final KeyRange range) { + final ByteBuffer start = range.getStart(); + final ByteBuffer stop = range.getStop(); + return range.getType() + " start: " + (start != null ? getNativeInt(start) : "") + + " stop: " + (stop != null ? getNativeInt(stop) : ""); + } + + private Dbi getDb() { + final Dbi dbi = dbiFactory.factory.apply(env); + populateDatabase(dbi); + return dbi; + } + + + // -------------------------------------------------------------------------------- + + + private static class DbiFactory { + private final String name; + private final Function, Dbi> factory; + + private DbiFactory(String name, Function, Dbi> factory) { + this.name = name; + this.factory = factory; + } + + @Override + public String toString() { + return name; + } + } + + + // -------------------------------------------------------------------------------- + + + static class MyArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) throws Exception { + final DbiFactory defaultComparatorDb = new DbiFactory("defaultComparator", env -> + env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory nativeComparatorDb = new DbiFactory("nativeComparator", env -> + env.buildDbi() + .setDbName(DB_2) + .withNativeComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory callbackComparatorDb = new DbiFactory("callbackComparator", env -> + env.buildDbi() + .setDbName(DB_3) + .withCallbackComparator(MyArgumentProvider::buildComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory iteratorComparatorDb = new DbiFactory("iteratorComparator", env -> + env.buildDbi() + .setDbName(DB_4) + .withIteratorComparator(MyArgumentProvider::buildComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + return Stream.of( + defaultComparatorDb, + nativeComparatorDb, + callbackComparatorDb, + iteratorComparatorDb) + .map(Arguments::of); + } + + private static Comparator buildComparator(final DbiFlagSet dbiFlagSet) { + final Comparator baseComparator = BUFFER_PROXY.getComparator(DBI_FLAGS); + return (o1, o2) -> { + if (o1.remaining() != o2.remaining()) { + // Make sure LMDB is always giving us consistent key lengths. + Assertions.fail("o1: " + o1 + " " + getNativeIntOrLong(o1) + + ", o2: " + o2 + " " + getNativeIntOrLong(o2)); + } + return baseComparator.compare(o1, o2); + }; + } + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java new file mode 100644 index 00000000..a0d9ab0c --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java @@ -0,0 +1,646 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.KeyRange.all; +import static org.lmdbjava.KeyRange.allBackward; +import static org.lmdbjava.KeyRange.atLeast; +import static org.lmdbjava.KeyRange.atLeastBackward; +import static org.lmdbjava.KeyRange.atMost; +import static org.lmdbjava.KeyRange.atMostBackward; +import static org.lmdbjava.KeyRange.closed; +import static org.lmdbjava.KeyRange.closedBackward; +import static org.lmdbjava.KeyRange.closedOpen; +import static org.lmdbjava.KeyRange.closedOpenBackward; +import static org.lmdbjava.KeyRange.greaterThan; +import static org.lmdbjava.KeyRange.greaterThanBackward; +import static org.lmdbjava.KeyRange.lessThan; +import static org.lmdbjava.KeyRange.lessThanBackward; +import static org.lmdbjava.KeyRange.open; +import static org.lmdbjava.KeyRange.openBackward; +import static org.lmdbjava.KeyRange.openClosed; +import static org.lmdbjava.KeyRange.openClosedBackward; +import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.DB_2; +import static org.lmdbjava.TestUtils.DB_3; +import static org.lmdbjava.TestUtils.DB_4; +import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.bbNative; +import static org.lmdbjava.TestUtils.getNativeInt; +import static org.lmdbjava.TestUtils.getNativeIntOrLong; +import static org.lmdbjava.TestUtils.getNativeLong; +import static org.lmdbjava.TestUtils.getString; + +import com.google.common.primitives.UnsignedBytes; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.lmdbjava.CursorIterable.KeyVal; + +/** + * Test {@link CursorIterable} using {@link DbiFlags#MDB_INTEGERKEY} to ensure that + * comparators work with native order integer keys. + */ +@ParameterizedClass(name = "{index}: dbi: {0}") +@ArgumentsSource(CursorIterableIntegerKeyTest.MyArgumentProvider.class) +public final class CursorIterableIntegerKeyTest { + + private static final DbiFlagSet DBI_FLAGS = DbiFlagSet.of(MDB_CREATE, MDB_INTEGERKEY); + private static final BufferProxy BUFFER_PROXY = ByteBufferProxy.PROXY_OPTIMAL; + + private Path file; + private Env env; + private Deque list; + + @Parameter + public DbiFactory dbiFactory; + + @BeforeEach + public void before() throws IOException { + file = FileUtil.createTempFile(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = Env.create(bufferProxy) + .setMapSize(KIBIBYTES.toBytes(256)) + .setMaxReaders(1) + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + populateTestDataList(); + } + + @AfterEach + public void after() { + env.close(); + FileUtil.delete(file); + } + + @Test + public void testNumericOrderLong() { + final Dbi dbi = dbiFactory.factory.apply(env); + + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + long i = 1; + while (true) { +// System.out.println("putting " + i); + c.put(bbNative(i), bb(i + "-long")); + final long i2 = i * 10; + if (i2 < i) { + // Overflowed + break; + } + i = i2; + } + txn.commit(); + } + + final List> entries = new ArrayList<>(); + try (Txn txn = env.txnRead()) { + try (CursorIterable iterable = dbi.iterate(txn)) { + for (KeyVal keyVal : iterable) { + assertThat(keyVal.key().remaining()).isEqualTo(Long.BYTES); + final String val = getString(keyVal.val()); + final long key = getNativeLong(keyVal.key()); + entries.add(new AbstractMap.SimpleEntry<>(key, val)); +// System.out.println(val); + } + } + } + + final List dbKeys = entries.stream() + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + final List dbKeysSorted = entries.stream() + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + for (int i = 0; i < dbKeys.size(); i++) { + final long dbKey1 = dbKeys.get(i); + final long dbKey2 = dbKeysSorted.get(i); + assertThat(dbKey1).isEqualTo(dbKey2); + } + } + + @Test + public void testNumericOrderInt() { + final Dbi dbi = dbiFactory.factory.apply(env); + + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + int i = 1; + while (true) { +// System.out.println("putting " + i); + c.put(bbNative(i), bb(i + "-int")); + final int i2 = i * 10; + if (i2 < i) { + // Overflowed + break; + } + i = i2; + } + txn.commit(); + } + + final List> entries = new ArrayList<>(); + try (Txn txn = env.txnRead()) { + try (CursorIterable iterable = dbi.iterate(txn)) { + for (KeyVal keyVal : iterable) { + assertThat(keyVal.key().remaining()).isEqualTo(Integer.BYTES); + final String val = getString(keyVal.val()); + final int key = TestUtils.getNativeInt(keyVal.key()); + entries.add(new AbstractMap.SimpleEntry<>(key, val)); +// System.out.println(val); + } + } + } + + final List dbKeys = entries.stream() + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + final List dbKeysSorted = entries.stream() + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + for (int i = 0; i < dbKeys.size(); i++) { + final long dbKey1 = dbKeys.get(i); + final long dbKey2 = dbKeysSorted.get(i); + assertThat(dbKey1).isEqualTo(dbKey2); + } + } + + @Test + public void testIntegerKeyKeySize() { + final Dbi db = dbiFactory.factory.apply(env); + long maxIntAsLong = Integer.MAX_VALUE; + + try (Txn txn = env.txnWrite()) { +// System.out.println("Flags: " + db.listFlags(txn)); + int val = 0; + db.put(txn, bbNative(0L), bb("val_" + ++val)); + db.put(txn, bbNative(10L), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong - 1_111_111_111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong - 111_111_111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong - 111_111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong - 111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong + 111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong + 111_111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong + 111_111_111), bb("val_" + ++val)); + db.put(txn, bbNative(maxIntAsLong + 1_111_111_111), bb("val_" + ++val)); + db.put(txn, bbNative(Long.MAX_VALUE), bb("val_" + ++val)); + txn.commit(); + } + + try (Txn txn = env.txnRead()) { + try (CursorIterable iterable = db.iterate(txn)) { + for (KeyVal keyVal : iterable) { + final String val = getString(keyVal.val()); + final long key = getNativeLong(keyVal.key()); + final int remaining = keyVal.key().remaining(); +// System.out.println("key: " + key + ", val: " + val + ", remaining: " + remaining); + } + } + } + + } + + @Test + public void allBackwardTest() { + verify(allBackward(), 8, 6, 4, 2); + } + + @Test + public void allTest() { + verify(all(), 2, 4, 6, 8); + } + + @Test + public void atLeastBackwardTest() { + verify(atLeastBackward(bbNative(5)), 4, 2); + verify(atLeastBackward(bbNative(6)), 6, 4, 2); + verify(atLeastBackward(bbNative(9)), 8, 6, 4, 2); + } + + @Test + public void atLeastTest() { + verify(atLeast(bbNative(5)), 6, 8); + verify(atLeast(bbNative(6)), 6, 8); + } + + @Test + public void atMostBackwardTest() { + verify(atMostBackward(bbNative(5)), 8, 6); + verify(atMostBackward(bbNative(6)), 8, 6); + } + + @Test + public void atMostTest() { + verify(atMost(bbNative(5)), 2, 4); + verify(atMost(bbNative(6)), 2, 4, 6); + } + + private void populateTestDataList() { + list = new LinkedList<>(); + list.addAll(asList(2, 3, 4, 5, 6, 7, 8, 9)); + } + + private void populateDatabase(final Dbi dbi) { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + c.put(bbNative(2), bb(3), MDB_NOOVERWRITE); + c.put(bbNative(4), bb(5)); + c.put(bbNative(6), bb(7)); + c.put(bbNative(8), bb(9)); + txn.commit(); + } + } + + @Test + public void closedBackwardTest() { + verify(closedBackward(bbNative(7), bbNative(3)), 6, 4); + verify(closedBackward(bbNative(6), bbNative(2)), 6, 4, 2); + verify(closedBackward(bbNative(9), bbNative(3)), 8, 6, 4); + } + + @Test + public void closedOpenBackwardTest() { + verify(closedOpenBackward(bbNative(8), bbNative(3)), 8, 6, 4); + verify(closedOpenBackward(bbNative(7), bbNative(2)), 6, 4); + verify(closedOpenBackward(bbNative(9), bbNative(3)), 8, 6, 4); + } + + @Test + public void closedOpenTest() { + verify(closedOpen(bbNative(3), bbNative(8)), 4, 6); + verify(closedOpen(bbNative(2), bbNative(6)), 2, 4); + } + + @Test + public void closedTest() { + verify(closed(bbNative(3), bbNative(7)), 4, 6); + verify(closed(bbNative(2), bbNative(6)), 2, 4, 6); + verify(closed(bbNative(1), bbNative(7)), 2, 4, 6); + } + + @Test + public void greaterThanBackwardTest() { + verify(greaterThanBackward(bbNative(6)), 4, 2); + verify(greaterThanBackward(bbNative(7)), 6, 4, 2); + verify(greaterThanBackward(bbNative(9)), 8, 6, 4, 2); + } + + @Test + public void greaterThanTest() { + verify(greaterThan(bbNative(4)), 6, 8); + verify(greaterThan(bbNative(3)), 4, 6, 8); + } + + public void iterableOnlyReturnedOnce() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }).isInstanceOf(IllegalStateException.class); + } + + @Test + public void iterate() { + populateTestDataList(); + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + + for (final KeyVal kv : c) { + assertThat(getNativeInt(kv.key())).isEqualTo(list.pollFirst()); + assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); + } + } + } + + public void iteratorOnlyReturnedOnce() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }).isInstanceOf(IllegalStateException.class); + } + + @Test + public void lessThanBackwardTest() { + verify(lessThanBackward(bbNative(5)), 8, 6); + verify(lessThanBackward(bbNative(2)), 8, 6, 4); + } + + @Test + public void lessThanTest() { + verify(lessThan(bbNative(5)), 2, 4); + verify(lessThan(bbNative(8)), 2, 4, 6); + } + + public void nextThrowsNoSuchElementExceptionIfNoMoreElements() { + Assertions.assertThatThrownBy(() -> { + populateTestDataList(); + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + final Iterator> i = c.iterator(); + while (i.hasNext()) { + final KeyVal kv = i.next(); + assertThat(getNativeInt(kv.key())).isEqualTo(list.pollFirst()); + assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); + } + assertThat(i.hasNext()).isEqualTo(false); + i.next(); + } + }).isInstanceOf(NoSuchElementException.class); + } + + @Test + public void openBackwardTest() { + verify(openBackward(bbNative(7), bbNative(2)), 6, 4); + verify(openBackward(bbNative(8), bbNative(1)), 6, 4, 2); + verify(openBackward(bbNative(9), bbNative(4)), 8, 6); + } + + @Test + public void openClosedBackwardTest() { + verify(openClosedBackward(bbNative(7), bbNative(2)), 6, 4, 2); + verify(openClosedBackward(bbNative(8), bbNative(4)), 6, 4); + verify(openClosedBackward(bbNative(9), bbNative(4)), 8, 6, 4); + } + + @Test + public void openClosedBackwardTestWithGuava() { + final Comparator guava = UnsignedBytes.lexicographicalComparator(); + final Comparator comparator = + (bb1, bb2) -> { + final byte[] array1 = new byte[bb1.remaining()]; + final byte[] array2 = new byte[bb2.remaining()]; + bb1.mark(); + bb2.mark(); + bb1.get(array1); + bb2.get(array2); + bb1.reset(); + bb2.reset(); + return guava.compare(array1, array2); + }; + final Dbi guavaDbi = env.openDbi(DB_1, comparator, MDB_CREATE); + populateDatabase(guavaDbi); + verify(openClosedBackward(bbNative(7), bbNative(2)), guavaDbi, 6, 4, 2); + verify(openClosedBackward(bbNative(8), bbNative(4)), guavaDbi, 6, 4); + } + + @Test + public void openClosedTest() { + verify(openClosed(bbNative(3), bbNative(8)), 4, 6, 8); + verify(openClosed(bbNative(2), bbNative(6)), 4, 6); + } + + @Test + public void openTest() { + verify(open(bbNative(3), bbNative(7)), 4, 6); + verify(open(bbNative(2), bbNative(8)), 4, 6); + } + + @Test + public void removeOddElements() { + final Dbi db = getDb(); + verify(db, all(), 2, 4, 6, 8); + int idx = -1; + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn)) { + final Iterator> c = ci.iterator(); + while (c.hasNext()) { + c.next(); + idx++; + if (idx % 2 == 0) { + c.remove(); + } + } + } + txn.commit(); + } + verify(db, all(), 4, 8); + } + + public void nextWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.next(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void removeWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + final KeyVal keyVal = c.next(); + assertThat(keyVal).isNotNull(); + + env.close(); + c.remove(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void hasNextWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.hasNext(); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + public void forEachRemainingWithClosedEnvTest() { + Assertions.assertThatThrownBy(() -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.forEachRemaining(keyVal -> { + }); + } + } + }).isInstanceOf(Env.AlreadyClosedException.class); + } + + private void verify(final KeyRange range, final int... expected) { + // Verify using all comparator types + final Dbi db = getDb(); + verify(range, db, expected); + } + + private void verify( + final Dbi dbi, final KeyRange range, final int... expected) { + verify(range, dbi, expected); + } + + private void verify( + final KeyRange range, final Dbi dbi, final int... expected) { + + final List results = new ArrayList<>(); + + try (Txn txn = env.txnRead(); + CursorIterable c = dbi.iterate(txn, range)) { + for (final KeyVal kv : c) { + final int key = kv.key().order(ByteOrder.nativeOrder()).getInt(); + final int val = kv.val().getInt(); + results.add(key); + assertThat(val).isEqualTo(key + 1); + } + } + + assertThat(results).hasSize(expected.length); + for (int idx = 0; idx < results.size(); idx++) { + assertThat(results.get(idx)).isEqualTo(expected[idx]); + } + } + + private Dbi getDb() { + final Dbi dbi = dbiFactory.factory.apply(env); + populateDatabase(dbi); + return dbi; + } + + + // -------------------------------------------------------------------------------- + + + private static class DbiFactory { + private final String name; + private final Function, Dbi> factory; + + private DbiFactory(String name, Function, Dbi> factory) { + this.name = name; + this.factory = factory; + } + + @Override + public String toString() { + return name; + } + } + + + // -------------------------------------------------------------------------------- + + + static class MyArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) throws Exception { + final DbiFactory defaultComparatorDb = new DbiFactory("defaultComparator", env -> + env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory nativeComparatorDb = new DbiFactory("nativeComparator", env -> + env.buildDbi() + .setDbName(DB_2) + .withNativeComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory callbackComparatorDb = new DbiFactory("callbackComparator", env -> + env.buildDbi() + .setDbName(DB_3) + .withCallbackComparator(MyArgumentProvider::buildComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory iteratorComparatorDb = new DbiFactory("iteratorComparator", env -> + env.buildDbi() + .setDbName(DB_4) + .withIteratorComparator(MyArgumentProvider::buildComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + return Stream.of( + defaultComparatorDb, + nativeComparatorDb, + callbackComparatorDb, + iteratorComparatorDb) + .map(Arguments::of); + } + + private static Comparator buildComparator(final DbiFlagSet dbiFlagSet) { + final Comparator baseComparator = BUFFER_PROXY.getComparator(DBI_FLAGS); + return (o1, o2) -> { + if (o1.remaining() != o2.remaining()) { + // Make sure LMDB is always giving us consistent key lengths. + Assertions.fail("o1: " + o1 + " " + getNativeIntOrLong(o1) + + ", o2: " + o2 + " " + getNativeIntOrLong(o2)); + } + return baseComparator.compare(o1, o2); + }; + } + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterablePerfTest.java b/src/test/java/org/lmdbjava/CursorIterablePerfTest.java new file mode 100644 index 00000000..e0a40fd9 --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterablePerfTest.java @@ -0,0 +1,198 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static com.jakewharton.byteunits.BinaryByteUnit.GIBIBYTES; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.PutFlags.MDB_APPEND; +import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; +import static org.lmdbjava.TestUtils.POSIX_MODE; +import static org.lmdbjava.TestUtils.bb; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CursorIterablePerfTest { + + // private static final int ITERATIONS = 5_000_000; + private static final int ITERATIONS = 100_000; + // private static final int ITERATIONS = 10; + + private Path file; + private Dbi dbJavaComparator; + private Dbi dbLmdbComparator; + private Dbi dbCallbackComparator; + private List> dbs = new ArrayList<>(); + private Env env; + private List data = new ArrayList<>(ITERATIONS); + + @BeforeEach + public void before() throws IOException { + file = FileUtil.createTempFile(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = + create(bufferProxy) + .setMapSize(GIBIBYTES.toBytes(1)) + .setMaxReaders(1) + .setMaxDbs(3) + .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); + + final DbiFlagSet dbiFlagSet = MDB_CREATE; + // Use a java comparator for start/stop keys only + dbJavaComparator = env.buildDbi() + .setDbName("JavaComparator") + .withDefaultComparator() + .setDbiFlags(dbiFlagSet) + .open(); + // Use LMDB comparator for start/stop keys + dbLmdbComparator = env.buildDbi() + .setDbName("LmdbComparator") + .withNativeComparator() + .setDbiFlags(dbiFlagSet) + .open(); + + // Use a java comparator for start/stop keys and as a callback comparator + dbCallbackComparator = env.buildDbi() + .setDbName("CallBackComparator") + .withCallbackComparator(bufferProxy::getComparator) + .setDbiFlags(dbiFlagSet) + .open(); + + dbs.add(dbJavaComparator); + dbs.add(dbLmdbComparator); + dbs.add(dbCallbackComparator); + + populateList(); + } + + @AfterEach + public void after() { + env.close(); + FileUtil.delete(file); + } + + private void populateList() { + for (int i = 0; i < ITERATIONS * 2; i += 2) { + data.add(i); + } + } + + private void populateDatabases(final boolean randomOrder) { + System.out.println("Clear then populate databases (randomOrder=" + randomOrder + ")"); + + final List data; + if (randomOrder) { + data = new ArrayList<>(this.data); + Collections.shuffle(data); + } else { + data = this.data; + } + + final PutFlagSet noOverwriteAndAppendFlagSet = PutFlagSet.of(MDB_NOOVERWRITE, MDB_APPEND); + + for (int round = 0; round < 3; round++) { + System.out.println("round: " + round + " -----------------------------------------"); + + for (final Dbi db : dbs) { + // Clean out the db first + try (Txn txn = env.txnWrite(); + final Cursor cursor = db.openCursor(txn)) { + while (cursor.next()) { + cursor.delete(); + } + } + + final String dbName = db.getNameAsString(StandardCharsets.UTF_8); + final Instant start = Instant.now(); + try (Txn txn = env.txnWrite()) { + for (final Integer i : data) { + if (randomOrder) { + db.put(txn, bb(i), bb(i + 1), MDB_NOOVERWRITE); + } else { + db.put(txn, bb(i), bb(i + 1), noOverwriteAndAppendFlagSet); + } + } + txn.commit(); + } + final Duration duration = Duration.between(start, Instant.now()); + System.out.println( + "DB: " + + dbName + + " - Loaded in duration: " + + duration + + ", millis: " + + duration.toMillis()); + } + } + } + + @Test + public void comparePerf_sequential() { + comparePerf(false); + } + + @Test + public void comparePerf_random() { + comparePerf(true); + } + + public void comparePerf(final boolean randomOrder) { + populateDatabases(randomOrder); + final ByteBuffer startKeyBuf = bb(data.get(0)); + final ByteBuffer stopKeyBuf = bb(data.get(data.size() - 1)); + final KeyRange keyRange = KeyRange.closed(startKeyBuf, stopKeyBuf); + + System.out.println("\nIterating over all entries"); + for (int round = 0; round < 3; round++) { + System.out.println("round: " + round + " -----------------------------------------"); + for (final Dbi db : dbs) { + final String dbName = new String(db.getName(), StandardCharsets.UTF_8); + + final Instant start = Instant.now(); + int cnt = 0; + // Exercise the stop key comparator on every entry + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn, keyRange)) { + for (final CursorIterable.KeyVal kv : c) { + cnt++; + } + } + final Duration duration = Duration.between(start, Instant.now()); + System.out.println( + "DB: " + + dbName + + " - Iterated in duration: " + + duration + + ", millis: " + + duration.toMillis() + + ", cnt: " + + cnt); + } + } + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 22f5f7c7..e48f1e69 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -43,7 +43,9 @@ import static org.lmdbjava.KeyRange.openClosedBackward; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; import static org.lmdbjava.TestUtils.DB_1; -import static org.lmdbjava.TestUtils.POSIX_MODE; +import static org.lmdbjava.TestUtils.DB_2; +import static org.lmdbjava.TestUtils.DB_3; +import static org.lmdbjava.TestUtils.DB_4; import static org.lmdbjava.TestUtils.bb; import com.google.common.primitives.UnsignedBytes; @@ -56,35 +58,68 @@ import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.lmdbjava.CursorIterable.KeyVal; -/** Test {@link CursorIterable}. */ +/** + * Test {@link CursorIterable}. + */ +@ParameterizedClass(name = "{index}: dbi: {0}") +@ArgumentsSource(CursorIterableTest.MyArgumentProvider.class) public final class CursorIterableTest { + private static final DbiFlagSet DBI_FLAGS = MDB_CREATE; + private static final BufferProxy BUFFER_PROXY = ByteBufferProxy.PROXY_OPTIMAL; + private Path file; - private Dbi db; private Env env; private Deque list; +// /** +// * Injected by {@link #data()} with appropriate runner. +// */ +// @SuppressWarnings("ClassEscapesDefinedScope") + @Parameter + public DbiFactory dbiFactory; + @BeforeEach void beforeEach() { file = FileUtil.createTempFile(); - env = - create() - .setMapSize(KIBIBYTES.toBytes(256)) - .setMaxReaders(1) - .setMaxDbs(1) - .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); - db = env.openDbi(DB_1, MDB_CREATE); - populateDatabase(db); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = create(bufferProxy) + .setMapSize(KIBIBYTES.toBytes(256)) + .setMaxReaders(1) + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + populateTestDataList(); } - private void populateDatabase(final Dbi dbi) { + @AfterEach + void afterEach() { + env.close(); + FileUtil.delete(file); + } + + + private void populateTestDataList() { list = new LinkedList<>(); list.addAll(asList(2, 3, 4, 5, 6, 7, 8, 9)); + } + + private void populateDatabase(final Dbi dbi) { try (Txn txn = env.txnWrite()) { final Cursor c = dbi.openCursor(txn); c.put(bb(2), bb(3), MDB_NOOVERWRITE); @@ -95,12 +130,6 @@ private void populateDatabase(final Dbi dbi) { } } - @AfterEach - void afterEach() { - env.close(); - FileUtil.delete(file); - } - @Test void allBackwardTest() { verify(allBackward(), 8, 6, 4, 2); @@ -179,20 +208,23 @@ void greaterThanTest() { @Test void iterableOnlyReturnedOnce() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - c.iterator(); // ok - c.iterator(); // fails - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }) .isInstanceOf(IllegalStateException.class); } @Test void iterate() { + final Dbi db = getDb(); try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { + CursorIterable c = db.iterate(txn)) { + for (final KeyVal kv : c) { assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); @@ -203,13 +235,14 @@ void iterate() { @Test void iteratorOnlyReturnedOnce() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - c.iterator(); // ok - c.iterator(); // fails - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + c.iterator(); // ok + c.iterator(); // fails + } + }) .isInstanceOf(IllegalStateException.class); } @@ -228,19 +261,21 @@ void lessThanTest() { @Test void nextThrowsNoSuchElementExceptionIfNoMoreElements() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - final Iterator> i = c.iterator(); - while (i.hasNext()) { - final KeyVal kv = i.next(); - assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); - assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); - } - assertThat(i.hasNext()).isFalse(); - i.next(); - } - }) + () -> { + final Dbi db = getDb(); + populateTestDataList(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + final Iterator> i = c.iterator(); + while (i.hasNext()) { + final KeyVal kv = i.next(); + assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); + assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); + } + assertThat(i.hasNext()).isFalse(); + i.next(); + } + }) .isInstanceOf(NoSuchElementException.class); } @@ -273,7 +308,11 @@ void openClosedBackwardTestWithGuava() { bb2.reset(); return guava.compare(array1, array2); }; - final Dbi guavaDbi = env.openDbi(DB_1, comparator, MDB_CREATE); + final Dbi guavaDbi = env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); populateDatabase(guavaDbi); verify(openClosedBackward(bb(7), bb(2)), guavaDbi, 6, 4, 2); verify(openClosedBackward(bb(8), bb(4)), guavaDbi, 6, 4); @@ -293,6 +332,7 @@ void openTest() { @Test void removeOddElements() { + final Dbi db = getDb(); verify(all(), 2, 4, 6, 8); int idx = -1; try (Txn txn = env.txnWrite()) { @@ -308,86 +348,146 @@ void removeOddElements() { } txn.commit(); } - verify(all(), 4, 8); + verify(db, all(), 4, 8); } @Test void nextWithClosedEnvTest() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.next(); - } - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.next(); + } + } + }) .isInstanceOf(Env.AlreadyClosedException.class); } @Test void removeWithClosedEnvTest() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnWrite()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - final KeyVal keyVal = c.next(); - assertThat(keyVal).isNotNull(); - - env.close(); - c.remove(); - } - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + final KeyVal keyVal = c.next(); + assertThat(keyVal).isNotNull(); + + env.close(); + c.remove(); + } + } + }) .isInstanceOf(Env.AlreadyClosedException.class); } @Test void hasNextWithClosedEnvTest() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.hasNext(); - } - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.hasNext(); + } + } + }) .isInstanceOf(Env.AlreadyClosedException.class); } @Test void forEachRemainingWithClosedEnvTest() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.forEachRemaining(keyVal -> {}); - } - } - }) + () -> { + final Dbi db = getDb(); + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.forEachRemaining(keyVal -> { + }); + } + } + }) .isInstanceOf(Env.AlreadyClosedException.class); } +// @Test +// public void testSignedVsUnsigned() { +// final ByteBuffer val1 = bb(1); +// final ByteBuffer val2 = bb(2); +// final ByteBuffer val110 = bb(110); +// final ByteBuffer val111 = bb(111); +// final ByteBuffer val150 = bb(150); +// +// final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; +// final Comparator unsignedComparator = bufferProxy.getUnsignedComparator(); +// final Comparator signedComparator = bufferProxy.getSignedComparator(); +// +// // Compare the same +// assertThat( +// unsignedComparator.compare(val1, val2), Matchers.is(signedComparator.compare(val1, val2))); +// +// // Compare differently +// assertThat( +// unsignedComparator.compare(val110, val150), +// Matchers.not(signedComparator.compare(val110, val150))); +// +// // Compare differently +// assertThat( +// unsignedComparator.compare(val111, val150), +// Matchers.not(signedComparator.compare(val111, val150))); +// +// // This will fail if the db is using a signed comparator for the start/stop keys +// for (final Dbi db : dbs) { +// db.put(val110, val110); +// db.put(val150, val150); +// +// final ByteBuffer startKeyBuf = val111; +// KeyRange keyRange = KeyRange.atLeastBackward(startKeyBuf); +// +// try (Txn txn = env.txnRead(); +// CursorIterable c = db.iterate(txn, keyRange)) { +// for (final CursorIterable.KeyVal kv : c) { +// final int key = kv.key().getInt(); +// final int val = kv.val().getInt(); +// // System.out.println("key: " + key + " val: " + val); +// assertThat(key, is(110)); +// break; +// } +// } +// } +// } + private void verify(final KeyRange range, final int... expected) { + final Dbi db = getDb(); verify(range, db, expected); } private void verify( - final KeyRange range, final Dbi dbi, final int... expected) { + final Dbi dbi, final KeyRange range, final int... expected) { + verify(range, dbi, expected); + } + + private void verify(final KeyRange range, + final Dbi dbi, + final int... expected) { + final List results = new ArrayList<>(); try (Txn txn = env.txnRead(); - CursorIterable c = dbi.iterate(txn, range)) { + CursorIterable c = dbi.iterate(txn, range)) { for (final KeyVal kv : c) { final int key = kv.key().getInt(); final int val = kv.val().getInt(); @@ -401,4 +501,71 @@ private void verify( assertThat(results.get(idx)).isEqualTo(expected[idx]); } } + + private Dbi getDb() { + final Dbi dbi = dbiFactory.factory.apply(env); + populateDatabase(dbi); + return dbi; + } + + + // -------------------------------------------------------------------------------- + + + private static class DbiFactory { + private final String name; + private final Function, Dbi> factory; + + private DbiFactory(String name, Function, Dbi> factory) { + this.name = name; + this.factory = factory; + } + + @Override + public String toString() { + return name; + } + } + + + // -------------------------------------------------------------------------------- + + + static class MyArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) throws Exception { + final DbiFactory defaultComparatorDb = new DbiFactory("defaultComparator", env -> + env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory nativeComparatorDb = new DbiFactory("nativeComparator", env -> + env.buildDbi() + .setDbName(DB_2) + .withNativeComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory callbackComparatorDb = new DbiFactory("callbackComparator", env -> + env.buildDbi() + .setDbName(DB_3) + .withCallbackComparator(BUFFER_PROXY::getComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory iteratorComparatorDb = new DbiFactory("iteratorComparator", env -> + env.buildDbi() + .setDbName(DB_4) + .withIteratorComparator(BUFFER_PROXY::getComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + return Stream.of( + defaultComparatorDb, + nativeComparatorDb, + callbackComparatorDb, + iteratorComparatorDb) + .map(Arguments::of); + } + } } diff --git a/src/test/java/org/lmdbjava/DbiBuilderTest.java b/src/test/java/org/lmdbjava/DbiBuilderTest.java new file mode 100644 index 00000000..d01f6417 --- /dev/null +++ b/src/test/java/org/lmdbjava/DbiBuilderTest.java @@ -0,0 +1,203 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.getString; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DbiBuilderTest { + + private Path file; + private Env env; + + @BeforeEach + public void before() { + file = FileUtil.createTempFile(); + env = create() + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(2) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + } + + @AfterEach + public void after() { + env.close(); + FileUtil.delete(file); + } + + @Test + public void unnamed() { + final Dbi dbi = env.buildDbi() + .withoutDbName() + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_CREATE) + .open(); + assertThat(dbi.getName()).isNull(); + assertThat(dbi.getNameAsString()).isEmpty(); + assertThat(env.getDbiNames()).isEmpty(); + assertPutAndGet(dbi); + } + + @Test + public void named() { + final Dbi dbi = env.buildDbi() + .setDbName("foo") + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_CREATE) + .open(); + + assertPutAndGet(dbi); + + assertThat(env.getDbiNames()).hasSize(1); + assertThat(new String(env.getDbiNames().get(0), StandardCharsets.UTF_8)) + .isEqualTo("foo"); + assertThat(dbi.getNameAsString()) + .isEqualTo("foo"); + assertThat(dbi.getNameAsString(StandardCharsets.UTF_8)) + .isEqualTo("foo"); + } + + @Test + public void named2() { + final Dbi dbi = env.buildDbi() + .setDbName("foo".getBytes(StandardCharsets.US_ASCII)) + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_CREATE) + .open(); + + assertPutAndGet(dbi); + + assertThat(env.getDbiNames()).hasSize(1); + assertThat(new String(env.getDbiNames().get(0), StandardCharsets.US_ASCII)) + .isEqualTo("foo"); + assertThat(dbi.getNameAsString()) + .isEqualTo("foo"); + assertThat(dbi.getNameAsString(StandardCharsets.US_ASCII)) + .isEqualTo("foo"); + } + + @Test + public void nativeComparator() { + final Dbi dbi = env.buildDbi() + .setDbName("foo") + .withNativeComparator() + .addDbiFlags(DbiFlags.MDB_CREATE) + .open(); + + assertPutAndGet(dbi); + assertThat(env.getDbiNames()).hasSize(1); + } + + @Test + public void callback() { + final Comparator proxyOptimal = ByteBufferProxy.PROXY_OPTIMAL.getComparator(); + // Compare on key length, falling back to default + final Comparator comparator = (o1, o2) -> { + final int res = Integer.compare(o1.remaining(), o2.remaining()); + if (res == 0) { + return proxyOptimal.compare(o1, o2); + } else { + return res; + } + }; + + final Dbi dbi = env.buildDbi() + .setDbName("foo") + .withCallbackComparator(ignored -> comparator) + .addDbiFlags(DbiFlags.MDB_CREATE) + .open(); + + TestUtils.doWithWriteTxn(env, txn -> { + dbi.put(txn, bb("fox"), bb("val_1")); + dbi.put(txn, bb("rabbit"), bb("val_2")); + dbi.put(txn, bb("deer"), bb("val_3")); + dbi.put(txn, bb("badger"), bb("val_4")); + txn.commit(); + }); + + final List keys = new ArrayList<>(); + TestUtils.doWithReadTxn(env, txn -> { + try (CursorIterable cursorIterable = dbi.iterate(txn)) { + final Iterator> iterator = cursorIterable.iterator(); + iterator.forEachRemaining(keyVal -> { + keys.add(getString(keyVal.key())); + }); + } + }); + assertThat(keys).containsExactly( + "fox", + "deer", + "badger", + "rabbit"); + } + + @Test + public void flags() { + final Dbi dbi = env.buildDbi() + .setDbName("foo") + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_DUPSORT, DbiFlags.MDB_DUPFIXED) // Will get overwritten + .setDbiFlags() // clear them + .addDbiFlags(DbiFlags.MDB_CREATE) // Not a dbi flag as far as lmdb is concerned. + .addDbiFlags(DbiFlags.MDB_INTEGERKEY) + .addDbiFlags(DbiFlags.MDB_REVERSEKEY) + .open(); + + assertPutAndGet(dbi); + + assertThat(env.getDbiNames()).hasSize(1); + assertThat(new String(env.getDbiNames().get(0), StandardCharsets.UTF_8)) + .isEqualTo("foo"); + + TestUtils.doWithReadTxn(env, readTxn -> { + assertThat(dbi.listFlags(readTxn)) + .containsExactlyInAnyOrder( + DbiFlags.MDB_INTEGERKEY, + DbiFlags.MDB_REVERSEKEY); + }); + } + + private void assertPutAndGet(Dbi dbi) { + try (Txn writeTxn = env.txnWrite()) { + dbi.put(writeTxn, bb(123), bb(123_000)); + writeTxn.commit(); + } + + try (Txn readTxn = env.txnRead()) { + final ByteBuffer byteBuffer = dbi.get(readTxn, bb(123)); + assertThat(byteBuffer).isNotNull(); + final int val = byteBuffer.getInt(); + assertThat(val).isEqualTo(123_000); + } + } +} diff --git a/src/test/java/org/lmdbjava/DbiFlagSetTest.java b/src/test/java/org/lmdbjava/DbiFlagSetTest.java new file mode 100644 index 00000000..aa66313f --- /dev/null +++ b/src/test/java/org/lmdbjava/DbiFlagSetTest.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashSet; +import org.junit.jupiter.api.Test; + +public class DbiFlagSetTest { + + @Test + public void testEmpty() { + final DbiFlagSet dbiFlagSet = DbiFlagSet.empty(); + assertThat(dbiFlagSet.getMask()).isEqualTo(0); + assertThat(dbiFlagSet.size()).isEqualTo(0); + assertThat(dbiFlagSet.isEmpty()).isEqualTo(true); + assertThat(dbiFlagSet.isSet(DbiFlags.MDB_REVERSEDUP)).isEqualTo(false); + final DbiFlagSet dbiFlagSet2 = DbiFlagSet.builder() + .build(); + assertThat(dbiFlagSet).isEqualTo(dbiFlagSet2); + assertThat(dbiFlagSet).isNotEqualTo(DbiFlagSet.of(DbiFlags.MDB_CREATE)); + assertThat(dbiFlagSet).isNotEqualTo(DbiFlagSet.of(DbiFlags.MDB_CREATE, DbiFlags.MDB_DUPSORT)); + assertThat(dbiFlagSet).isNotEqualTo(DbiFlagSet.builder() + .setFlag(DbiFlags.MDB_CREATE) + .setFlag(DbiFlags.MDB_DUPFIXED) + .build()); + } + + @Test + public void testOf() { + final DbiFlags dbiFlag = DbiFlags.MDB_CREATE; + final DbiFlagSet dbiFlagSet = DbiFlagSet.of(dbiFlag); + assertThat(dbiFlagSet.getMask()).isEqualTo(MaskedFlag.mask(dbiFlag)); + assertThat(dbiFlagSet.size()).isEqualTo(1); + assertThat(dbiFlagSet.isSet(DbiFlags.MDB_REVERSEDUP)).isEqualTo(false); + for (DbiFlags flag : dbiFlagSet) { + assertThat(dbiFlagSet.isSet(flag)).isEqualTo(true); + } + + final DbiFlagSet dbiFlagSet2 = DbiFlagSet.builder() + .setFlag(dbiFlag) + .build(); + assertThat(dbiFlagSet).isEqualTo(dbiFlagSet2); + } + + @Test + public void testOf2() { + final DbiFlags dbiFlag1 = DbiFlags.MDB_CREATE; + final DbiFlags dbiFlag2 = DbiFlags.MDB_INTEGERKEY; + final DbiFlagSet dbiFlagSet = DbiFlagSet.of(dbiFlag1, dbiFlag2); + assertThat(dbiFlagSet.getMask()).isEqualTo(MaskedFlag.mask(dbiFlag1, dbiFlag2)); + assertThat(dbiFlagSet.size()).isEqualTo(2); + assertThat(dbiFlagSet.isSet(DbiFlags.MDB_REVERSEDUP)).isEqualTo(false); + for (DbiFlags flag : dbiFlagSet) { + assertThat(dbiFlagSet.isSet(flag)).isEqualTo(true); + } + } + + @Test + public void testBuilder() { + final DbiFlags dbiFlag1 = DbiFlags.MDB_CREATE; + final DbiFlags dbiFlag2 = DbiFlags.MDB_INTEGERKEY; + final DbiFlagSet dbiFlagSet = DbiFlagSet.builder() + .setFlag(dbiFlag1) + .setFlag(dbiFlag2) + .build(); + assertThat(dbiFlagSet.getMask()).isEqualTo(MaskedFlag.mask(dbiFlag1, dbiFlag2)); + assertThat(dbiFlagSet.size()).isEqualTo(2); + assertThat(dbiFlagSet.isSet(DbiFlags.MDB_REVERSEDUP)).isEqualTo(false); + for (DbiFlags flag : dbiFlagSet) { + assertThat(dbiFlagSet.isSet(flag)).isEqualTo(true); + } + final DbiFlagSet dbiFlagSet2 = DbiFlagSet.builder() + .withFlags(dbiFlag1, dbiFlag2) + .build(); + final DbiFlagSet dbiFlagSet3 = DbiFlagSet.builder() + .withFlags(new HashSet<>(Arrays.asList(dbiFlag1, dbiFlag2))) + .build(); + assertThat(dbiFlagSet).isEqualTo(dbiFlagSet2); + assertThat(dbiFlagSet).isEqualTo(dbiFlagSet3); + } +} diff --git a/src/test/java/org/lmdbjava/DbiTest.java b/src/test/java/org/lmdbjava/DbiTest.java index 23b6790e..7302018c 100644 --- a/src/test/java/org/lmdbjava/DbiTest.java +++ b/src/test/java/org/lmdbjava/DbiTest.java @@ -29,14 +29,20 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.ByteArrayProxy.PROXY_BA; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; -import static org.lmdbjava.DbiFlags.*; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.DbiFlags.MDB_DUPSORT; +import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; +import static org.lmdbjava.DbiFlags.MDB_REVERSEKEY; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; import static org.lmdbjava.GetOp.MDB_SET_KEY; import static org.lmdbjava.KeyRange.atMost; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; -import static org.lmdbjava.TestUtils.*; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.ba; +import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.fromBa; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -44,11 +50,15 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; -import java.util.concurrent.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; -import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Supplier; import java.util.function.ToIntFunction; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -59,7 +69,9 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.LmdbNativeException.ConstantDerivedException; -/** Test {@link Dbi}. */ +/** + * Test {@link Dbi}. + */ public final class DbiTest { private Path file; @@ -97,7 +109,11 @@ void afterEach() { void close() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = env.buildDbi() + .setDbName(DB_1) + .withDefaultComparator() + .addDbiFlag(MDB_CREATE) + .open(); db.put(bb(1), bb(42)); db.close(); db.put(bb(2), bb(42)); // error @@ -145,7 +161,7 @@ private void doCustomComparator( txn.commit(); } try (Txn txn = env.txnRead(); - CursorIterable ci = db.iterate(txn, atMost(serializer.apply(4)))) { + CursorIterable ci = db.iterate(txn, atMost(serializer.apply(4)))) { final Iterator> iter = ci.iterator(); assertThat(deserializer.applyAsInt(iter.next().key())).isEqualTo(8); assertThat(deserializer.applyAsInt(iter.next().key())).isEqualTo(6); @@ -178,11 +194,11 @@ void dbiWithComparatorThreadSafetyByteArray() { private void doDbiWithComparatorThreadSafety( Env env, - Function> comparator, + Supplier> comparatorSupplier, IntFunction serializer, ToIntFunction deserializer) { - final DbiFlags[] flags = new DbiFlags[] {MDB_CREATE, MDB_INTEGERKEY}; - final Comparator c = comparator.apply(flags); + final DbiFlags[] flags = new DbiFlags[]{MDB_CREATE, MDB_INTEGERKEY}; + final Comparator c = comparatorSupplier.get(); final Dbi db = env.openDbi(DB_1, c, true, flags); final List keys = range(0, 1_000).boxed().collect(toList()); @@ -207,7 +223,7 @@ private void doDbiWithComparatorThreadSafety( } try (Txn txn = env.txnRead(); - CursorIterable ci = db.iterate(txn)) { + CursorIterable ci = db.iterate(txn)) { final Iterator> iter = ci.iterator(); final List result = new ArrayList<>(); while (iter.hasNext()) { @@ -287,8 +303,8 @@ void getName() { @Test void getNamesWhenDbisPresent() { - final byte[] dbHello = new byte[] {'h', 'e', 'l', 'l', 'o'}; - final byte[] dbWorld = new byte[] {'w', 'o', 'r', 'l', 'd'}; + final byte[] dbHello = new byte[]{'h', 'e', 'l', 'l', 'o'}; + final byte[] dbWorld = new byte[]{'w', 'o', 'r', 'l', 'd'}; env.openDbi(dbHello, MDB_CREATE); env.openDbi(dbWorld, MDB_CREATE); final List dbiNames = env.getDbiNames(); @@ -364,11 +380,11 @@ void putCommitGet() { void putCommitGetByteArray() { FileUtil.useTempFile( file -> { - try (Env envBa = - create(PROXY_BA) - .setMapSize(MEBIBYTES.toBytes(64)) - .setMaxReaders(1) - .setMaxDbs(2) + try (Env envBa = + create(PROXY_BA) + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(1) + .setMaxDbs(2) .open(file.toFile(), MDB_NOSUBDIR)) { final Dbi db = envBa.openDbi(DB_1, MDB_CREATE); try (Txn txn = envBa.txnWrite()) { diff --git a/src/test/java/org/lmdbjava/EnvFlagSetTest.java b/src/test/java/org/lmdbjava/EnvFlagSetTest.java new file mode 100644 index 00000000..485aae13 --- /dev/null +++ b/src/test/java/org/lmdbjava/EnvFlagSetTest.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashSet; +import org.junit.jupiter.api.Test; + +public class EnvFlagSetTest { + + @Test + public void testEmpty() { + final EnvFlagSet envFlagSet = EnvFlagSet.empty(); + assertThat(envFlagSet.getMask()).isEqualTo(0); + assertThat(envFlagSet.size()).isEqualTo(0); + assertThat(envFlagSet.isEmpty()).isEqualTo(true); + assertThat(envFlagSet.isSet(EnvFlags.MDB_NOSUBDIR)).isEqualTo(false); + final EnvFlagSet envFlagSet2 = EnvFlagSet.builder() + .build(); + assertThat(envFlagSet).isEqualTo(envFlagSet2); + assertThat(envFlagSet).isNotEqualTo(EnvFlagSet.of(EnvFlags.MDB_FIXEDMAP)); + assertThat(envFlagSet).isNotEqualTo(EnvFlagSet.of(EnvFlags.MDB_FIXEDMAP, EnvFlags.MDB_NORDAHEAD)); + assertThat(envFlagSet).isNotEqualTo(EnvFlagSet.builder() + .setFlag(EnvFlags.MDB_FIXEDMAP) + .setFlag(EnvFlags.MDB_NORDAHEAD) + .build()); + } + + @Test + public void testOf() { + final EnvFlags envFlag = EnvFlags.MDB_FIXEDMAP; + final EnvFlagSet envFlagSet = EnvFlagSet.of(envFlag); + assertThat(envFlagSet.getMask()).isEqualTo(MaskedFlag.mask(envFlag)); + assertThat(envFlagSet.size()).isEqualTo(1); + assertThat(envFlagSet.isSet(EnvFlags.MDB_NOSUBDIR)).isEqualTo(false); + for (EnvFlags flag : envFlagSet) { + assertThat(envFlagSet.isSet(flag)).isEqualTo(true); + } + + final EnvFlagSet envFlagSet2 = EnvFlagSet.builder() + .setFlag(envFlag) + .build(); + assertThat(envFlagSet).isEqualTo(envFlagSet2); + } + + @Test + public void testOf2() { + final EnvFlags envFlag1 = EnvFlags.MDB_FIXEDMAP; + final EnvFlags envFlag2 = EnvFlags.MDB_NORDAHEAD; + final EnvFlagSet envFlagSet = EnvFlagSet.of(envFlag1, envFlag2); + assertThat(envFlagSet.getMask()).isEqualTo(MaskedFlag.mask(envFlag1, envFlag2)); + assertThat(envFlagSet.size()).isEqualTo(2); + assertThat(envFlagSet.isSet(EnvFlags.MDB_WRITEMAP)).isEqualTo(false); + for (EnvFlags flag : envFlagSet) { + assertThat(envFlagSet.isSet(flag)).isEqualTo(true); + } + } + + @Test + public void testBuilder() { + final EnvFlags envFlag1 = EnvFlags.MDB_FIXEDMAP; + final EnvFlags envFlag2 = EnvFlags.MDB_NORDAHEAD; + final EnvFlagSet envFlagSet = EnvFlagSet.builder() + .setFlag(envFlag1) + .setFlag(envFlag2) + .build(); + assertThat(envFlagSet.getMask()).isEqualTo(MaskedFlag.mask(envFlag1, envFlag2)); + assertThat(envFlagSet.size()).isEqualTo(2); + assertThat(envFlagSet.isSet(EnvFlags.MDB_NOTLS)).isEqualTo(false); + for (EnvFlags flag : envFlagSet) { + assertThat(envFlagSet.isSet(flag)).isEqualTo(true); + } + final EnvFlagSet envFlagSet2 = EnvFlagSet.builder() + .withFlags(envFlag1, envFlag2) + .build(); + final EnvFlagSet envFlagSet3 = EnvFlagSet.builder() + .withFlags(new HashSet<>(Arrays.asList(envFlag1, envFlag2))) + .build(); + assertThat(envFlagSet).isEqualTo(envFlagSet2); + assertThat(envFlagSet).isEqualTo(envFlagSet3); + } +} diff --git a/src/test/java/org/lmdbjava/KeyRangeTest.java b/src/test/java/org/lmdbjava/KeyRangeTest.java index 0f77b92f..2e8854b9 100644 --- a/src/test/java/org/lmdbjava/KeyRangeTest.java +++ b/src/test/java/org/lmdbjava/KeyRangeTest.java @@ -194,7 +194,10 @@ private void verify(final KeyRange range, final int... expected) { IteratorOp op; do { - op = range.getType().iteratorOp(range.getStart(), range.getStop(), buff, Integer::compare); + final Integer finalBuff = buff; + final RangeComparator rangeComparator = + new CursorIterable.JavaRangeComparator<>(range, Integer::compareTo, () -> finalBuff); + op = range.getType().iteratorOp(buff, rangeComparator); switch (op) { case CALL_NEXT_OP: buff = cursor.apply(range.getType().nextOp(), range.getStart()); diff --git a/src/test/java/org/lmdbjava/PutFlagSetTest.java b/src/test/java/org/lmdbjava/PutFlagSetTest.java new file mode 100644 index 00000000..23cf65a4 --- /dev/null +++ b/src/test/java/org/lmdbjava/PutFlagSetTest.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +public class PutFlagSetTest { + + @Test + public void testEmpty() { + final PutFlagSet putFlagSet = PutFlagSet.empty(); + assertThat(putFlagSet.getMask()).isEqualTo(0); + assertThat(putFlagSet.size()).isEqualTo(0); + assertThat(putFlagSet.isEmpty()).isEqualTo(true); + assertThat(putFlagSet.isSet(PutFlags.MDB_MULTIPLE)).isEqualTo(false); + final PutFlagSet putFlagSet2 = PutFlagSet.builder() + .build(); + assertThat(putFlagSet).isEqualTo(putFlagSet2); + assertThat(putFlagSet).isNotEqualTo(PutFlagSet.of(PutFlags.MDB_APPEND)); + assertThat(putFlagSet).isNotEqualTo(PutFlagSet.of(PutFlags.MDB_APPEND, PutFlags.MDB_RESERVE)); + assertThat(putFlagSet).isNotEqualTo(PutFlagSet.builder() + .setFlag(PutFlags.MDB_CURRENT) + .setFlag(PutFlags.MDB_MULTIPLE) + .build()); + } + + @Test + public void testOf() { + final PutFlags putFlag = PutFlags.MDB_APPEND; + final PutFlagSet putFlagSet = PutFlagSet.of(putFlag); + assertThat(putFlagSet.getMask()).isEqualTo(MaskedFlag.mask(putFlag)); + assertThat(putFlagSet.size()).isEqualTo(1); + assertThat(putFlagSet.isSet(PutFlags.MDB_MULTIPLE)).isEqualTo(false); + for (PutFlags flag : putFlagSet) { + assertThat(putFlagSet.isSet(flag)).isEqualTo(true); + } + + final PutFlagSet putFlagSet2 = PutFlagSet.builder() + .setFlag(putFlag) + .build(); + assertThat(putFlagSet).isEqualTo(putFlagSet2); + } + + @Test + public void testOf2() { + final PutFlags putFlag1 = PutFlags.MDB_APPEND; + final PutFlags putFlag2 = PutFlags.MDB_NOOVERWRITE; + final PutFlagSet putFlagSet = PutFlagSet.of(putFlag1, putFlag2); + assertThat(putFlagSet.getMask()).isEqualTo(MaskedFlag.mask(putFlag1, putFlag2)); + assertThat(putFlagSet.size()).isEqualTo(2); + assertThat(putFlagSet.isSet(PutFlags.MDB_MULTIPLE)).isEqualTo(false); + for (PutFlags flag : putFlagSet) { + assertThat(putFlagSet.isSet(flag)).isEqualTo(true); + } + } + + @Test + public void testBuilder() { + final PutFlags putFlag1 = PutFlags.MDB_APPEND; + final PutFlags putFlag2 = PutFlags.MDB_NOOVERWRITE; + final PutFlagSet putFlagSet = PutFlagSet.builder() + .setFlag(putFlag1) + .setFlag(putFlag2) + .build(); + assertThat(putFlagSet.getMask()).isEqualTo(MaskedFlag.mask(putFlag1, putFlag2)); + assertThat(putFlagSet.size()).isEqualTo(2); + assertThat(putFlagSet.isSet(PutFlags.MDB_MULTIPLE)).isEqualTo(false); + for (PutFlags flag : putFlagSet) { + assertThat(putFlagSet.isSet(flag)).isEqualTo(true); + } + final PutFlagSet putFlagSet2 = PutFlagSet.builder() + .withFlags(putFlag1, putFlag2) + .build(); + final PutFlagSet putFlagSet3 = PutFlagSet.builder() + .withFlags(new HashSet<>(Arrays.asList(putFlag1, putFlag2))) + .build(); + assertThat(putFlagSet).isEqualTo(putFlagSet2); + assertThat(putFlagSet).isEqualTo(putFlagSet3); + } + + @Test + public void testAddFlagVsCheckPresence() { + + final int cnt = 10_000_000; + final int[] arr = new int[cnt]; + final List flagSets = IntStream.range(0, cnt) + .boxed() + .map(i -> PutFlagSet.of(PutFlags.MDB_APPEND, PutFlags.MDB_NOOVERWRITE, PutFlags.MDB_RESERVE)) + .collect(Collectors.toList()); + + Instant time; + for (int i = 0; i < 5; i++) { + time = Instant.now(); + for (int j = 0; j < flagSets.size(); j++) { + PutFlagSet flagSet = flagSets.get(j); + if (!flagSet.isSet(PutFlags.MDB_RESERVE)) { + throw new RuntimeException("Not set"); + } + arr[j] = flagSet.getMask(); + } + System.out.println("Check: " + Duration.between(time, Instant.now())); + + time = Instant.now(); + for (int j = 0; j < flagSets.size(); j++) { + PutFlagSet flagSet = flagSets.get(j); + final int mask = flagSet.getMaskWith(PutFlags.MDB_RESERVE); + arr[j] = mask; + } + System.out.println("Append:" + Duration.between(time, Instant.now())); + } + } +} diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index c0203264..68d988d1 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -23,6 +23,11 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; import org.agrona.MutableDirectBuffer; import org.agrona.concurrent.UnsafeBuffer; @@ -30,6 +35,9 @@ final class TestUtils { public static final String DB_1 = "test-db-1"; + public static final String DB_2 = "test-db-2"; + public static final String DB_3 = "test-db-3"; + public static final String DB_4 = "test-db-2"; public static final int POSIX_MODE = 0664; @@ -51,6 +59,58 @@ static ByteBuffer bb(final int value) { return bb; } + static ByteBuffer bb(final String value) { + final ByteBuffer bb = allocateDirect(100); + if (value != null) { + bb.put(value.getBytes(StandardCharsets.UTF_8)); + bb.flip(); + } + return bb; + } + + static ByteBuffer bbNative(final int value) { + final ByteBuffer bb = allocateDirect(Integer.BYTES) + .order(ByteOrder.nativeOrder()); + bb.putInt(value).flip(); + return bb; + } + + static ByteBuffer bbNative(final long value) { + final ByteBuffer bb = allocateDirect(Long.BYTES) + .order(ByteOrder.nativeOrder()); + bb.putLong(value).flip(); + return bb; + } + + static int getNativeInt(final ByteBuffer bb) { + final int val = bb.order(ByteOrder.nativeOrder()) + .getInt(); + bb.rewind(); + return val; + } + + static long getNativeLong(final ByteBuffer bb) { + final long val = bb.order(ByteOrder.nativeOrder()) + .getLong(); + bb.rewind(); + return val; + } + + static long getNativeIntOrLong(final ByteBuffer bb) { + if (bb.remaining() == BYTES) { + return getNativeInt(bb); + } else { + return getNativeLong(bb); + } + } + + static String getString(final ByteBuffer bb) { + final String str = StandardCharsets.UTF_8.decode(bb) + .toString(); + bb.rewind(); + return str; + } + static void invokePrivateConstructor(final Class clazz) { try { final Constructor c = clazz.getDeclaredConstructor(); @@ -76,4 +136,36 @@ static ByteBuf nb(final int value) { b.writeInt(value); return b; } + + static void doWithReadTxn(final Env env, final Consumer> work) { + Objects.requireNonNull(env); + Objects.requireNonNull(work); + try (Txn readTxn = env.txnRead()) { + work.accept(readTxn); + } + } + + static R getWithReadTxn(final Env env, final Function, R> work) { + Objects.requireNonNull(env); + Objects.requireNonNull(work); + try (Txn readTxn = env.txnRead()) { + return work.apply(readTxn); + } + } + + static void doWithWriteTxn(final Env env, final Consumer> work) { + Objects.requireNonNull(env); + Objects.requireNonNull(work); + try (Txn readTxn = env.txnWrite()) { + work.accept(readTxn); + } + } + + static R getWithWriteTxn(final Env env, final Function, R> work) { + Objects.requireNonNull(env); + Objects.requireNonNull(work); + try (Txn readTxn = env.txnWrite()) { + return work.apply(readTxn); + } + } } diff --git a/src/test/java/org/lmdbjava/TxnFlagSetTest.java b/src/test/java/org/lmdbjava/TxnFlagSetTest.java new file mode 100644 index 00000000..58f75aa6 --- /dev/null +++ b/src/test/java/org/lmdbjava/TxnFlagSetTest.java @@ -0,0 +1,96 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.HashSet; +import org.junit.jupiter.api.Test; + +public class TxnFlagSetTest { + + @Test + void testSingleEnum() { + final TxnFlagSet txnFlagSet = TxnFlags.MDB_RDONLY_TXN; + assertThat(txnFlagSet.getMask()).isEqualTo(MaskedFlag.mask(TxnFlags.MDB_RDONLY_TXN)); + assertThat(txnFlagSet.size()).isEqualTo(1); + for (TxnFlags flag : txnFlagSet) { + assertThat(txnFlagSet.isSet(flag)).isEqualTo(true); + } + + final TxnFlagSet txnFlagSet2 = TxnFlagSet.builder() + .setFlag(TxnFlags.MDB_RDONLY_TXN) + .build(); + assertThat(txnFlagSet2.getFlags()).containsExactlyElementsOf(txnFlagSet.getFlags()); + assertThat(txnFlagSet.areAnySet(TxnFlags.MDB_RDONLY_TXN)).isTrue(); + assertThat(txnFlagSet.areAnySet(TxnFlagSet.empty())).isFalse(); + } + + @Test + public void testEmpty() { + final TxnFlagSet txnFlagSet = TxnFlagSet.empty(); + assertThat(txnFlagSet.getMask()).isEqualTo(0); + assertThat(txnFlagSet.size()).isEqualTo(0); + assertThat(txnFlagSet.isEmpty()).isEqualTo(true); + assertThat(txnFlagSet.isSet(TxnFlags.MDB_RDONLY_TXN)).isEqualTo(false); + final TxnFlagSet txnFlagSet2 = TxnFlagSet.builder() + .build(); + assertThat(txnFlagSet).isEqualTo(txnFlagSet2); + assertThat(txnFlagSet).isNotEqualTo(TxnFlagSet.of(TxnFlags.MDB_RDONLY_TXN)); + assertThat(txnFlagSet).isNotEqualTo(TxnFlagSet.builder() + .setFlag(TxnFlags.MDB_RDONLY_TXN) + .build()); + } + + @Test + public void testOf() { + final TxnFlags txnFlag = TxnFlags.MDB_RDONLY_TXN; + final TxnFlagSet txnFlagSet = TxnFlagSet.of(txnFlag); + assertThat(txnFlagSet.getMask()).isEqualTo(MaskedFlag.mask(txnFlag)); + assertThat(txnFlagSet.size()).isEqualTo(1); + for (TxnFlags flag : txnFlagSet) { + assertThat(txnFlagSet.isSet(flag)).isEqualTo(true); + } + + final TxnFlagSet txnFlagSet2 = TxnFlagSet.builder() + .setFlag(txnFlag) + .build(); + assertThat(txnFlagSet).isEqualTo(txnFlagSet2); + } + + @Test + public void testBuilder() { + final TxnFlags txnFlag1 = TxnFlags.MDB_RDONLY_TXN; + final TxnFlagSet txnFlagSet = TxnFlagSet.builder() + .setFlag(txnFlag1) + .build(); + assertThat(txnFlagSet.getMask()).isEqualTo(MaskedFlag.mask(txnFlag1)); + assertThat(txnFlagSet.size()).isEqualTo(1); + assertThat(txnFlagSet.isSet(TxnFlags.MDB_RDONLY_TXN)).isEqualTo(true); + for (TxnFlags flag : txnFlagSet) { + assertThat(txnFlagSet.isSet(flag)).isEqualTo(true); + } + final TxnFlagSet txnFlagSet2 = TxnFlagSet.builder() + .withFlags(txnFlag1) + .build(); + final TxnFlagSet txnFlagSet3 = TxnFlagSet.builder() + .withFlags(new HashSet<>(Collections.singletonList(txnFlag1))) + .build(); + assertThat(txnFlagSet).isEqualTo(txnFlagSet2); + assertThat(txnFlagSet).isEqualTo(txnFlagSet3); + } +}